Build and Ship a React Productivity App
Build a daily priority app with React and deploy it live on Vercel.
Introduction
30 Second Summary
You know that feeling when you start your day with 15 things on your to-do list, get scattered jumping between them, and end up finishing none of them? Picking fewer priorities is the fix, but most apps let you add unlimited tasks.
In this project, you will build a "Daily Focus" app that forces you to pick only 3 priorities per day, track your progress, and reset fresh each morning. You will deploy it live on the internet so anyone can use it.
What You'll Build
A React productivity micro-app scaffolded with Vite and deployed live on Vercel.
By the end of this project, you'll have:
- A product definition that identifies the user, the problem, and the success metric before any code is written.
- A functional priority-setting UI with a max-3 constraint, progress tracking, and localStorage persistence that resets daily.
- A live deployment on Vercel connected to GitHub, with automatic redeployment on every push.
- Secret Mission: Add a daily streak counter that tracks consecutive days of focus.
Are there any prerequisites?
You should have basic HTML, CSS, and JavaScript knowledge. Some familiarity with React concepts (components, state) is helpful but not required.
You will need VS Code, Node.js, npm, and a GitHub account already set up on your machine. A free Vercel account is also needed, but setup is included in the project.
Set Up the Project
Every product starts with a foundation. Before you build any features, you need a working project that you can run locally and deploy from.
In this step, you'll scaffold a React project using Vite, verify it runs on your machine, and push the code to GitHub. This gives you a clean starting point with version control and a remote backup from day one.
In this step, get ready to:
- Scaffold a React project with Vite.
- Verify the dev server runs locally.
- Push your code to GitHub.
Create the project with Vite
Vite is a build tool that scaffolds a complete React project in seconds. You'll use it to generate all the starter files your app needs.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project by running this command:
npm create vite@latest daily-focus -- --template react
What does this command do?
This tells npm to download and run the create-vite tool. The daily-focus part names your project folder. The --template react flag tells Vite to use the React starter template so you skip the interactive setup.
Seeing a prompt about installing a package?
If you see a message like "Need to install the following packages" followed by "Ok to proceed?", type y and press Enter. This is npm downloading the scaffolding tool for the first time.
- Navigate into your new project folder and install its dependencies by running these commands:
cd daily-focus
npm install
What does npm install do?
Your project has a package.json file listing the libraries it needs (like React). Running npm install downloads all of them into a node_modules folder so your app can use them.
- Confirm the project files exist by running:
ls
You should see files like index.html, package.json, vite.config.js, and folders like src and node_modules in the output.
Command not found or scaffold failed?
If you see a "command not found" error for npm, your Node.js installation may not be on your system PATH. Close and reopen your terminal, then try again.
Vite requires Node.js version 20.19+ or 22.12+. Run node --version to check yours. If it's lower, update Node.js from the official site at nodejs.org.
help me fix an npm or Node.js error when scaffolding my Vite project
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the daily-focus folder, and click Open.
Why open the project in VS Code?
VS Code gives you a file explorer, a code editor, and an integrated terminal all in one window. You'll use its built-in terminal for the remaining commands in this project.
Start the dev server and verify
Vite includes a development server that runs your app locally. This lets you see changes instantly without deploying anything.
- Open VS Code's integrated terminal by pressing Ctrl+` (backtick key, found above Tab on your keyboard).
- Start the dev server by running:
npm run dev
You should see output showing a local URL. Open http://localhost:5173/ in your browser.
You should see the default Vite + React starter page with spinning logos and a counter button.
Don't see the starter page?
Make sure you ran npm install before npm run dev. Without installed dependencies, the server cannot start.
Check that nothing else is already using port 5173 on your machine. If the terminal shows a different port number, use that URL instead.
help me troubleshoot my Vite dev server
- Once you've confirmed the page loads, go back to your terminal and press Ctrl+C to stop the dev server.
Initialize Git and connect to GitHub
Version control tracks every change you make. Pushing to GitHub also gives you a remote backup and sets you up for automatic deployments later.
- Initialize Git and make your first commit by running these commands:
git init
git add .
git commit -m "Initial scaffold"
What do these commands do?
- git init creates a new Git repository in your project folder.
- git add . stages all your files for the first commit.
- git commit -m "Initial scaffold" saves a snapshot of your project with a descriptive message.
- Open GitHub in your browser at github.com and sign in.
- Click the + button in the top-right corner of the page.
- Click New repository.
- Enter daily-focus as the repository name.
- Leave all other settings at their defaults (Public, no README, no .gitignore).
- Click Create repository.
GitHub shows you a quick setup page with commands. You've already done the init and commit, so you only need the push commands.
- Connect your local project to GitHub and push your code by running these commands:
git remote add origin https://github.com/[[YOUR_USERNAME="your GitHub username"]]/daily-focus.git
git branch -M main
git push -u origin main
What do these commands do?
- git remote add origin links your local repo to the GitHub repo you just created.
- git branch -M main renames your default branch to main.
- git push -u origin main uploads your code to GitHub.
- Refresh your GitHub repository page in the browser.
You should see your project files listed on the repository page, including index.html, src/, and package.json.
Push failed or authentication error?
If Git asks for a password and rejects it, GitHub no longer accepts passwords for HTTPS pushes. You need a Personal Access Token (PAT) instead. Go to GitHub > Settings > Developer settings > Personal access tokens to create one, then use it as your password.
If you see "remote already exists", you may have run the git remote add command twice. Run git remote -v to check the current remote URL.
help me push my code to GitHub
Your foundation is in place. You have a working React project, a running dev server, and code backed up on GitHub. Next up, you'll define the product and build the UI shell.
Define the Product and Build the UI Shell
You have a working React project on your machine. Before jumping into code, product engineers always start by defining who they're building for and what problem they're solving.
In this step, you'll write a product definition, build a static UI shell, and apply clean styles. The shell gives you the visual structure to fill with real functionality in the next step.
In this step, get ready to:
- Define the product's user, problem, and success metric.
- Build the UI component with a title, input, and progress indicator.
- Style the app with clean CSS and update the page title.
Write the product definition
Before writing any component code, define who this product is for and what problem it solves. This forces clarity on the actual goal before you get lost in implementation details.
Here's the product definition you'll include at the top of your app file:
/*
* PRODUCT: Daily Focus
* USER: Students and professionals overwhelmed by long to-do lists
* PROBLEM: Too many tasks leads to scattered attention and nothing getting done
* SOLUTION: Force a max of 3 priorities per day. Simple. Focused. Done.
* SUCCESS METRIC: User completes all 3 priorities before end of day
*/
What is product thinking?
Product thinking means defining the user, problem, and success metric before building anything. It prevents you from building features nobody needs.
Each line in this comment captures a decision. The USER identifies who you're helping. The PROBLEM states what's broken for them. The SUCCESS METRIC tells you how you'll know the product works.
Build the UI shell
- Start the dev server so you can see changes live as you edit files. Run this in your terminal:
npm run dev
What does this command do?
This starts Vite's development server with hot module replacement. Every time you save a file, the browser updates automatically without a manual refresh.
- In VS Code, open src/App.jsx from the Explorer sidebar on the left.
- Select all the existing code and delete it.
- Replace it with this code, which includes the product definition and a static UI shell:
/*
* PRODUCT: Daily Focus
* USER: Students and professionals overwhelmed by long to-do lists
* PROBLEM: Too many tasks leads to scattered attention and nothing getting done
* SOLUTION: Force a max of 3 priorities per day. Simple. Focused. Done.
* SUCCESS METRIC: User completes all 3 priorities before end of day
*/
import './App.css'
function App() {
const today = new Date().toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric'
})
return (
<div className="app">
<header className="header">
<h1>Daily Focus</h1>
<p className="date">{today}</p>
</header>
<div className="progress">
<span className="progress-text">0/3 complete</span>
<div className="progress-bar">
<div className="progress-fill" style={{ width: '0%' }}></div>
</div>
</div>
<div className="input-section">
<input type="text" placeholder="What's your priority?" className="priority-input" />
<button className="add-button">Add</button>
</div>
<ul className="priority-list"></ul>
<p className="empty-state">Add up to 3 priorities for today. Focus on what matters most.</p>
</div>
)
}
export default App
What does this code do?
- The today variable formats the current date into a readable string like "Monday, June 15".
- The progress section shows a "0/3 complete" counter and an empty progress bar. These are hardcoded for now.
- The input-section contains a text input and an Add button. They don't do anything yet.
- The empty-state paragraph shows placeholder text when no priorities exist.
- Save src/App.jsx.
- Check your browser at http://localhost:5173/. You should see the text "Daily Focus", today's date, "0/3 complete", an input field, and an Add button. The layout looks unstyled because we haven't updated the CSS yet.
Seeing an error in the browser?
- Make sure you saved the file. Unsaved changes won't trigger a browser update.
- Check that export default App appears at the bottom of the file. Without it, React can't find your component.
- Confirm your terminal still shows the dev server running. If it stopped, run npm run dev again.
Why is my App.jsx showing an error after I replaced the code?
Clean up default styles
The default Vite styles add animations and colors you don't need. You'll replace them with clean, minimal styles for your app.
- Open src/App.css in VS Code. Select all the existing code and delete it.
- Replace it with this stylesheet:
.app {
max-width: 480px;
margin: 0 auto;
padding: 2rem 1.5rem;
}
.header {
text-align: center;
margin-bottom: 2rem;
}
.header h1 {
font-size: 1.75rem;
font-weight: 700;
color: #1a1a1a;
margin: 0;
}
.date {
color: #666;
font-size: 0.9rem;
margin-top: 0.25rem;
}
.progress {
margin-bottom: 1.5rem;
}
.progress-text {
font-size: 0.85rem;
color: #666;
display: block;
margin-bottom: 0.5rem;
}
.progress-bar {
height: 6px;
background: #e5e7eb;
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #10b981;
border-radius: 3px;
transition: width 0.3s ease;
}
.input-section {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.priority-input {
flex: 1;
padding: 0.75rem 1rem;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-size: 0.95rem;
outline: none;
transition: border-color 0.2s;
}
.priority-input:focus {
border-color: #6366f1;
}
.priority-input:disabled {
background: #f9fafb;
color: #9ca3af;
}
.add-button {
padding: 0.75rem 1.25rem;
background: #6366f1;
color: white;
border: none;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.add-button:hover:not(:disabled) {
background: #4f46e5;
}
.add-button:disabled {
background: #d1d5db;
cursor: not-allowed;
}
.priority-list {
list-style: none;
padding: 0;
margin: 0;
}
.priority-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.875rem 0;
border-bottom: 1px solid #f3f4f6;
}
.priority-item.completed .priority-text {
text-decoration: line-through;
color: #9ca3af;
}
.toggle-button {
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid #d1d5db;
background: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.85rem;
color: #6366f1;
transition: border-color 0.2s;
flex-shrink: 0;
}
.priority-item.completed .toggle-button {
border-color: #10b981;
color: #10b981;
}
.priority-text {
flex: 1;
font-size: 0.95rem;
color: #1a1a1a;
}
.remove-button {
background: none;
border: none;
color: #9ca3af;
cursor: pointer;
font-size: 1rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
transition: color 0.2s;
}
.remove-button:hover {
color: #ef4444;
}
.empty-state {
text-align: center;
color: #9ca3af;
font-size: 0.9rem;
padding: 2rem 0;
}
What does this stylesheet do?
- The .app class centers the layout at a max width of 480px for a focused, mobile-friendly feel.
- The .progress-bar and .progress-fill create a green animated bar that will grow as priorities are completed.
- The .add-button uses a purple (#6366f1) background and grays out when disabled.
- The remaining classes style the priority list items, toggle buttons, and empty state. These will be visible once you add priorities in the next step.
- Save src/App.css.
- Check your browser. The app should now be centered on the page with a purple Add button, a rounded input field, and clean typography.
App still looks unstyled?
- Confirm that the import at the top of src/App.jsx says import './App.css'. Without this import, the styles won't load.
- Check that you saved both src/App.jsx and src/App.css.
Why aren't my CSS styles showing up in the browser?
- Now open src/index.css. Select all and replace it with this global reset:
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #ffffff;
color: #1a1a1a;
line-height: 1.6;
min-height: 100vh;
}
#root {
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 3rem;
}
What does this reset do?
The universal selector (*) removes all default browser margins and padding. The #root styles center the app horizontally and push it down from the top. This gives you a consistent blank canvas across all browsers.
- Save src/index.css.
- Your browser should now show the app pushed slightly below the top of the viewport with consistent spacing.
- Finally, open index.html in the project root (not inside src/).
- Find this line:
<title>Vite + React</title>
- Replace it with:
<title>Daily Focus</title>
- Save index.html.
- Check your browser tab. It should now display "Daily Focus" instead of "Vite + React".
✔️ Awesome, I've got everything!
Great. Double check you've saved all four files before moving on.
ⓧ I'd like to double check the full code
Here's what each file should look like at this point:
src/App.jsx
/*
* PRODUCT: Daily Focus
* USER: Students and professionals overwhelmed by long to-do lists
* PROBLEM: Too many tasks leads to scattered attention and nothing getting done
* SOLUTION: Force a max of 3 priorities per day. Simple. Focused. Done.
* SUCCESS METRIC: User completes all 3 priorities before end of day
*/
import './App.css'
function App() {
const today = new Date().toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric'
})
return (
<div className="app">
<header className="header">
<h1>Daily Focus</h1>
<p className="date">{today}</p>
</header>
<div className="progress">
<span className="progress-text">0/3 complete</span>
<div className="progress-bar">
<div className="progress-fill" style={{ width: '0%' }}></div>
</div>
</div>
<div className="input-section">
<input type="text" placeholder="What's your priority?" className="priority-input" />
<button className="add-button">Add</button>
</div>
<ul className="priority-list"></ul>
<p className="empty-state">Add up to 3 priorities for today. Focus on what matters most.</p>
</div>
)
}
export default App
src/App.css
.app {
max-width: 480px;
margin: 0 auto;
padding: 2rem 1.5rem;
}
.header {
text-align: center;
margin-bottom: 2rem;
}
.header h1 {
font-size: 1.75rem;
font-weight: 700;
color: #1a1a1a;
margin: 0;
}
.date {
color: #666;
font-size: 0.9rem;
margin-top: 0.25rem;
}
.progress {
margin-bottom: 1.5rem;
}
.progress-text {
font-size: 0.85rem;
color: #666;
display: block;
margin-bottom: 0.5rem;
}
.progress-bar {
height: 6px;
background: #e5e7eb;
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #10b981;
border-radius: 3px;
transition: width 0.3s ease;
}
.input-section {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.priority-input {
flex: 1;
padding: 0.75rem 1rem;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-size: 0.95rem;
outline: none;
transition: border-color 0.2s;
}
.priority-input:focus {
border-color: #6366f1;
}
.priority-input:disabled {
background: #f9fafb;
color: #9ca3af;
}
.add-button {
padding: 0.75rem 1.25rem;
background: #6366f1;
color: white;
border: none;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.add-button:hover:not(:disabled) {
background: #4f46e5;
}
.add-button:disabled {
background: #d1d5db;
cursor: not-allowed;
}
.priority-list {
list-style: none;
padding: 0;
margin: 0;
}
.priority-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.875rem 0;
border-bottom: 1px solid #f3f4f6;
}
.priority-item.completed .priority-text {
text-decoration: line-through;
color: #9ca3af;
}
.toggle-button {
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid #d1d5db;
background: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.85rem;
color: #6366f1;
transition: border-color 0.2s;
flex-shrink: 0;
}
.priority-item.completed .toggle-button {
border-color: #10b981;
color: #10b981;
}
.priority-text {
flex: 1;
font-size: 0.95rem;
color: #1a1a1a;
}
.remove-button {
background: none;
border: none;
color: #9ca3af;
cursor: pointer;
font-size: 1rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
transition: color 0.2s;
}
.remove-button:hover {
color: #ef4444;
}
.empty-state {
text-align: center;
color: #9ca3af;
font-size: 0.9rem;
padding: 2rem 0;
}
src/index.css
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #ffffff;
color: #1a1a1a;
line-height: 1.6;
min-height: 100vh;
}
#root {
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 3rem;
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Daily Focus</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Your UI shell is complete. It has structure, style, and a clear product purpose. Next up, you'll bring it to life by wiring up state so the user can actually add, remove, and track priorities.
Build the Priority-Setting Feature
In the last step, you built a clean UI shell for Daily Focus. It looks good, but nothing happens when you type or click.
In this step, you'll bring the app to life with React state management. The core product constraint is simple: users can add a maximum of 3 priorities per day. You'll implement this limit, along with the ability to add and remove priorities from the list.
In this step, get ready to:
- Add React state to manage priorities and input text.
- Implement add and remove priority functions with a max-3 constraint.
- Wire up the input for keyboard submission and auto-focus.
Add state and the add-priority function
React's useState hook lets your component remember values between renders. You need two pieces of state: the list of priorities and whatever the user is currently typing.
- In VS Code, open src/App.jsx.
- Replace the entire file contents with the following code:
/*
* PRODUCT: Daily Focus
* USER: Students and professionals overwhelmed by long to-do lists
* PROBLEM: Too many tasks leads to scattered attention and nothing getting done
* SOLUTION: Force a max of 3 priorities per day. Simple. Focused. Done.
* SUCCESS METRIC: User completes all 3 priorities before end of day
*/
import { useState, useRef } from 'react'
import './App.css'
const MAX_PRIORITIES = 3
function App() {
// State: array of priority objects and current input text
const [priorities, setPriorities] = useState([])
const [input, setInput] = useState('')
const inputRef = useRef(null)
// Add a new priority if input is valid and under the limit
function addPriority() {
const text = input.trim()
if (!text || priorities.length >= MAX_PRIORITIES) return
setPriorities([...priorities, { id: Date.now(), text, completed: false }])
setInput('')
}
// Remove a priority by its unique id
function removePriority(id) {
setPriorities(priorities.filter(p => p.id !== id))
}
// Submit on Enter key press
function handleKeyDown(e) {
if (e.key === 'Enter') {
addPriority()
}
}
const completedCount = priorities.filter(p => p.completed).length
const today = new Date().toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric'
})
return (
<div className="app">
<header className="header">
<h1>Daily Focus</h1>
<p className="date">{today}</p>
</header>
<div className="progress">
<span className="progress-text">{completedCount}/{MAX_PRIORITIES} complete</span>
<div className="progress-bar">
<div className="progress-fill" style={{ width: `${(completedCount / MAX_PRIORITIES) * 100}%` }}></div>
</div>
</div>
<div className="input-section">
<input
ref={inputRef}
type="text"
placeholder={priorities.length >= MAX_PRIORITIES ? 'Max 3 priorities reached' : "What's your priority?"}
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={handleKeyDown}
disabled={priorities.length >= MAX_PRIORITIES}
className="priority-input"
/>
<button
onClick={addPriority}
disabled={priorities.length >= MAX_PRIORITIES || !input.trim()}
className="add-button"
>
Add
</button>
</div>
<ul className="priority-list">
{priorities.map(priority => (
<li key={priority.id} className={`priority-item ${priority.completed ? 'completed' : ''}`}>
<button className="toggle-button" aria-label={priority.completed ? 'Mark incomplete' : 'Mark complete'}>
{priority.completed ? '\u2713' : '\u25CB'}
</button>
<span className="priority-text">{priority.text}</span>
<button className="remove-button" onClick={() => removePriority(priority.id)} aria-label="Remove priority">x</button>
</li>
))}
</ul>
{priorities.length === 0 && (
<p className="empty-state">Add up to 3 priorities for today. Focus on what matters most.</p>
)}
</div>
)
}
export default App
What does this code do?
The logic at the top of the component manages your app's data:
- useState([]) creates a priorities array that React re-renders the UI whenever it changes.
- MAX_PRIORITIES = 3 is the product constraint. The addPriority function refuses to add more once you hit this limit.
- Each priority is an object with an id (timestamp), text (what the user typed), and completed (false by default).
- useRef(null) creates a reference to the input element so you can auto-focus it later.
- Save src/App.jsx.
- Return to your browser at http://localhost:5173/. The page auto-refreshes. You should see the same layout as before, but now the input field is interactive.
Seeing a blank page or error?
- Check for a missing closing brace or parenthesis. React components must be syntactically complete.
- Make sure you replaced the entire file, including the import line at the top. The old import (import './App.css') does not include useState or useRef.
- Look at the terminal running your dev server for specific error messages pointing to the line number.
Help me debug a blank page in my React Vite app after updating App.jsx with useState and useRef.
Test the priority list and remove feature
The priority list uses React's map function to render each priority as a list item. Every time the priorities array changes, React automatically re-renders the list.
- Type a priority (like Finish project report) into the input field and click Add.
- Add two more priorities to reach the limit of 3.
- Notice the Add button turns grey and the input field becomes disabled.
- Click the x button next to any priority to remove it.
How does the max-3 limit work?
The constraint enforces itself in three places at once:
- addPriority() checks priorities.length >= MAX_PRIORITIES and exits early if at capacity.
- The disabled prop on both the input and button prevents interaction once you hit 3.
- The placeholder text switches to Max 3 priorities reached to communicate the limit visually.
This is a product decision enforced through code. The user can never feel overwhelmed because the app physically prevents overload.
Wire up the input and keyboard interaction
Good UX means the user never has to reach for their mouse when they don't want to. The input is already wired up to submit on Enter and auto-focus when the page loads.
- Click into the input field and type a priority.
- Press Enter instead of clicking Add. The priority should appear in the list.
- Try submitting with an empty input. Nothing should happen.
- Refresh the page and confirm the input field receives focus automatically (your cursor is already in the text box).
How is the input connected to state?
This pattern is called a "controlled component" in React. The input's value is always equal to the input state variable. Every keystroke fires onChange, which updates state, which re-renders the input with the new value.
The handleKeyDown function listens for the Enter key and calls addPriority() directly. This means clicking the button and pressing Enter do the exact same thing.
Input not responding to Enter?
- Make sure onKeyDown={handleKeyDown} is on the <input> element, not on the <button>.
- Check that your handleKeyDown function compares e.key === 'Enter' with a capital E.
Help me debug why pressing Enter in my React input isn't submitting the form.
✔️ Awesome, I've got everything!
Great. Double check you've saved src/App.jsx and your priority list is working.
ⓧ I'd like to double check the full code
Here is the complete src/App.jsx at this point:
/*
* PRODUCT: Daily Focus
* USER: Students and professionals overwhelmed by long to-do lists
* PROBLEM: Too many tasks leads to scattered attention and nothing getting done
* SOLUTION: Force a max of 3 priorities per day. Simple. Focused. Done.
* SUCCESS METRIC: User completes all 3 priorities before end of day
*/
import { useState, useRef } from 'react'
import './App.css'
const MAX_PRIORITIES = 3
function App() {
const [priorities, setPriorities] = useState([])
const [input, setInput] = useState('')
const inputRef = useRef(null)
function addPriority() {
const text = input.trim()
if (!text || priorities.length >= MAX_PRIORITIES) return
setPriorities([...priorities, { id: Date.now(), text, completed: false }])
setInput('')
}
function removePriority(id) {
setPriorities(priorities.filter(p => p.id !== id))
}
function handleKeyDown(e) {
if (e.key === 'Enter') {
addPriority()
}
}
const completedCount = priorities.filter(p => p.completed).length
const today = new Date().toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric'
})
return (
<div className="app">
<header className="header">
<h1>Daily Focus</h1>
<p className="date">{today}</p>
</header>
<div className="progress">
<span className="progress-text">{completedCount}/{MAX_PRIORITIES} complete</span>
<div className="progress-bar">
<div className="progress-fill" style={{ width: `${(completedCount / MAX_PRIORITIES) * 100}%` }}></div>
</div>
</div>
<div className="input-section">
<input
ref={inputRef}
type="text"
placeholder={priorities.length >= MAX_PRIORITIES ? 'Max 3 priorities reached' : "What's your priority?"}
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={handleKeyDown}
disabled={priorities.length >= MAX_PRIORITIES}
className="priority-input"
/>
<button
onClick={addPriority}
disabled={priorities.length >= MAX_PRIORITIES || !input.trim()}
className="add-button"
>
Add
</button>
</div>
<ul className="priority-list">
{priorities.map(priority => (
<li key={priority.id} className={`priority-item ${priority.completed ? 'completed' : ''}`}>
<button className="toggle-button" aria-label={priority.completed ? 'Mark incomplete' : 'Mark complete'}>
{priority.completed ? '\u2713' : '\u25CB'}
</button>
<span className="priority-text">{priority.text}</span>
<button className="remove-button" onClick={() => removePriority(priority.id)} aria-label="Remove priority">x</button>
</li>
))}
</ul>
{priorities.length === 0 && (
<p className="empty-state">Add up to 3 priorities for today. Focus on what matters most.</p>
)}
</div>
)
}
export default App
Your priority list is fully interactive. You can add, remove, and the max-3 constraint keeps things focused. Next up, you'll add progress tracking and make priorities survive a page refresh with localStorage.
Add Progress Tracking and localStorage Persistence
Your priorities list works, but there's a problem. If you refresh the page, everything disappears.
In this step, you'll make priorities clickable to mark them complete, then save everything to the browser's localStorage so data survives page refreshes. You'll also add logic that automatically resets priorities each morning.
In this step, get ready to:
- Implement toggle-complete so you can mark priorities as done.
- Save and load priorities from localStorage.
- Add automatic daily reset logic.
Add the toggle-complete functionality
Right now, clicking the circle button next to a priority does nothing. You need a function that flips the completed boolean for the clicked item.
- In src/App.jsx, find the removePriority function.
- Add the following togglePriority function directly above it:
function togglePriority(id) {
setPriorities(priorities.map(p =>
p.id === id ? { ...p, completed: !p.completed } : p
))
}
What does this code do?
The map function loops through every priority. When it finds the one with the matching id, it creates a copy with completed flipped to the opposite value.
All other priorities stay unchanged. This pattern (mapping to produce a new array) is how you update individual items in React state without mutating the original.
Now you need to wire this function to the toggle button in your JSX.
- Scroll down to the <ul className="priority-list"> section.
- Find the toggle button that currently looks like this:
<button className="toggle-button" aria-label={priority.completed ? 'Mark incomplete' : 'Mark complete'}>
{priority.completed ? '\u2713' : '\u25CB'}
</button>
- Add onClick={() => togglePriority(priority.id)} to the button, so it now looks like this:
<button
className="toggle-button"
onClick={() => togglePriority(priority.id)}
aria-label={priority.completed ? 'Mark incomplete' : 'Mark complete'}
>
{priority.completed ? '\u2713' : '\u25CB'}
</button>
- Save src/App.jsx.
- In your browser, add a priority and click the circle button next to it. You should see a checkmark appear, the text gets a strikethrough, and the progress bar fills up.
Clicking the circle does nothing?
Make sure onClick={() => togglePriority(priority.id)} is on the button with className="toggle-button", not on the remove button.
Also check that your togglePriority function is defined inside the App function (not outside it), so it has access to priorities and setPriorities.
help me debug why clicking my toggle button isn't working
Add localStorage persistence
Your app loses all data on refresh because React state only lives in memory. localStorage is a browser API that stores key-value pairs on the user's device, surviving page refreshes and browser restarts.
- At the top of src/App.jsx, find this import line:
import { useState, useRef } from 'react'
- Replace it with this to add useEffect:
import { useState, useEffect, useRef } from 'react'
Next, you'll add helper functions that handle reading from and writing to localStorage. These go outside the App function because they don't depend on React state.
- Find the line const MAX_PRIORITIES = 3. Replace it with the following block that adds a storage key constant, keeps MAX_PRIORITIES, and defines two helper functions:
const STORAGE_KEY = 'daily-focus-data'
const MAX_PRIORITIES = 3
function getToday() {
return new Date().toISOString().split('T')[0]
}
function loadFromStorage() {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return { priorities: [], date: getToday() }
const data = JSON.parse(raw)
if (data.date !== getToday()) {
return { priorities: [], date: getToday() }
}
return data
} catch {
return { priorities: [], date: getToday() }
}
}
What does this code do?
- STORAGE_KEY is the name under which your data is stored in the browser. Think of it like a filename.
- getToday() returns today's date as a YYYY-MM-DD string for easy comparison.
- loadFromStorage() reads saved data, checks if it's from today, and returns either the saved priorities or an empty set. If the data is from yesterday, it returns empty priorities so the user starts fresh.
Now wire these helpers into your component's state and lifecycle.
- Inside the App function, find this line:
const [priorities, setPriorities] = useState([])
- Replace it with this, which loads saved priorities on startup:
const [priorities, setPriorities] = useState(() => loadFromStorage().priorities)
Finally, add two useEffect hooks that save data and auto-focus the input.
- Find the line const inputRef = useRef(null). Add the following code directly below it:
useEffect(() => {
const data = { priorities, date: getToday() }
localStorage.setItem(STORAGE_KEY, JSON.stringify(data))
}, [priorities])
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus()
}
}, [])
What do these effects do?
- The first useEffect runs every time priorities changes. It saves the current priorities and today's date to localStorage as a JSON string.
- The second useEffect runs once on mount (empty [] dependency array). It auto-focuses the input field so the user can start typing immediately.
- Save src/App.jsx.
- In your browser, add 2 priorities and mark one as complete. Then refresh the page. Your priorities and their completion status should still be there.
Priorities disappear on refresh?
Open your browser's DevTools (Cmd+Option+I on macOS or Ctrl+Shift+I on Windows) and check the Console tab for errors.
Verify that your useEffect with [priorities] is inside the App function, not outside it. Also confirm you imported useEffect from 'react'.
help me debug why my localStorage persistence isn't working
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete src/App.jsx at this point:
/*
* PRODUCT: Daily Focus
* USER: Students and professionals overwhelmed by long to-do lists
* PROBLEM: Too many tasks leads to scattered attention and nothing getting done
* SOLUTION: Force a max of 3 priorities per day. Simple. Focused. Done.
* SUCCESS METRIC: User completes all 3 priorities before end of day
*/
import { useState, useEffect, useRef } from 'react'
import './App.css'
const STORAGE_KEY = 'daily-focus-data'
const MAX_PRIORITIES = 3
function getToday() {
return new Date().toISOString().split('T')[0]
}
function loadFromStorage() {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return { priorities: [], date: getToday() }
const data = JSON.parse(raw)
if (data.date !== getToday()) {
return { priorities: [], date: getToday() }
}
return data
} catch {
return { priorities: [], date: getToday() }
}
}
function App() {
const [priorities, setPriorities] = useState(() => loadFromStorage().priorities)
const [input, setInput] = useState('')
const inputRef = useRef(null)
useEffect(() => {
const data = { priorities, date: getToday() }
localStorage.setItem(STORAGE_KEY, JSON.stringify(data))
}, [priorities])
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus()
}
}, [])
function addPriority() {
const text = input.trim()
if (!text || priorities.length >= MAX_PRIORITIES) return
setPriorities([...priorities, { id: Date.now(), text, completed: false }])
setInput('')
}
function togglePriority(id) {
setPriorities(priorities.map(p =>
p.id === id ? { ...p, completed: !p.completed } : p
))
}
function removePriority(id) {
setPriorities(priorities.filter(p => p.id !== id))
}
function handleKeyDown(e) {
if (e.key === 'Enter') {
addPriority()
}
}
const completedCount = priorities.filter(p => p.completed).length
const today = new Date().toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric'
})
return (
<div className="app">
<header className="header">
<h1>Daily Focus</h1>
<p className="date">{today}</p>
</header>
<div className="progress">
<span className="progress-text">{completedCount}/{MAX_PRIORITIES} complete</span>
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${(completedCount / MAX_PRIORITIES) * 100}%` }}
></div>
</div>
</div>
<div className="input-section">
<input
ref={inputRef}
type="text"
placeholder={priorities.length >= MAX_PRIORITIES ? "Max 3 priorities reached" : "What's your priority?"}
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={handleKeyDown}
disabled={priorities.length >= MAX_PRIORITIES}
className="priority-input"
/>
<button
onClick={addPriority}
disabled={priorities.length >= MAX_PRIORITIES || !input.trim()}
className="add-button"
>
Add
</button>
</div>
<ul className="priority-list">
{priorities.map(priority => (
<li key={priority.id} className={`priority-item ${priority.completed ? 'completed' : ''}`}>
<button
className="toggle-button"
onClick={() => togglePriority(priority.id)}
aria-label={priority.completed ? 'Mark incomplete' : 'Mark complete'}
>
{priority.completed ? '\u2713' : '\u25CB'}
</button>
<span className="priority-text">{priority.text}</span>
<button
className="remove-button"
onClick={() => removePriority(priority.id)}
aria-label="Remove priority"
>
x
</button>
</li>
))}
</ul>
{priorities.length === 0 && (
<p className="empty-state">Add up to 3 priorities for today. Focus on what matters most.</p>
)}
</div>
)
}
export default App
Understand the daily reset logic
The loadFromStorage function you added does more than just read data. It also enforces the core product mechanic: fresh priorities every day.
How does the daily reset work?
Every time the app loads, loadFromStorage() compares the stored date against today's date. If they don't match, it ignores the saved priorities and returns an empty array.
This means if you open the app tomorrow morning, yesterday's priorities are gone. You start fresh. That's the product hypothesis in action: forcing a daily decision about what matters most.
The save effect stores today's date alongside the priorities. When tomorrow comes and the dates mismatch, the reset triggers automatically.
- To verify the reset logic, open DevTools (Cmd+Option+I on macOS or Ctrl+Shift+I on Windows).
- Go to the Application tab, then expand Local Storage in the left sidebar.
- Click on http://localhost:5173. You should see a daily-focus-data key with a JSON value containing your priorities and today's date.
Test the reset without waiting
You can manually edit the date in the localStorage entry (change it to yesterday's date), then refresh the page. Your priorities should disappear because the app detects a date mismatch.
Your app now has real product value. It tracks progress, remembers your priorities across refreshes, and resets daily to keep you focused. Next up, you'll ship this to the internet so anyone can use it.
Deploy to Vercel
Your app works beautifully on your machine. It saves priorities, resets daily, and tracks progress. Now it's time to put it on the internet so anyone can use it.
In this step, you'll ship the app to a live URL using Vercel. Vercel connects directly to your GitHub repository and automatically rebuilds your app every time you push new code.
In this step, get ready to:
- Add SPA routing configuration for Vercel.
- Push your code to GitHub and deploy on Vercel.
- Verify the live app works with persistence.
Add SPA routing configuration
React apps are Single-Page Applications. The browser loads one HTML file and JavaScript handles all the page transitions.
Without a routing configuration file, Vercel would return a 404 error if someone tried to access your app at any path other than the homepage. A vercel.json file tells Vercel to always serve your index.html instead.
- Create a new file called vercel.json in the root of your daily-focus folder (the same level as package.json) and paste this code:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}
What does this file do?
- The rewrites rule tells Vercel to serve index.html for every URL path, no matter what path the user visits.
- This means your React app handles routing on the client side instead of the server returning a 404.
- Save vercel.json.
- Confirm the file exists by running:
ls vercel.json
You should see vercel.json printed in the output.
File not showing up?
- Make sure you created vercel.json in the root of your daily-focus folder, not inside src/.
- The filename must be exactly vercel.json (all lowercase, no spaces).
help me find where to put vercel.json in my project
Push to GitHub and deploy
With the routing config in place, you can commit everything and push to GitHub. Then you'll import the repository into Vercel to trigger your first deployment.
- Commit and push all changes by running these commands:
git add .
git commit -m "Complete Daily Focus app"
git push
What do these commands do?
- git add . stages all changed and new files (including vercel.json).
- git commit saves a snapshot of your project with a descriptive message.
- git push uploads the commit to your GitHub repository.
- Open your GitHub repository in a browser to confirm the new commit appears on the main branch.
Push failed?
- If you see a permission error, make sure your GitHub credentials are configured in your terminal.
- If you see "rejected", someone else may have pushed changes. Pull first by running git pull --rebase origin main and then push again.
help me fix a git push error
Now you'll import this repository into Vercel to deploy it live.
- Go to vercel.com in your browser and sign in to your account.
- Click Add New... in the top right, then select Project.
- Find daily-focus in your repository list and click Import.
- Vercel auto-detects your Vite project and fills in the build settings.
- Confirm the Build Command shows npm run build and the Output Directory shows dist.
- Click Deploy.
What is CI/CD?
CI/CD stands for Continuous Integration and Continuous Deployment. It means your code is automatically built and deployed every time you push to GitHub.
From now on, any commit you push to the main branch will trigger a fresh deployment on Vercel. No manual uploads needed.
Don't see your repository?
- Make sure you connected your GitHub account to Vercel. If prompted, click Authorize to give Vercel access to your repositories.
- If the repo still doesn't appear, click Adjust GitHub App Permissions and grant access to the daily-focus repository.
help me connect my GitHub account to Vercel
Verify your live app
Deployment usually completes in under a minute. Once it finishes, Vercel displays a live URL for your app.
- Click the URL that Vercel displays on the deployment success page (it looks something like daily-focus-abc123.vercel.app).
- Add a priority and mark it complete to test the full flow.
- Refresh the page to confirm localStorage persistence works on the live site.
- Your priorities should still be there after the refresh.
What should I see?
Your live app should look and behave exactly like it did on localhost:5173. You can add priorities, mark them complete, and refresh without losing data.
The difference is that this URL is public. You can share it with anyone and they can use Daily Focus on their own device.
Deployment failed?
- Check the build logs on the Vercel dashboard for error messages.
- Make sure vercel.json is committed and pushed. Run git status to check for uncommitted files.
- If you see a build command error, confirm your package.json has a build script (Vite adds this automatically during scaffold).
help me debug a Vercel deployment failure
✔️ Awesome, I've got everything!
Great. Double check that you've saved vercel.json and that your deployment is live.
ⓧ I'd like to double check the full code
Here is the vercel.json file you should have at the root of your project:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}
Secret mission
Add a Daily Streak Counter
Your Daily Focus app helps users pick priorities, but what keeps them coming back tomorrow? Add a streak counter that tracks consecutive days of use. If the user set priorities yesterday and today, the streak grows. Miss a day? Back to zero.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally and deploys to Vercel's free Hobby plan, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Local daily-focus project folder on your Desktop.
- GitHub repository (daily-focus).
- Vercel project and deployment (connected to GitHub with automatic CI/CD).
Keep everything running
No action needed. Choose this if you plan to keep using Daily Focus or want to continue building on it.
- Your Vercel deployment stays live at its public URL for free on the Hobby plan.
- Every push to main on GitHub automatically redeploys to Vercel.
- Your local project folder remains on your Desktop for further development.
Pause - I'll come back to this later
Stop the local dev server to free up memory. Your deployed app stays live on Vercel at no cost, and your code stays safe on GitHub.
- If the dev server is still running in your terminal, press Ctrl+C to stop it.
- Your Vercel deployment remains live for free. No need to take it offline.
- When you are ready to resume, open the daily-focus folder in VS Code and run npm run dev to start developing again.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below to fully clean up.
- Delete the Vercel project:
- Open your Vercel dashboard and select the daily-focus project.
- Click Settings in the sidebar.
- Scroll to the bottom of the General page and find the Delete Project section. Click Delete.
- Type the project name to confirm and click Continue.
- Delete the GitHub repository:
- Go to your daily-focus repository on GitHub.
- Click Settings (under the repository name).
- Scroll down to the Danger Zone section and click Delete this repository.
- Follow the confirmation prompts. Type the repository name when asked, then click Delete this repository.
- Delete the local project folder. On macOS/Linux, run:
cd ~/Desktop
rm -rf daily-focus
On Windows, run:
cd ~\Desktop
Remove-Item -Recurse -Force daily-focus
- Confirm the folder is gone by running ls (macOS/Linux) or dir (Windows). You should no longer see daily-focus in the output.
That's a Wrap!
That's a Wrap!
Nice work! You just built and shipped a real product from scratch using the full product engineering loop.
You've learned how to:
- Practiced product thinking by defining a user, problem, and success metric before writing a single line of code.
- Built an interactive React app with state management (useState, useEffect) and client-side persistence using localStorage with daily reset logic.
- Deployed a live web app to Vercel with automatic CI/CD from GitHub, so every push to main redeploys your app.
- Secret Mission: Added a daily streak counter to gamify daily consistency.
Ready to quiz yourself?