Build an Interactive User Directory
Build a vanilla JS web app that fetches API data with live search and debouncing.
Introduction
30 Second Summary
Think about how you browse contacts, employee directories, or social media profiles. Behind every search bar and card layout is code that fetches data, filters it in real time, and updates the page without a full reload.
In this project, you will build an interactive user directory web app using vanilla JavaScript. You'll fetch data from a live API, render it as a grid of clickable cards, and implement live search with debouncing.
What You'll Build
A responsive user directory that fetches user data from an API, displays it in a card grid with click-to-expand detail views, and provides real-time filtered search powered by a debounce function.
By the end of this project, you'll have:
- A working interactive web app that fetches data from an API, renders it dynamically, and implements live search with debouncing.
- Hands-on experience with JavaScript's key differences from Python: closures, the event loop, and async/await.
- A professional Git history with feature branches and meaningful commits created through WebStorm's Git integration.
- Secret Mission: Add sorting controls and keyboard shortcuts to level up the UX.
Are there any prerequisites?
This project assumes you have intermediate Python experience and are comfortable with Git branching and merging. You'll also need WebStorm installed with a valid license, Node.js/npm installed, and a GitHub account.
Set Up the Project in WebStorm
Before you write any JavaScript, you need a solid project foundation. WebStorm gives you a complete project setup flow that handles file creation, a built-in web server, and Git integration all in one place.
In this step, you will create a new project, set up the HTML skeleton that ties your files together, and make your first commit to GitHub.
In this step, get ready to:
- Create a new WebStorm project and initialize a Git repository.
- Write the HTML skeleton and verify it loads in Chrome.
- Commit your scaffolding and push to GitHub.
Create a new project and initialize Git
WebStorm's New Project wizard creates a project folder and can initialize Git in one flow. Let's start by opening the IDE.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type WebStorm and press Enter to open it.
- On the Welcome screen, click New Project.
- In the left panel of the New Project dialog, select Empty Project.
- Set the project location to your Desktop and name it user-directory.
- Click Create.
WebStorm opens your new empty project. Now initialize Git so your changes are tracked from the start.
- In the main menu, go to VCS | Create Git Repository.
- In the dialog that opens, confirm the directory is your user-directory project folder and click OK.
What did that just do?
VCS | Create Git Repository runs git init behind the scenes. It creates a hidden .git folder that tracks every change you make from now on.
Next, create the three files your project needs.
- In the Project tool window on the left, right-click the user-directory root folder.
- Select New | HTML File.
- Type index as the name and press Enter. WebStorm creates index.html with a basic template.
- Right-click the user-directory folder again. Select New | Stylesheet.
- Type style and press Enter. This creates style.css.
- Right-click the root folder once more. Select New | JavaScript File.
- Type app and press Enter. This creates app.js.
Add placeholder comments so these files are not empty.
- Open style.css in the editor and replace any content with:
/* Empty placeholder — styling added in Step 5 */
- Open app.js in the editor and replace any content with:
/* Empty placeholder — logic added in Steps 2-5 */
- Save both files with Cmd+S (macOS) or Ctrl+S (Windows).
Don't see the New menu options?
Make sure you are right-clicking the project root folder in the Project tool window, not a file. If you still cannot find the options, press Cmd+N (macOS) or Alt+Insert (Windows) while the project folder is selected.
help me create files in WebStorm's Project tool window
Write the HTML skeleton and verify in Chrome
The HTML file ties your CSS and JavaScript together. It also defines the structure of your user directory page.
- Open index.html in the editor. Select all the existing content and replace it with the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User Directory</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<header>
<h1>User Directory</h1>
<div class="search-wrapper">
<input type="text" id="search-input" placeholder="Search users by name, email, or city...">
</div>
</header>
<main>
<div id="user-grid" class="user-grid"></div>
<div id="user-detail" class="user-detail hidden"></div>
</main>
</div>
<script src="app.js"></script>
</body>
</html>
What does this HTML do?
- The <link> tag in the <head> connects your stylesheet.
- The <script> tag at the bottom of <body> loads your JavaScript after the page structure is ready.
- The search-input element will become your live search bar. The user-grid and user-detail divs are empty containers that JavaScript will fill dynamically.
- Save index.html with Cmd+S (macOS) or Ctrl+S (Windows).
Now set the JavaScript language version so WebStorm understands modern syntax.
- Open Settings with Cmd+, (macOS) or Ctrl+Alt+S (Windows).
- Navigate to Languages and Frameworks | JavaScript.
- In the JavaScript language version dropdown, select ECMAScript 6+.
- Click Apply, then OK.
Why ECMAScript 6+?
ECMAScript 6 (also called ES2015) introduced let/const, arrow functions, template literals, and async/await. Setting this tells WebStorm to provide autocompletion and error detection for modern JavaScript features.
Verify the page loads in Chrome using WebStorm's built-in server.
- With index.html open in the editor, hover over the code area until the browser icons bar appears in the top-right corner.
- Click the Chrome icon to open the page.
You should see a page with the heading "User Directory" and a search input field. The page is served by WebStorm's built-in server on port 63342, so no external server is needed.
Don't see the browser icons?
The icons only appear when hovering in the editor area with an HTML file open. You can also use the menu: go to View | Open in Browser and select Chrome.
help me open my HTML file in Chrome from WebStorm
Commit and push to GitHub
Your project files are ready. Before moving on, lock in this foundation with a Git commit and push it to GitHub so you have a remote backup.
First, create a .gitignore file to keep WebStorm's internal settings out of your repository.
- Right-click the user-directory root folder in the Project tool window.
- Select New | File.
- Type .gitignore and press Enter.
- Add the following content to the file:
.idea/
What is .idea/?
The .idea/ folder stores WebStorm project settings like your keymap preferences and run configurations. These are personal to your machine and should not be shared in a team repository.
- Save .gitignore.
- Open the Commit tool window by pressing Cmd+K (macOS) or Ctrl+K (Windows).
- In the Commit tool window, select the checkboxes next to all four files: .gitignore, app.js, index.html, and style.css.
- In the commit message field, type:
Initial project structure with HTML/CSS/JS scaffolding
- Click Commit.
Why meaningful commit messages?
Commit messages describe what changed and why. A teammate reading your Git history should understand the project's evolution without opening every file.
Now push this commit to a new GitHub repository.
- In the main menu, go to Git | GitHub | Share Project on GitHub.
- If prompted to log in, click Log In via GitHub and authorize WebStorm in the browser window that opens.
- In the Share Project on GitHub dialog, leave the repository name as user-directory (the default matches your project name).
- Click Share.
WebStorm creates a new repository on GitHub and pushes your commit. You should see a notification with a link to your new repository.
GitHub login not working?
Make sure you are signed in to GitHub in your browser before clicking Log In via GitHub. If you have two-factor authentication enabled, you will be prompted for a code from your authenticator app.
help me connect WebStorm to my GitHub account
✔️ Awesome, I've got everything!
Great. Double check you have saved all your files before moving on.
ⓧ I'd like to double check the full code
Here is what each file should look like at this point.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User Directory</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<header>
<h1>User Directory</h1>
<div class="search-wrapper">
<input type="text" id="search-input" placeholder="Search users by name, email, or city...">
</div>
</header>
<main>
<div id="user-grid" class="user-grid"></div>
<div id="user-detail" class="user-detail hidden"></div>
</main>
</div>
<script src="app.js"></script>
</body>
</html>
/* Empty placeholder — styling added in Step 5 */
/* Empty placeholder — logic added in Steps 2-5 */
.idea/
Your project foundation is locked in. Next up, you will write your first async JavaScript function to fetch real user data from an API.
Fetch Users from the API
Your project structure is in place, and your repository is ready on GitHub. Now it's time to write some actual JavaScript.
In this step, you'll connect your app to a live API and pull down real user data. This is where JavaScript starts to feel different from Python. Instead of blocking the entire program while waiting for a network response, JavaScript uses Promises and async/await to keep things moving.
In this step, get ready to:
- Write an async function that fetches user data from a live API.
- Verify the data in Chrome DevTools.
- Use WebStorm's debugger to step through the async flow.
Create a feature branch and write the fetch function
Professional teams isolate each piece of functionality on its own Git branch. This way, incomplete work never touches the stable main branch.
- In WebStorm's main window header, click the VCS widget that currently shows main.
- Choose New Branch from the popup.
- Type feature/fetch-users as the branch name.
- Make sure Checkout branch is selected, then click Create.
The VCS widget now shows feature/fetch-users instead of main.
Why a feature branch?
Feature branches let you experiment freely. If something breaks, main stays untouched. Once your feature works, you merge the branch back in.
Now you'll set up the app.js file with the state and DOM references your app needs.
- Open app.js in the editor (double-click it in the Project tool window).
- Delete the placeholder comment and replace the entire file with this code:
'use strict';
// --- State ---
let allUsers = [];
// --- DOM References ---
const searchInput = document.getElementById('search-input');
const userGrid = document.getElementById('user-grid');
const userDetail = document.getElementById('user-detail');
What does this code do?
- 'use strict' enables strict mode, which catches common coding mistakes and throws errors instead of silently ignoring them.
- allUsers is an empty array that will eventually hold all the user objects from the API.
- The three const lines grab references to HTML elements by their id attribute so you can manipulate them later without querying the DOM again.
- You won't see a change in the browser yet. The next chunk uses these references.
- Below the DOM references section, add the fetchUsers function:
// --- API Functions ---
async function fetchUsers() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const users = await response.json();
return users;
} catch (error) {
console.error('Failed to fetch users:', error.message);
return [];
}
}
What does this code do?
- async function fetchUsers() declares an asynchronous function. The async keyword means this function can pause and wait for network responses without blocking the rest of the program.
- await fetch(...) sends a network request to the JSONPlaceholder API. Unlike Python's requests.get() which blocks the entire program, fetch() returns a Promise. The await keyword pauses only this function, not the entire program.
- response.json() also returns a Promise (another await needed) that parses the response body into a JavaScript object.
- The try/catch block handles network errors gracefully, logging the problem and returning an empty array instead of crashing.
- Save app.js.
Seeing red underlines in WebStorm?
If WebStorm highlights await or async as errors, your JavaScript language version may be set too low. Go to Settings | Languages & Frameworks | JavaScript and set the version to ECMAScript 6+.
help me fix JavaScript language version errors in WebStorm
Call the fetch function and verify in Chrome
The fetchUsers function exists, but nothing calls it yet. You need an entry point that kicks off your app when the page loads.
- At the bottom of app.js, below the fetchUsers function, add the initialization code:
// --- Initialize ---
async function init() {
userGrid.innerHTML = '<p class="loading">Loading users...</p>';
allUsers = await fetchUsers();
console.log(allUsers);
}
init();
What does this code do?
- init() is the app's entry point. It runs as soon as the script loads.
- It first sets a loading message in the grid container so the user sees feedback while the API call runs.
- await fetchUsers() pauses init until the data arrives, then stores the result in allUsers.
- console.log(allUsers) prints the data to the browser console so you can verify the fetch worked.
- Save app.js.
- Open index.html in the editor, hover over the code to reveal the browser icons bar, then click the Chrome icon to open the page.
- In Chrome, open Chrome DevTools by pressing Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows).
- Click the Console tab. You should see an array of 10 user objects, each with fields like name, username, email, address, phone, website, and company.
Console showing an empty array?
An empty array means the fetch failed silently. Check the Console for a red error message like Failed to fetch users. This usually means your internet connection dropped, or the URL has a typo.
Double-check that the URL in your fetch() call is exactly https://jsonplaceholder.typicode.com/users.
help me debug why fetchUsers returns an empty array
Debug the async flow with WebStorm's debugger
Seeing the data in the console proves it works. Now use WebStorm's built-in debugger to understand how the async flow actually executes.
- Switch back to WebStorm.
- In app.js, click in the gutter (the gray area left of line numbers) next to the line if (!response.ok) { to set a breakpoint. A red dot appears.
- Right-click index.html in the Project tool window.
- Choose Debug 'index.html'.
- Chrome opens and execution pauses at your breakpoint. The Debug tool window appears at the bottom of WebStorm.
What to observe in the debugger
- The Variables pane shows response as a Response object. Expand it to see properties like status: 200 and ok: true.
- The Call Stack pane shows how the browser jumped from init() into fetchUsers(), paused at the await, resumed when the Promise resolved, and landed on your breakpoint.
- Click the Step Over button (or press F8) to advance one line at a time. Watch users populate with the 10-item array after the response.json() line executes.
- Click the Resume button (green play icon) or press F9 to let the program finish running.
- Stop the debug session by clicking the red Stop button in the Debug tool window.
Time to commit your work on this branch.
- Open the Commit tool window by pressing Cmd+K (macOS) or Ctrl+K (Windows).
- Select app.js in the file list.
- Enter the commit message:
Add async user fetching with error handling
- Click Commit.
Commit button grayed out?
Make sure you've checked the box next to app.js in the Commit tool window. WebStorm won't let you commit with zero files selected.
help me fix the Commit button being grayed out in WebStorm
✔️ Awesome, I've got everything!
Great. Double check you've saved app.js and committed your changes on the feature/fetch-users branch.
ⓧ I'd like to double check the full code
Here's what your app.js should look like at the end of this step:
'use strict';
// --- State ---
let allUsers = [];
// --- DOM References ---
const searchInput = document.getElementById('search-input');
const userGrid = document.getElementById('user-grid');
const userDetail = document.getElementById('user-detail');
// --- API Functions ---
async function fetchUsers() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const users = await response.json();
return users;
} catch (error) {
console.error('Failed to fetch users:', error.message);
return [];
}
}
// --- Initialize ---
async function init() {
userGrid.innerHTML = '<p class="loading">Loading users...</p>';
allUsers = await fetchUsers();
console.log(allUsers);
}
init();
Your app now talks to a live API and pulls real data into the browser. Next up, you'll take those 10 user objects and render them as an interactive card grid.
Render User Cards with DOM Manipulation
You can fetch users from the API, but they only appear in the console. Nobody visits a website to read console.log output.
In this step, you will turn raw API data into visible, clickable user cards. This is where DOM manipulation comes in. You will use JavaScript to build HTML elements on the fly and inject them into the page.
In this step, get ready to:
- Build a card component function that turns user data into HTML elements.
- Render all users into a responsive grid and add a detail view with navigation.
- Commit and merge the feature branch into main.
Create a new branch and build the card function
Each feature gets its own branch so your commit history stays clean and reviewable. You will create a new branch from main for the rendering work.
- Click the branch name in the bottom-left status bar of WebStorm. It currently shows feature/fetch-users.
- In the popup, expand **Local**, select main, and choose Checkout to switch back to the main branch.
- Click the branch name again (it now shows main) and choose New Branch.
- Type feature/render-cards as the branch name.
- Make sure Checkout branch is selected, then click Create.
The status bar now shows feature/render-cards. You are ready to write the rendering code.
Each user card needs to be built programmatically from the data you fetched. The createUserCard function takes a single user object and returns a fully constructed DOM element.
- In app.js, find the comment // --- Initialize --- and add the following code above it:
// --- Render Functions ---
function createUserCard(user) {
const card = document.createElement('div');
card.className = 'user-card';
card.dataset.userId = user.id;
card.innerHTML = `
<div class="user-card-header">
<div class="avatar">${user.name.charAt(0)}</div>
<div class="user-info">
<h3>${user.name}</h3>
<p class="username">@${user.username}</p>
</div>
</div>
<div class="user-card-body">
<p><span class="label">Email:</span> ${user.email}</p>
<p><span class="label">City:</span> ${user.address.city}</p>
<p><span class="label">Company:</span> ${user.company.name}</p>
</div>
`;
return card;
}
What does this code do?
This function builds a card element entirely in JavaScript. document.createElement('div') creates a new <div> that does not exist in your HTML file yet.
Template literals (the backtick strings with ${} expressions) work like Python f-strings but use backtick delimiters instead of quotes. They let you inject user data directly into the HTML structure.
The dataset.userId line stores the user's ID on the element itself, which is useful for later lookups.
What is DOM manipulation?
DOM manipulation is the process of using JavaScript to create, modify, or remove HTML elements after the page has loaded. There is no Python equivalent. You are directly building the visual interface that the browser renders.
- Save app.js.
You will not see a visible change yet. The next chunk uses this function to display cards on screen.
Render cards to the grid and update initialization
With the card builder ready, you need a function that loops through all users and adds their cards to the page. You also need to update init() to call this function instead of logging to the console.
- In app.js, add the renderUsers function directly below createUserCard:
function renderUsers(users) {
userGrid.innerHTML = '';
if (users.length === 0) {
userGrid.innerHTML = '<p class="no-results">No users found.</p>';
return;
}
users.forEach(user => {
const card = createUserCard(user);
userGrid.appendChild(card);
});
}
What does this code do?
First, userGrid.innerHTML = '' clears whatever is currently in the grid (like the loading message). This prevents duplicate cards if you call the function more than once.
The empty-state check handles the case where no users match a future search filter. forEach loops through each user and appends a new card element to the grid container.
Now update the init() function to call renderUsers and handle errors gracefully.
- Find the init() function at the bottom of app.js. It currently looks like this:
// --- Initialize ---
async function init() {
userGrid.innerHTML = '<p class="loading">Loading users...</p>';
allUsers = await fetchUsers();
console.log(allUsers);
}
init();
- Replace the entire init() function and the init() call with:
// --- Initialize ---
async function init() {
userGrid.innerHTML = '<p class="loading">Loading users...</p>';
allUsers = await fetchUsers();
if (allUsers.length === 0) {
userGrid.innerHTML = '<p class="error-message">Failed to load users. Please check your connection and refresh.</p>';
return;
}
renderUsers(allUsers);
}
init();
What changed in init()?
The console.log is gone. Instead, the function now checks whether the fetch returned an empty array (which happens when the API is unreachable). If so, it shows an error message in the UI.
If users are present, renderUsers(allUsers) builds and displays all 10 cards.
- Save app.js.
- Open index.html in the editor, hover over the code to reveal the browser icons bar, and click the Chrome icon to open the page.
- You should see 10 user cards displayed on the page. Each card shows a name, username, email, city, and company.
Don't see any cards?
Open Chrome DevTools (Cmd+Option+I on macOS or F12 on Windows) and check the Console tab for errors. A common mistake is a typo in user.address.city or user.company.name. JavaScript is case-sensitive, so user.Address.city would fail.
Also confirm that the // --- Render Functions --- section is placed above // --- Initialize --- in your file. Functions must be defined before they are called.
Help me debug why my user cards are not showing up.
Add click handling and the detail view
Cards should be interactive. When a user clicks a card, the app hides the grid and displays a detailed profile view. This requires a click event listener on each card and a new function that builds the detail layout.
- In app.js, find the return card; line at the end of createUserCard. Add this line directly above it:
card.addEventListener('click', () => showUserDetail(user));
Why is this a closure?
The arrow function () => showUserDetail(user) captures the user variable from the outer scope. Each card gets its own event listener with its own captured reference to a different user object. This is a closure in action.
In Python, you would hit the "late binding" gotcha in a loop. JavaScript avoids this by creating a new scope for each function call, so each card's listener remembers the correct user.
Now write the showUserDetail function that the click handler calls.
- Add the following function below renderUsers and above // --- Initialize ---:
function showUserDetail(user) {
userDetail.classList.remove('hidden');
userGrid.classList.add('hidden');
userDetail.innerHTML = `
<button id="back-button" class="back-button">Back to Directory</button>
<div class="detail-card">
<div class="detail-header">
<div class="avatar-large">${user.name.charAt(0)}</div>
<div>
<h2>${user.name}</h2>
<p class="username">@${user.username}</p>
</div>
</div>
<div class="detail-body">
<div class="detail-section">
<h4>Contact</h4>
<p><span class="label">Email:</span> ${user.email}</p>
<p><span class="label">Phone:</span> ${user.phone}</p>
<p><span class="label">Website:</span> ${user.website}</p>
</div>
<div class="detail-section">
<h4>Address</h4>
<p>${user.address.street}, ${user.address.suite}</p>
<p>${user.address.city}, ${user.address.zipcode}</p>
</div>
<div class="detail-section">
<h4>Company</h4>
<p><span class="label">Name:</span> ${user.company.name}</p>
<p><span class="label">Catchphrase:</span> ${user.company.catchPhrase}</p>
</div>
</div>
</div>
`;
document.getElementById('back-button').addEventListener('click', () => {
userDetail.classList.add('hidden');
userGrid.classList.remove('hidden');
});
}
What does this code do?
The function swaps visibility between the grid and the detail panel using CSS classes. classList.add('hidden') hides an element and classList.remove('hidden') shows it.
The detail view displays all user fields including address, phone, website, and company catchphrase. The back button reverses the visibility swap so the learner returns to the card grid.
- Save app.js and refresh Chrome.
- Click any user card. The grid disappears and you see a detailed profile with contact info, address, and company details.
- Click the Back to Directory button. The card grid reappears.
Clicking a card does nothing?
Make sure the card.addEventListener line is inside createUserCard, placed between the closing backtick of innerHTML and return card;. If it is outside the function, cards will not have listeners attached.
Also check that showUserDetail is defined before init() runs. If you placed it after the init() call, JavaScript cannot find it at the time cards are clicked.
Help me figure out why clicking a user card does not open the detail view.
Everything works. Time to commit and merge this feature into main.
- Open the Commit tool window with Cmd+K (macOS) or Ctrl+K (Windows).
- Check app.js in the file list.
- Enter the commit message: Add user card rendering with detail view and navigation.
- Click Commit.
- Click the branch name in the status bar, select main under Local, and choose Checkout to switch to main.
- Click the branch name again, select feature/render-cards under Local, and choose Merge 'feature/render-cards' into 'main'.
The status bar shows main and your rendering code is now part of the main branch.
Seeing a merge conflict?
This should not happen if you created feature/render-cards from main and did not edit main separately. If WebStorm shows a Conflicts dialog, click Accept Theirs to keep the branch changes.
Help me resolve a merge conflict in WebStorm when merging feature/render-cards into main.
✔️ Awesome, I've got everything!
Great. Double check you have saved app.js and your merge completed successfully.
ⓧ I'd like to double check the full code
Here is the complete app.js at the end of this step:
'use strict';
// --- State ---
let allUsers = [];
// --- DOM References ---
const searchInput = document.getElementById('search-input');
const userGrid = document.getElementById('user-grid');
const userDetail = document.getElementById('user-detail');
// --- API Functions ---
async function fetchUsers() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const users = await response.json();
return users;
} catch (error) {
console.error('Failed to fetch users:', error.message);
return [];
}
}
// --- Render Functions ---
function createUserCard(user) {
const card = document.createElement('div');
card.className = 'user-card';
card.dataset.userId = user.id;
card.innerHTML = `
<div class="user-card-header">
<div class="avatar">${user.name.charAt(0)}</div>
<div class="user-info">
<h3>${user.name}</h3>
<p class="username">@${user.username}</p>
</div>
</div>
<div class="user-card-body">
<p><span class="label">Email:</span> ${user.email}</p>
<p><span class="label">City:</span> ${user.address.city}</p>
<p><span class="label">Company:</span> ${user.company.name}</p>
</div>
`;
card.addEventListener('click', () => showUserDetail(user));
return card;
}
function renderUsers(users) {
userGrid.innerHTML = '';
if (users.length === 0) {
userGrid.innerHTML = '<p class="no-results">No users found.</p>';
return;
}
users.forEach(user => {
const card = createUserCard(user);
userGrid.appendChild(card);
});
}
function showUserDetail(user) {
userDetail.classList.remove('hidden');
userGrid.classList.add('hidden');
userDetail.innerHTML = `
<button id="back-button" class="back-button">Back to Directory</button>
<div class="detail-card">
<div class="detail-header">
<div class="avatar-large">${user.name.charAt(0)}</div>
<div>
<h2>${user.name}</h2>
<p class="username">@${user.username}</p>
</div>
</div>
<div class="detail-body">
<div class="detail-section">
<h4>Contact</h4>
<p><span class="label">Email:</span> ${user.email}</p>
<p><span class="label">Phone:</span> ${user.phone}</p>
<p><span class="label">Website:</span> ${user.website}</p>
</div>
<div class="detail-section">
<h4>Address</h4>
<p>${user.address.street}, ${user.address.suite}</p>
<p>${user.address.city}, ${user.address.zipcode}</p>
</div>
<div class="detail-section">
<h4>Company</h4>
<p><span class="label">Name:</span> ${user.company.name}</p>
<p><span class="label">Catchphrase:</span> ${user.company.catchPhrase}</p>
</div>
</div>
</div>
`;
document.getElementById('back-button').addEventListener('click', () => {
userDetail.classList.add('hidden');
userGrid.classList.remove('hidden');
});
}
// --- Initialize ---
async function init() {
userGrid.innerHTML = '<p class="loading">Loading users...</p>';
allUsers = await fetchUsers();
if (allUsers.length === 0) {
userGrid.innerHTML = '<p class="error-message">Failed to load users. Please check your connection and refresh.</p>';
return;
}
renderUsers(allUsers);
}
init();
Your user directory now fetches data, renders interactive cards, and supports a detail view with navigation. Next up, you will add live search with a debounce function that puts closures to practical use.
Implement Live Search with Debouncing
Your user directory displays 10 cards and lets you click into detailed views. But with only 10 users, imagine how difficult it would be to find someone in a directory of hundreds or thousands.
In this step, you will add a live search feature that filters cards as the user types. You will also build a debounce utility function that demonstrates one of JavaScript's most important concepts: closures.
In this step, get ready to:
- Write a filter function using Array.filter() to search users by name, email, or city.
- Build a debounce utility that uses closures to delay execution.
- Wire the search input to the debounced filter and verify it in Chrome DevTools.
Create a feature branch and write the filter function
Just like you did for rendering cards, you will isolate this new feature on its own Git branch. This keeps your main branch stable while you experiment.
- Click the branch name (main) in the VCS widget in WebStorm's main window header.
- Select New Branch from the popup.
- Type feature/search as the branch name.
- Make sure Checkout branch is selected, then click Create.
The VCS widget now shows feature/search as the active branch.
What is Array.filter()?
In Python, you filter a list with a comprehension: [u for u in users if term in u.name.lower()]. JavaScript uses a method chain instead.
You call .filter() on an array and pass it a function. The function runs once per item and returns true to keep the item or false to exclude it.
- In app.js, find the // --- Initialize --- comment near the bottom of the file.
- Add the following code above that comment:
// --- Search with Debounce ---
function filterUsers(searchTerm) {
const term = searchTerm.toLowerCase().trim();
if (term === '') {
renderUsers(allUsers);
return;
}
const filtered = allUsers.filter(user =>
user.name.toLowerCase().includes(term) ||
user.email.toLowerCase().includes(term) ||
user.address.city.toLowerCase().includes(term)
);
renderUsers(filtered);
}
What does this code do?
- The term variable lowercases and trims the input so that searches are case-insensitive.
- If the search box is empty, it renders all users again (resets the view).
- The .filter() call checks each user's name, email, and city. If any field includes the search term, the user stays in the results.
- Finally, renderUsers(filtered) redraws the grid with only the matching cards.
- Save app.js.
You will not see a change in the browser yet. The next chunk wires this function to the search input.
Write a debounce utility using closures
Without debouncing, every single keystroke would trigger a full filter and re-render. With 10 users that is fine, but with hundreds it would feel sluggish.
A debounce function waits for the user to stop typing before it runs. If a new keystroke arrives within the delay window, it resets the timer. This is the canonical example of a closure in JavaScript.
What is a closure?
A closure is a function that remembers variables from its outer scope even after the outer function has returned. In debounce, the returned function "closes over" the timeoutId variable. Each call can read and update timeoutId because the variable persists in the closure's memory.
Python has closures too, but they are rarely used this way because Python relies on decorators and classes for similar patterns.
- In app.js, find the // --- Search with Debounce --- comment you just added.
- Add the following function directly below that comment, above your filterUsers function:
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
What does this code do?
- The let timeoutId declaration lives inside debounce but persists across calls thanks to the closure.
- Each time the returned function is called, clearTimeout(timeoutId) cancels any pending timer.
- Then setTimeout sets a new timer. Only when the delay (300ms) passes without another call does fn actually execute.
- The ...args spread ensures whatever arguments the caller passes get forwarded to the original function.
Why is setTimeout a Web API?
The setTimeout function is not part of the JavaScript language itself. It is a Web API provided by the browser. The browser's timer puts the callback on the macrotask queue after the delay, and the event loop picks it up once the call stack is empty.
- Save app.js.
The debounce function is ready but nothing calls it yet. Next you will wire it to the search input.
- In app.js, find your filterUsers function and scroll to the end of it.
- Add the following code below filterUsers, above the // --- Initialize --- comment:
const debouncedFilter = debounce(filterUsers, 300);
searchInput.addEventListener('input', (event) => {
debouncedFilter(event.target.value);
});
What does this code do?
- The debouncedFilter constant stores the debounced version of filterUsers with a 300ms delay.
- The event listener on searchInput fires every time the user types. It passes the current input value to the debounced function.
- Because of the debounce, filterUsers only runs 300ms after the user stops typing.
- Save app.js.
- Switch to Chrome and refresh the page.
- Type a name (like Leanne) into the search box.
You should see the grid filter down to only the matching user card. Clear the search box and all 10 cards reappear.
Search not filtering?
- Make sure the event listener references debouncedFilter and not filterUsers directly.
- Check that searchInput is defined at the top of your file as document.getElementById('search-input'). The ID must match your HTML.
- Open the Console tab in Chrome DevTools and look for error messages. A typo in a variable name will show as a ReferenceError.
Help me figure out why my search filter is not working in app.js.
Test the debounce in DevTools and commit your work
The search works, but how can you prove the debounce is actually delaying execution? You will add a temporary log inside the debounce wrapper and use WebStorm's debugger to inspect the closure.
- Open Chrome DevTools by pressing Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows).
- Switch to the Console tab.
- In app.js, temporarily add console.log('debounce fired', searchTerm) as the first line inside the filterUsers function body.
- Save and refresh the browser.
- Type quickly in the search box and watch the Console.
You should see only one log message appear 300ms after you stop typing, not one per keystroke. This confirms the debounce is working.
- Now test with WebStorm's debugger. Right-click index.html in the Project tool window and choose Debug 'index.html'.
- Set a breakpoint on the clearTimeout(timeoutId) line inside the debounce function by clicking in the gutter (left margin of the editor).
- Type a character in the search box in the Chrome window that opened.
- When the breakpoint hits, look at the Variables pane in WebStorm's Debug tool window. You should see timeoutId listed under the closure scope.
What does this prove?
The timeoutId variable appears in the closure scope because it was declared in the outer debounce function. The inner function remembers it across multiple calls. This is a closure in action.
- Stop the debugger by clicking the red Stop button in the Debug tool window.
- Remove the temporary console.log line you added to filterUsers.
- Save app.js.
Now commit your work and merge the branch back into main.
- Open the Commit tool window by pressing Cmd+K (macOS) or Ctrl+K (Windows).
- Select app.js in the changed files list.
- Enter the commit message: Add live search with debounce (closure pattern).
- Click Commit.
- Now merge back into main. Click the branch name feature/search in the VCS widget.
- Under Local, select main and click Checkout.
- Click the branch name (main) in the VCS widget again.
- Under Local, select feature/search and click Merge 'feature/search' into 'main'.
WebStorm merges the branch cleanly. Your search functionality is now part of main.
Merge option not appearing?
- Make sure you checked out main first. You can only merge another branch into the currently active branch.
- If you see a conflict, check that you did not edit the same lines on both branches. Accept the incoming changes from feature/search.
Help me merge my feature/search branch into main in WebStorm.
✔️ Awesome, I've got everything!
Great. Double check you have saved app.js and that your VCS widget shows main as the active branch.
ⓧ I'd like to double check the full code
Here is the complete app.js at this point in the project:
'use strict';
// --- State ---
let allUsers = [];
// --- DOM References ---
const searchInput = document.getElementById('search-input');
const userGrid = document.getElementById('user-grid');
const userDetail = document.getElementById('user-detail');
// --- API Functions ---
async function fetchUsers() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const users = await response.json();
return users;
} catch (error) {
console.error('Failed to fetch users:', error.message);
return [];
}
}
// --- Render Functions ---
function createUserCard(user) {
const card = document.createElement('div');
card.className = 'user-card';
card.dataset.userId = user.id;
card.innerHTML = `
<div class="user-card-header">
<div class="avatar">${user.name.charAt(0)}</div>
<div class="user-info">
<h3>${user.name}</h3>
<p class="username">@${user.username}</p>
</div>
</div>
<div class="user-card-body">
<p><span class="label">Email:</span> ${user.email}</p>
<p><span class="label">City:</span> ${user.address.city}</p>
<p><span class="label">Company:</span> ${user.company.name}</p>
</div>
`;
card.addEventListener('click', () => showUserDetail(user));
return card;
}
function renderUsers(users) {
userGrid.innerHTML = '';
if (users.length === 0) {
userGrid.innerHTML = '<p class="no-results">No users found.</p>';
return;
}
users.forEach(user => {
const card = createUserCard(user);
userGrid.appendChild(card);
});
}
function showUserDetail(user) {
userDetail.classList.remove('hidden');
userGrid.classList.add('hidden');
userDetail.innerHTML = `
<button id="back-button" class="back-button">Back to Directory</button>
<div class="detail-card">
<div class="detail-header">
<div class="avatar-large">${user.name.charAt(0)}</div>
<div>
<h2>${user.name}</h2>
<p class="username">@${user.username}</p>
</div>
</div>
<div class="detail-body">
<div class="detail-section">
<h4>Contact</h4>
<p><span class="label">Email:</span> ${user.email}</p>
<p><span class="label">Phone:</span> ${user.phone}</p>
<p><span class="label">Website:</span> ${user.website}</p>
</div>
<div class="detail-section">
<h4>Address</h4>
<p>${user.address.street}, ${user.address.suite}</p>
<p>${user.address.city}, ${user.address.zipcode}</p>
</div>
<div class="detail-section">
<h4>Company</h4>
<p><span class="label">Name:</span> ${user.company.name}</p>
<p><span class="label">Catchphrase:</span> ${user.company.catchPhrase}</p>
</div>
</div>
</div>
`;
document.getElementById('back-button').addEventListener('click', () => {
userDetail.classList.add('hidden');
userGrid.classList.remove('hidden');
});
}
// --- Search with Debounce ---
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
function filterUsers(searchTerm) {
const term = searchTerm.toLowerCase().trim();
if (term === '') {
renderUsers(allUsers);
return;
}
const filtered = allUsers.filter(user =>
user.name.toLowerCase().includes(term) ||
user.email.toLowerCase().includes(term) ||
user.address.city.toLowerCase().includes(term)
);
renderUsers(filtered);
}
const debouncedFilter = debounce(filterUsers, 300);
searchInput.addEventListener('input', (event) => {
debouncedFilter(event.target.value);
});
// --- Initialize ---
async function init() {
userGrid.innerHTML = '<p class="loading">Loading users...</p>';
allUsers = await fetchUsers();
if (allUsers.length === 0) {
userGrid.innerHTML = '<p class="error-message">Failed to load users. Please check your connection and refresh.</p>';
return;
}
renderUsers(allUsers);
}
init();
Your directory now has a live search that filters users in real time. Next up, you will add CSS styling and polish to make the app look professional.
Polish and Push to GitHub
Your user directory works. Search is responsive, cards render, and the detail view fires on click. But right now it looks like unstyled HTML from 1995.
In this step, you will add professional styling with CSS, verify the edge-case states display correctly, and push the final project to GitHub.
In this step, get ready to:
- Style the entire app with a responsive grid, card hover effects, and clean transitions.
- Verify empty-state, loading, and error-handling display correctly.
- Commit, review the Git log, and push to GitHub.
Add CSS styling with auto-reload
Before writing CSS, you will configure WebStorm to reload the browser on every change. This way you see style updates instantly without manually saving.
- Open WebStorm settings with Cmd+, (macOS) or Ctrl+Alt+S (Windows).
- Navigate to Tools then Web Browsers and Preview.
- Set Reload behavior to On Change.
- Click OK to apply the setting.
What does On Change do?
The default reload behavior is On Save, which refreshes the browser each time you save a file. On Change goes further and pushes updates to the browser as you type, without waiting for a save. This makes CSS iteration much faster.
Now open style.css in the editor. It is currently empty. You will build the stylesheet in three chunks, each producing a visible result.
Write the page layout and search styles
- Replace the empty contents of style.css with the following code:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f0f2f5;
color: #1a1a2e;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
header {
text-align: center;
margin-bottom: 2rem;
}
header h1 {
font-size: 2rem;
margin-bottom: 1rem;
color: #16213e;
}
.search-wrapper {
max-width: 500px;
margin: 0 auto;
}
#search-input {
width: 100%;
padding: 0.75rem 1rem;
font-size: 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
outline: none;
transition: border-color 0.2s;
}
#search-input:focus {
border-color: #4a90d9;
}
What does this code do?
The * selector resets default margins and padding so every browser starts from the same baseline. box-sizing: border-box ensures padding is included inside the element's total width.
The .container centers the app at a max width of 1200px. The search input gets a focus state that turns the border blue, giving the user visual feedback.
- Save style.css and refresh your browser.
- You should see the page background turn light grey, the heading centered, and the search input styled with rounded borders.
Still seeing unstyled HTML?
Confirm that your index.html contains <link rel="stylesheet" href="style.css"> in the <head> section. The filename must match exactly, including the lowercase s in style.
Help me debug why my CSS isn't loading in the browser.
Style the card grid and card components
- Add the following CSS below the #search-input:focus rule in style.css:
.user-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.user-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.user-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.user-card-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: #4a90d9;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
font-weight: bold;
}
.avatar-large {
width: 72px;
height: 72px;
border-radius: 50%;
background: #4a90d9;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
font-weight: bold;
}
.user-info h3 {
font-size: 1.1rem;
color: #16213e;
}
.username {
color: #666;
font-size: 0.9rem;
}
.user-card-body p {
margin-bottom: 0.25rem;
font-size: 0.9rem;
}
.label {
font-weight: 600;
color: #444;
}
What does this code do?
The .user-grid uses CSS Grid with auto-fill to create a responsive layout. Cards automatically wrap to the next row as the window shrinks. Each card has a minimum width of 300px.
The .user-card:hover rule lifts the card up by 2 pixels and deepens its shadow, giving a satisfying interactive feel. The transition property ensures the movement is smooth rather than instant.
- Save style.css and check your browser.
- You should see 10 user cards in a responsive grid. Each card is white with rounded corners and a subtle shadow. Hover over a card to see it lift slightly.
Cards not appearing in a grid?
Make sure your HTML has class="user-grid" on the div that wraps the cards. Check index.html for <div id="user-grid" class="user-grid">.
Help me figure out why my CSS grid isn't laying out the cards correctly.
Style the detail view and utility classes
- Add the following CSS below the .label rule in style.css:
.hidden {
display: none;
}
.no-results,
.loading,
.error-message {
text-align: center;
color: #666;
font-size: 1.1rem;
padding: 2rem;
grid-column: 1 / -1;
}
.error-message {
color: #d32f2f;
}
.back-button {
background: #4a90d9;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.9rem;
cursor: pointer;
margin-bottom: 1.5rem;
transition: background 0.2s;
}
.back-button:hover {
background: #357abd;
}
.detail-card {
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.detail-header {
display: flex;
align-items: center;
gap: 1.5rem;
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid #eee;
}
.detail-header h2 {
font-size: 1.5rem;
color: #16213e;
}
.detail-body {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.detail-section h4 {
color: #4a90d9;
margin-bottom: 0.5rem;
font-size: 1rem;
}
.detail-section p {
margin-bottom: 0.25rem;
font-size: 0.95rem;
}
What does this code do?
The .hidden class toggles visibility when switching between the grid and detail view. The .no-results, .loading, and .error-message classes style the feedback messages that appear during fetching or when search returns nothing.
The detail view uses grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)) to lay out the contact, address, and company sections responsively. On wide screens they sit side by side. On narrow screens they stack.
- Save style.css and click any user card in your browser.
- You should see a polished detail view with a large avatar, a blue Back to Directory button, and the user's contact, address, and company information arranged in columns.
Detail view not showing?
Verify that your app.js toggles the hidden class on userDetail and userGrid when a card is clicked. Open Chrome DevTools (F12) and check whether user-detail has class="user-detail hidden" before you click.
Help me figure out why clicking a card doesn't show the detail view.
✔️ Awesome, I've got everything!
Great. Double check you have saved style.css and all three sections are present.
ⓧ I'd like to double check the full code
Here is the complete style.css file for comparison:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f0f2f5;
color: #1a1a2e;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
header {
text-align: center;
margin-bottom: 2rem;
}
header h1 {
font-size: 2rem;
margin-bottom: 1rem;
color: #16213e;
}
.search-wrapper {
max-width: 500px;
margin: 0 auto;
}
#search-input {
width: 100%;
padding: 0.75rem 1rem;
font-size: 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
outline: none;
transition: border-color 0.2s;
}
#search-input:focus {
border-color: #4a90d9;
}
.user-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.user-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.user-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.user-card-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: #4a90d9;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
font-weight: bold;
}
.avatar-large {
width: 72px;
height: 72px;
border-radius: 50%;
background: #4a90d9;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
font-weight: bold;
}
.user-info h3 {
font-size: 1.1rem;
color: #16213e;
}
.username {
color: #666;
font-size: 0.9rem;
}
.user-card-body p {
margin-bottom: 0.25rem;
font-size: 0.9rem;
}
.label {
font-weight: 600;
color: #444;
}
.hidden {
display: none;
}
.no-results,
.loading,
.error-message {
text-align: center;
color: #666;
font-size: 1.1rem;
padding: 2rem;
grid-column: 1 / -1;
}
.error-message {
color: #d32f2f;
}
.back-button {
background: #4a90d9;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.9rem;
cursor: pointer;
margin-bottom: 1.5rem;
transition: background 0.2s;
}
.back-button:hover {
background: #357abd;
}
.detail-card {
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.detail-header {
display: flex;
align-items: center;
gap: 1.5rem;
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid #eee;
}
.detail-header h2 {
font-size: 1.5rem;
color: #16213e;
}
.detail-body {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.detail-section h4 {
color: #4a90d9;
margin-bottom: 0.5rem;
font-size: 1rem;
}
.detail-section p {
margin-bottom: 0.25rem;
font-size: 0.95rem;
}
Verify empty state, loading, and error handling
Your app.js already handles three important edge cases. Now that CSS is in place, you can confirm they look correct.
- Type zzzzz in the search box. You should see a centered message that says "No users found." in grey text.
- Clear the search box and hard-refresh the page (Cmd+Shift+R on macOS or Ctrl+Shift+R on Windows). Watch for a brief "Loading users..." message before the cards appear.
- To test the error state, open Chrome DevTools (F12), go to the Network tab, and check Offline. Refresh the page. You should see a red error message that says "Failed to load users. Please check your connection and refresh."
- Uncheck Offline in DevTools and refresh again to confirm the app recovers.
Where do these states come from?
The init() function in app.js sets the loading message before calling fetchUsers(). If the fetch returns an empty array (which happens on network errors), it replaces the grid content with the error message. The renderUsers() function checks for an empty array after filtering and shows the no-results message.
- Commit your work by pressing Cmd+K (macOS) or Ctrl+K (Windows) to open the Commit tool window.
- Select all changed files (style.css should be listed).
- Enter the commit message:
Add polish: loading state, empty state, error handling UI
- Click Commit.
Review the Git log and push to GitHub
Before pushing, take a moment to appreciate the commit history you have built across multiple feature branches.
- Open the Git tool window by pressing Alt+9 (or go to View then Tool Windows then Git).
- Switch to the Log tab.
- In the branch filter dropdown, select All to view commits across all branches.
You should see a commit graph showing your feature branches (feature/fetch-users, feature/render-cards, feature/search) merging into main. Each branch has a descriptive commit message.
Why review the log?
In professional teams, a clean Git history tells a story. Reviewers can trace each feature through its branch, see what changed, and understand why. The graph view makes merge points visible at a glance.
- Push to GitHub by pressing Cmd+Shift+K (macOS) or Ctrl+Shift+K (Windows).
- In the Push Commits dialog, review the list of commits to be pushed, then click Push.
Push rejected or authentication error?
If WebStorm asks you to log in, go to Settings then Version Control then GitHub and confirm your account is connected. If the push is rejected because the remote has newer commits, click Merge when prompted, then push again.
Help me fix a push rejection or authentication error in WebStorm.
Your user directory is fully styled, handles edge cases gracefully, and lives on GitHub with a professional commit history. Next up, the Secret Mission challenges you to add sorting and keyboard shortcuts.
Secret mission
Add Sorting and Keyboard Shortcuts
Your directory works great, but power users expect more. Can you add sorting controls and keyboard shortcuts that let users navigate without touching the mouse? This challenge combines UI state management with multiple event listeners and closures.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally and uses a free GitHub repository. 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:
- A GitHub repository containing your user directory project (free, no cost).
- Local project files on your Desktop (index.html, style.css, app.js, .gitignore).
Keep everything running
No action needed. Choose this if you're still actively building or want to keep the project as a portfolio piece.
- Your GitHub repository stays online for free. There are no running costs.
- Your local project files remain on your Desktop for future reference or continued development.
Pause - I'll come back to this later
Since there are no running processes or paid services, there is nothing to pause. Your GitHub repository and local files will be here when you return.
- Your repository stays on GitHub at no cost.
- Your local files stay on disk until you manually delete them.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Delete the GitHub repository by opening the repository page on GitHub.
- Click Settings in the repository navigation bar.
- Scroll to the bottom to find the Danger Zone section.
- Click Delete this repository and follow the confirmation prompts.
- Delete your local project files by running this command in your terminal:
rm -rf ~/Desktop/user-directory
On Windows, run this instead:
rmdir /s /q ~\Desktop\user-directory
Nice Work!
Nice Work!
Nice work! You just built a fully interactive web app from scratch using vanilla JavaScript, complete with live API data, real-time search, and a professional Git history.
You've learned how to:
- Built an async data-fetching layer using fetch() and async/await, and dynamically rendered user cards with DOM manipulation.
- Implemented a debounce function from scratch, demonstrating how closures work in JavaScript by persisting the timeoutId variable between calls.
- Maintained a professional Git workflow with feature branches and meaningful commits using WebStorm's integrated version control.
- Secret Mission: Added sorting controls and keyboard shortcuts, combining multiple event patterns with closure-based state management.
Ready to quiz yourself?