Build a Job Application Tracker

Build and publish a responsive job tracker with persistent browser storage.

Introduction

30 Second Summary

Job searches get messy when company names live in one document while interview updates sit in emails. A promising role can disappear from view just when it needs your attention.

In this project, you will build a responsive Job Application Tracker for managing every opportunity. Your finished app will preserve its data with browser storage before going live through GitHub Pages.

What You'll Build

You open your public tracker, add a role, and watch a new application card appear while the dashboard totals update immediately.

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

  • A responsive job tracker where you can record a company, role, and application status from any screen size.
  • An interactive dashboard where status changes, deletions, and application totals update without a page reload.
  • A shareable public app that restores saved applications after a reload, plus readable source code in a public GitHub repository.
  • Secret Mission: Add a status filter that shows one stage of the job search without changing the saved application data.

Before We Start

This is your moment to commit to building a Job Application Tracker that organizes your employment search. You will also use it to demonstrate practical front-end web development skills.

Set Up Your Windows Development Toolkit

Your Job Application Tracker needs a repeatable local workspace. It also needs a public code identity that employers can connect to your work.

This step prepares that foundation with Visual Studio Code. It also sets up Git for Windows plus a verified GitHub account.

In this step, get ready to:
  • Install Visual Studio Code plus Git for Windows.
  • Create your GitHub identity for future commits.
  • Open an empty project workspace in Visual Studio Code.
Install Visual Studio Code and Git

PowerShell gives you one place to check whether Windows can find each development tool. You will test each command before following the matching installation path.

  • Press the Windows key to open the search bar.
  • Type PowerShell into the search bar.
  • Press Enter to open PowerShell.
  • Check whether PowerShell recognizes Visual Studio Code by running this command:
code --version

What does this check?

This command asks Visual Studio Code to print its version information. It also prints the commit ID plus the installed architecture.

✔️ Visual Studio Code prints version information

Visual Studio Code is installed. PowerShell can find its command-line tool.

Good start. Your code editor is available from a fresh terminal workflow.

ⓧ The command is not recognized

Windows cannot find the Visual Studio Code command yet. The Windows User Setup installer adds that command to your system path.

The installer opens a setup wizard. Its prompts guide you through the installation before a fresh PowerShell window loads the updated path.

  • Open the official Visual Studio Code setup page in your browser.
  • Download the User Setup installer for Windows.
  • Run the downloaded .exe file.
  • Follow the installer prompts to complete the installation.
  • Close the current PowerShell window after the installer finishes.
  • Press the Windows key to open the search bar.
  • Type PowerShell into the search bar.
  • Press Enter to open a fresh PowerShell window.

The fresh window loads the command path added by the installer.

  • Confirm that the fresh window recognizes Visual Studio Code by running:
code --version

What should you see?

You should see Visual Studio Code version information. This output confirms that Windows can locate the editor from PowerShell.

Still missing the code command?

Close every PowerShell window after the installer completes. An older window can retain the previous command path.

Run the Windows User Setup installer again if a fresh window still cannot find the command.

Help me troubleshoot the Visual Studio Code command on Windows.

  • Check whether PowerShell recognizes Git by running this command:
git version

What does this check?

This command asks Git to print its installed version. A version response confirms that PowerShell can locate Git.

✔️ Git prints version information

Git is installed. PowerShell can find the tool you will use to record project changes.

ⓧ The command is not recognized

Git for Windows offers separate x64 plus ARM64 installers. Finding the matching option can feel fiddly, so check your Windows architecture first.

  • Display your Windows architecture by running this command:
Get-ComputerInfo -Property OsArchitecture

What does the result mean?

The result identifies the architecture used by your Windows installation. Choose the Git for Windows installer with the matching architecture.

  • Open the official Git for Windows installer page in your browser.
  • Download the installer matching the architecture shown in PowerShell.
  • Run the downloaded installer.
  • Follow the installer prompts to complete the installation.
  • Close the current PowerShell window after the installer finishes.
  • Press the Windows key to open the search bar.
  • Type PowerShell into the search bar.
  • Press Enter to open a fresh PowerShell window.

The fresh window now loads the command path created by Git for Windows.

  • Confirm that PowerShell recognizes Git by running:
git version

What should you see?

You should see Git version information. That output confirms that Git is available outside the installer.

Still missing the Git command?

Close every PowerShell window before checking again. An older window can retain the path from before the installation.

Run the installer matching your Windows architecture again if a fresh window still cannot find Git.

Help me troubleshoot my Git for Windows installation.

Create and configure your GitHub identity

GitHub gives your finished tracker a public home. A verified email address unlocks core account tasks such as repository creation.

This part includes an account password. GitHub keeps that credential inside its secure sign-up flow, so you never paste it into PowerShell.

  • Open the official GitHub signup page in your browser.
  • Follow the prompts to create a free personal account.
  • Open the verification message sent to your email address.
  • Use the verification link in that message to verify your account.
  • Sign in to GitHub with the verified account.
  • Select the account menu in the upper-right corner of GitHub.
  • Select New repository to test your repository access.

You should see the new repository form with fields for the owner plus repository name. This confirms that your verified account can create repositories.

Git records an author name plus email address in every future commit. Use the same verified GitHub email address so those commits can connect to your account.

  • Switch back to the PowerShell window from the installation checks.
  • Configure your future commit identity by replacing both placeholders in these commands:
git config --global user.name "Your Name"
git config --global user.email "YOUR_EMAIL"

What does this configuration do?

The first command sets the name attached to future commits. The second command sets the matching author email.

The global scope applies this identity to repositories used by your Windows account.

  • Display the saved Git identity by running these commands:
git config --global user.name
git config --global user.email

What should Git return?

The first line should show your chosen commit name. The second line should show your verified GitHub email address.

That is your public code identity connected. Git can now label your future tracker commits with the correct author details.

Seeing the wrong Git identity?

Run the configuration commands again with the correct values. Each command replaces the previous global value.

Use the email address verified on GitHub if the displayed email belongs to another account.

Help me correct my global Git identity.

Create your project workspace

The empty job-application-tracker folder gives every project file one predictable home. Opening that folder in Visual Studio Code keeps the next step inside the correct workspace.

  • Create the project folder on your Windows Desktop by running these commands:
New-Item -Path "$env:USERPROFILE\Desktop" -Name "job-application-tracker" -ItemType Directory
Set-Location "$env:USERPROFILE\Desktop\job-application-tracker"

What do these commands do?

The first command creates job-application-tracker on your Desktop. The second command moves PowerShell into that folder.

PowerShell should print a directory result that names job-application-tracker. Your prompt now points to the new folder.

Does the folder already exist?

Check the existing folder before continuing. Keep it only when it is empty.

Rename the existing folder in File Explorer if it contains unrelated files. Run the creation commands again afterward.

Help me prepare the empty project folder.

Visual Studio Code may ask whether you trust this folder when it opens. You created the empty folder yourself, so confirm that you trust it if prompted.

  • Open the current project folder in Visual Studio Code by running this command:
code .

What does this command do?

The command opens the current PowerShell location in Visual Studio Code. The period represents the folder PowerShell currently uses.

  • Confirm that you trust the folder if Visual Studio Code displays a trust prompt.

You should see job-application-tracker at the top of the Explorer sidebar. The folder should contain no files.

Project folder not opening?

Restart PowerShell if the code command is missing. A fresh window reloads the path created by the Visual Studio Code installer.

Confirm that your PowerShell prompt ends with job-application-tracker before running the folder command again.

Help me open the correct project folder.

One final check proves that both development commands remain available outside the installer windows.

  • Press the Windows key to open the search bar.
  • Type PowerShell into the search bar.
  • Press Enter to open a new PowerShell window.

Before you run the final check, do you expect both tools to answer from this brand-new PowerShell window?

  • Verify both installations by running these commands:
code --version
git version

What does this final check prove?

The first command confirms that Windows can locate Visual Studio Code. The second command confirms that Windows can locate Git.

You should see Visual Studio Code version information first. Git version information should appear underneath it.

You have the full toolkit ready. Your verified account plus local workspace can now support the tracker you are about to build.

Is one command still missing?

Restart Windows if a command works in one PowerShell window but disappears in another. A restart reloads the command path for every new process.

Run the relevant installer again if the command remains unavailable after the restart.

Help me fix the missing development command.

✔️ Awesome, I've got everything!

Your verified GitHub account is signed in. Visual Studio Code has the empty job-application-tracker folder open.

PowerShell prints version information for both tools. Git reports your configured name plus verified GitHub email address.

ⓧ I'd like to double check the full code

This setup step creates no application source code. Double check these exact setup artifacts before continuing.

  • Git global configuration: Configured with the learner's name and verified GitHub email address.
  • GitHub account: Verified personal GitHub account signed in and able to create repositories.
  • job-application-tracker: Local project folder created on the Windows device and open in Visual Studio Code.

Your Windows development toolkit is ready. Next, you will turn the empty workspace into the first visible version of your Job Application Tracker.

Build the Tracker Interface

Your Windows toolkit is ready. The empty job-application-tracker folder can now become a working browser interface.

This visible interface gives you an early win. It establishes the form, dashboard, statistics, and empty state that the application logic controls later.

You will use semantic HTML to give each area a clear purpose. Responsive CSS keeps the tracker readable across wide and narrow windows.

In this step, get ready to:
  • Create the semantic structure for the tracker form and dashboard.
  • Style the interface as a responsive two-column layout.
  • Open the tracker in a browser and verify its narrow-screen layout.
Build the semantic page structure

The page structure separates the introductory header from the form and dashboard. Connected labels make each form field clear to browsers and assistive technology.

  • Select the new-file control at the top of the file sidebar in Visual Studio Code.
  • Enter index.html as the file name.

You should see index.html inside the job-application-tracker folder in the file sidebar.

  • Create the document metadata and introductory header by pasting this code into index.html:
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="A browser-based tracker for job applications and interview progress." />
    <title>Job Application Tracker</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <header class="hero">
      <div class="hero-content">
        <p class="eyebrow">Career dashboard</p>
        <h1>Job Application Tracker</h1>
        <p>Keep every opportunity and next step in one clear view.</p>
      </div>
    </header>
  </body>
</html>

What does this structure do?

  • The metadata helps the page display correctly across different screen sizes.
  • The stylesheet link connects the page to styles.css.
  • The header introduces the tracker before someone reaches the application form.
  • Save index.html.
  • Navigate to the job-application-tracker folder in File Explorer.
  • Double-click index.html to open it in your browser.

You should see the Job Application Tracker heading with its supporting text. The browser displays the page without finished styling because styles.css does not exist yet.

Don't see the tracker heading?

Confirm that index.html is saved inside job-application-tracker. Check that the header sits between the opening and closing body tags.

Help me find why the tracker heading is missing in my browser.

  • Switch back to index.html in Visual Studio Code.
  • Place your cursor after the closing header tag.
  • Add the application form and dashboard by pasting this code before the closing body tag:
    <main class="container">
      <section class="panel" aria-labelledby="add-application-heading">
        <h2 id="add-application-heading">Add an application</h2>
        <form id="application-form" class="form-grid">
          <label class="form-field" for="company"><span>Company</span><input id="company" name="company" type="text" maxlength="60" placeholder="Example Company" required /></label>
          <label class="form-field" for="role"><span>Role</span><input id="role" name="role" type="text" maxlength="60" placeholder="Junior Web Developer" required /></label>
          <label class="form-field" for="status"><span>Status</span><select id="status" name="status"><option value="Applied">Applied</option><option value="Interviewing">Interviewing</option><option value="Offer">Offer</option></select></label>
          <button class="primary-button" type="submit">Add application</button>
          <p id="form-message" class="form-message" role="status" aria-live="polite"></p>
        </form>
      </section>
      <section class="dashboard" aria-labelledby="applications-heading">
        <div class="dashboard-heading"><div><p class="eyebrow">Your progress</p><h2 id="applications-heading">Applications</h2></div></div>
        <div class="stats" aria-label="Application statistics"><article class="stat-card"><span id="total-count" class="stat-number">0</span><span>Total</span></article><article class="stat-card"><span id="interviewing-count" class="stat-number">0</span><span>Interviewing</span></article><article class="stat-card"><span id="offer-count" class="stat-number">0</span><span>Offers</span></article></div>
        <div id="application-list" class="application-list"></div>
        <p id="empty-state" class="empty-state">No applications yet. Add your first role above.</p>
      </section>
    </main>

How is the interface organized?

  • The form collects a company, role, and application status.
  • The required attributes prevent an empty company or role from being submitted.
  • The dashboard starts all three statistics at 0.
  • The empty state explains what to do before the list contains any application cards.
  • Save index.html.
  • Switch back to the browser window from earlier.
  • Refresh the page.

You should see company, role, and status fields. You should also see three zero-valued statistics above the empty-state message.

  • Select Add application while the company field is empty.

Your browser should highlight the missing required field. This confirms that the form protects its essential input before JavaScript is added.

Is part of the interface missing?

Confirm that the complete main block sits after the closing header tag. Check that it remains above the closing body tag.

Help me debug the missing form or dashboard in my HTML.

✔️ Awesome, I've got everything!

Your saved index.html now shows the complete unstyled tracker interface.

ⓧ I'd like to double check the full code

Compare your complete index.html file with this version:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="A browser-based tracker for job applications and interview progress." />
    <title>Job Application Tracker</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <header class="hero">
      <div class="hero-content">
        <p class="eyebrow">Career dashboard</p>
        <h1>Job Application Tracker</h1>
        <p>Keep every opportunity and next step in one clear view.</p>
      </div>
    </header>
    <main class="container">
      <section class="panel" aria-labelledby="add-application-heading">
        <h2 id="add-application-heading">Add an application</h2>
        <form id="application-form" class="form-grid">
          <label class="form-field" for="company"><span>Company</span><input id="company" name="company" type="text" maxlength="60" placeholder="Example Company" required /></label>
          <label class="form-field" for="role"><span>Role</span><input id="role" name="role" type="text" maxlength="60" placeholder="Junior Web Developer" required /></label>
          <label class="form-field" for="status"><span>Status</span><select id="status" name="status"><option value="Applied">Applied</option><option value="Interviewing">Interviewing</option><option value="Offer">Offer</option></select></label>
          <button class="primary-button" type="submit">Add application</button>
          <p id="form-message" class="form-message" role="status" aria-live="polite"></p>
        </form>
      </section>
      <section class="dashboard" aria-labelledby="applications-heading">
        <div class="dashboard-heading"><div><p class="eyebrow">Your progress</p><h2 id="applications-heading">Applications</h2></div></div>
        <div class="stats" aria-label="Application statistics"><article class="stat-card"><span id="total-count" class="stat-number">0</span><span>Total</span></article><article class="stat-card"><span id="interviewing-count" class="stat-number">0</span><span>Interviewing</span></article><article class="stat-card"><span id="offer-count" class="stat-number">0</span><span>Offers</span></article></div>
        <div id="application-list" class="application-list"></div>
        <p id="empty-state" class="empty-state">No applications yet. Add your first role above.</p>
      </section>
    </main>
  </body>
</html>
Style the tracker

The stylesheet turns the document structure into a clear dashboard. Each group of rules creates a visible change you can check in the browser.

  • Select the new-file control at the top of the file sidebar.
  • Enter styles.css as the file name.

You should see styles.css beside index.html in the job-application-tracker folder.

  • Establish the page colors and hero styling by pasting this code into styles.css:
:root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #172033; background: #f4f7fb; --navy: #172033; --blue: #2563eb; --blue-dark: #1d4ed8; --line: #dbe3ef; --muted: #64748b; --surface: #ffffff; --danger: #b42318; }
* { box-sizing: border-box; }
body { margin: 0; min-width: 320px; min-height: 100vh; background: #f4f7fb; }
button, input, select { font: inherit; }
button, select { cursor: pointer; }
.hero { padding: 3.5rem 1.25rem 5.5rem; color: #ffffff; background: linear-gradient(135deg, #172033, #1d4ed8); }
.hero-content { width: min(1100px, 100%); margin: 0 auto; }
.hero h1 { max-width: 720px; margin: 0.25rem 0 0.75rem; font-size: clamp(2.25rem, 6vw, 4.5rem); line-height: 1; }
.hero p:last-child { max-width: 620px; margin: 0; color: #dbeafe; font-size: 1.05rem; }
.eyebrow { margin: 0; color: #2563eb; font-size: 0.75rem; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; }
.hero .eyebrow { color: #bfdbfe; }

What do these styles control?

  • The root rule defines the page font and reusable colors.
  • The body rule creates a pale background across the full browser height.
  • The hero rules create the blue gradient and responsive heading size.
  • Save styles.css.
  • Switch back to the browser.
  • Refresh the page.

You should see a dark blue gradient behind the white tracker heading. The rest of the page should have a pale background.

Is the page still plain?

Confirm that styles.css sits beside index.html. Check that the stylesheet file name matches the link inside index.html.

Help me find why styles.css is not affecting index.html.

  • Place your cursor after the final rule in styles.css.
  • Add the two-column layout and form styling by pasting this code below it:
.container { display: grid; grid-template-columns: minmax(280px, 360px) minmax(0, 1fr); gap: 1.5rem; width: min(1100px, calc(100% - 2rem)); margin: -3rem auto 3rem; align-items: start; }
.panel, .dashboard { padding: 1.5rem; border: 1px solid var(--line); border-radius: 1rem; background: var(--surface); box-shadow: 0 16px 40px rgba(23, 32, 51, 0.08); }
.panel h2, .dashboard h2 { margin: 0 0 1rem; }
.form-grid { display: grid; gap: 1rem; }
.form-field, .status-control { display: grid; gap: 0.4rem; color: var(--navy); font-size: 0.9rem; font-weight: 700; }
input, select { width: 100%; padding: 0.75rem 0.85rem; border: 1px solid #cbd5e1; border-radius: 0.65rem; color: var(--navy); background: #ffffff; }
input:focus, select:focus, button:focus-visible { outline: 3px solid rgba(37, 99, 235, 0.25); outline-offset: 2px; }
.primary-button, .danger-button { border: 0; border-radius: 0.65rem; font-weight: 800; }
.primary-button { padding: 0.85rem 1rem; color: #ffffff; background: var(--blue); }
.primary-button:hover { background: var(--blue-dark); }
.form-message { min-height: 1.25rem; margin: 0; color: var(--blue-dark); font-size: 0.9rem; }
.dashboard-heading { display: flex; justify-content: space-between; gap: 1rem; align-items: end; }

How does the main layout work?

  • The container uses a narrow form column beside a wider dashboard column.
  • The panel rules give both columns matching borders, rounded corners, and shadows.
  • The form rules make each control fill the available width.
  • The focus outline makes keyboard navigation easier to follow.
  • Save styles.css.
  • Switch back to the browser.
  • Refresh the page.

You should see the form and dashboard inside separate white panels. On a wide window, the form should sit to the left of the dashboard.

Are the panels stacked?

Make the browser window wider before checking the grid. Confirm that the .container selector matches the class in index.html.

Help me debug the two-column tracker layout.

  • Place your cursor after the .dashboard-heading rule in styles.css.
  • Add the dashboard details and responsive behavior by pasting this code below it:
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem; margin-bottom: 1.25rem; }
.stat-card { display: grid; gap: 0.2rem; padding: 1rem; border-radius: 0.75rem; background: #eff6ff; color: var(--muted); }
.stat-number { color: var(--navy); font-size: 1.7rem; font-weight: 900; }
.application-list { display: grid; gap: 0.85rem; }
.application-card { display: flex; justify-content: space-between; gap: 1rem; padding: 1rem; border: 1px solid var(--line); border-radius: 0.8rem; background: #ffffff; }
.application-card h3 { margin: 0 0 0.25rem; }
.role-name { margin: 0; color: var(--muted); }
.card-actions { display: flex; gap: 0.65rem; align-items: end; }
.status-control { min-width: 145px; }
.danger-button { padding: 0.78rem 0.9rem; color: var(--danger); background: #fee4e2; }
.danger-button:hover { background: #fecdca; }
.empty-state { margin: 1rem 0 0; padding: 1.5rem; border: 1px dashed #94a3b8; border-radius: 0.8rem; color: var(--muted); text-align: center; }
.empty-state[hidden] { display: none; }
@media (max-width: 760px) { .container { grid-template-columns: 1fr; } .application-card { flex-direction: column; } .card-actions { align-items: stretch; } }
@media (max-width: 520px) { .stats { grid-template-columns: 1fr; } .card-actions { flex-direction: column; } }

What changes at smaller widths?

  • The statistic cards use three equal columns while enough space is available.
  • The empty state gains a dashed border that separates it from the dashboard.
  • The first media query changes the main grid to one column.
  • The second media query stacks the statistics on very narrow screens.
  • Save styles.css.
  • Switch back to the browser.
  • Refresh the page.

You should see three pale blue statistic cards in one row. The empty-state message should sit inside a dashed box.

Don't see the dashboard styles?

Confirm that the statistic selector is written as .stats. Check that each statistic uses the stat-card class in index.html.

Help me compare the dashboard selectors with my HTML classes.

✔️ Awesome, I've got everything!

Your form, dashboard, statistics, and empty state now have their finished styles.

ⓧ I'd like to double check the full code

Compare your complete styles.css file with this version:

:root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #172033; background: #f4f7fb; --navy: #172033; --blue: #2563eb; --blue-dark: #1d4ed8; --line: #dbe3ef; --muted: #64748b; --surface: #ffffff; --danger: #b42318; }
* { box-sizing: border-box; }
body { margin: 0; min-width: 320px; min-height: 100vh; background: #f4f7fb; }
button, input, select { font: inherit; }
button, select { cursor: pointer; }
.hero { padding: 3.5rem 1.25rem 5.5rem; color: #ffffff; background: linear-gradient(135deg, #172033, #1d4ed8); }
.hero-content { width: min(1100px, 100%); margin: 0 auto; }
.hero h1 { max-width: 720px; margin: 0.25rem 0 0.75rem; font-size: clamp(2.25rem, 6vw, 4.5rem); line-height: 1; }
.hero p:last-child { max-width: 620px; margin: 0; color: #dbeafe; font-size: 1.05rem; }
.eyebrow { margin: 0; color: #2563eb; font-size: 0.75rem; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; }
.hero .eyebrow { color: #bfdbfe; }
.container { display: grid; grid-template-columns: minmax(280px, 360px) minmax(0, 1fr); gap: 1.5rem; width: min(1100px, calc(100% - 2rem)); margin: -3rem auto 3rem; align-items: start; }
.panel, .dashboard { padding: 1.5rem; border: 1px solid var(--line); border-radius: 1rem; background: var(--surface); box-shadow: 0 16px 40px rgba(23, 32, 51, 0.08); }
.panel h2, .dashboard h2 { margin: 0 0 1rem; }
.form-grid { display: grid; gap: 1rem; }
.form-field, .status-control { display: grid; gap: 0.4rem; color: var(--navy); font-size: 0.9rem; font-weight: 700; }
input, select { width: 100%; padding: 0.75rem 0.85rem; border: 1px solid #cbd5e1; border-radius: 0.65rem; color: var(--navy); background: #ffffff; }
input:focus, select:focus, button:focus-visible { outline: 3px solid rgba(37, 99, 235, 0.25); outline-offset: 2px; }
.primary-button, .danger-button { border: 0; border-radius: 0.65rem; font-weight: 800; }
.primary-button { padding: 0.85rem 1rem; color: #ffffff; background: var(--blue); }
.primary-button:hover { background: var(--blue-dark); }
.form-message { min-height: 1.25rem; margin: 0; color: var(--blue-dark); font-size: 0.9rem; }
.dashboard-heading { display: flex; justify-content: space-between; gap: 1rem; align-items: end; }
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem; margin-bottom: 1.25rem; }
.stat-card { display: grid; gap: 0.2rem; padding: 1rem; border-radius: 0.75rem; background: #eff6ff; color: var(--muted); }
.stat-number { color: var(--navy); font-size: 1.7rem; font-weight: 900; }
.application-list { display: grid; gap: 0.85rem; }
.application-card { display: flex; justify-content: space-between; gap: 1rem; padding: 1rem; border: 1px solid var(--line); border-radius: 0.8rem; background: #ffffff; }
.application-card h3 { margin: 0 0 0.25rem; }
.role-name { margin: 0; color: var(--muted); }
.card-actions { display: flex; gap: 0.65rem; align-items: end; }
.status-control { min-width: 145px; }
.danger-button { padding: 0.78rem 0.9rem; color: var(--danger); background: #fee4e2; }
.danger-button:hover { background: #fecdca; }
.empty-state { margin: 1rem 0 0; padding: 1.5rem; border: 1px dashed #94a3b8; border-radius: 0.8rem; color: var(--muted); text-align: center; }
.empty-state[hidden] { display: none; }
@media (max-width: 760px) { .container { grid-template-columns: 1fr; } .application-card { flex-direction: column; } .card-actions { align-items: stretch; } }
@media (max-width: 520px) { .stats { grid-template-columns: 1fr; } .card-actions { flex-direction: column; } }
Verify the responsive layout

A responsive layout adapts to the space available in the browser. This final check proves that the tracker remains readable when the window becomes narrow.

  • Switch back to the browser window showing index.html.
  • Refresh the page.
  • Widen the browser window until the form sits to the left of the dashboard.

You should see a two-column Job Application Tracker. The form should contain company, role, and status fields.

Before you resize the browser, which parts of the interface do you expect to stack first?

  • Narrow the browser window below 760px.

You should see the dashboard move below the form. The page should now use one main column.

  • Narrow the browser window below 520px.

You should see the Total, Interviewing, and Offers cards stack into one column. The form and empty-state message should remain readable.

Does the layout stay in two columns?

Confirm that both media queries appear at the bottom of styles.css. Check that the first media query targets .container.

Help me debug why the responsive layout does not switch to one column.

That is your first browser-ready win. Next up, you will connect the form to JavaScript so applications appear as interactive cards.

Make Applications Interactive

Your responsive tracker now presents the form and dashboard in a clear browser interface. The next goal is to make every control respond to the job seeker.

JavaScript will turn form values into application cards and live statistics. You will also test how this in-memory version behaves after a reload.

In this step, get ready to:
  • Connect the existing interface to an application script.
  • Render application cards with status controls and delete buttons.
  • Test the complete interaction flow through a deliberate browser reload.
Connect the interface to JavaScript

The browser needs a script reference before it can run the tracker logic. Placing that reference at the end of the page lets the existing interface load before JavaScript looks for its elements.

  • Return to index.html in Visual Studio Code.
  • Find these closing lines at the bottom of the file:
  </body>
</html>

Why use the bottom of the page?

The closing body tag marks the end of the visible page content. The script belongs immediately before it so every referenced form and dashboard element already exists.

  • Replace those closing lines with this script reference:
    <script src="app.js"></script>
  </body>
</html>

What does this change do?

The script element tells the browser to load app.js. Its position lets the script connect to the interface as soon as the page reaches this line.

The tracker needs one shared list of allowed statuses. It also needs references to the existing page elements and an array that holds applications during the current browser session.

  • Create app.js inside the open job-application-tracker folder using the file sidebar's new-file control.
  • Add the status values and interface references by pasting this code into app.js:
const STATUSES = ["Applied", "Interviewing", "Offer"];

const form = document.querySelector("#application-form");
const companyInput = document.querySelector("#company");
const roleInput = document.querySelector("#role");
const statusInput = document.querySelector("#status");
const formMessage = document.querySelector("#form-message");
const applicationList = document.querySelector("#application-list");
const emptyState = document.querySelector("#empty-state");
const totalCount = document.querySelector("#total-count");
const interviewingCount = document.querySelector("#interviewing-count");
const offerCount = document.querySelector("#offer-count");

let applications = [];

What does this code set up?

  • The STATUSES array keeps the three permitted stages in one place.
  • Each document.querySelector() reference connects JavaScript to an element that already exists in index.html.
  • The applications array starts empty and becomes the source for every card shown during this session.
  • Save index.html.
  • Save app.js.
  • Switch back to the browser from the previous step.
  • Refresh the tracker page.

You should see the existing form with three zero-valued statistics. The empty-state message should remain visible while the new array is empty.

Does the page look incomplete?

Check that app.js sits beside index.html inside job-application-tracker. Confirm that the script reference appears immediately before the closing body tag.

Help me check why my HTML page is not loading app.js.

Render and manage application cards

Rendering converts each application object into visible DOM elements. The first function builds the status control that updates an application's stage.

  • In app.js, add this function below let applications = [];:
function buildStatusSelect(application) {
  const label = document.createElement("label");
  label.className = "status-control";
  const labelText = document.createElement("span");
  labelText.textContent = "Status";
  const select = document.createElement("select");
  select.setAttribute("aria-label", `Status for ${application.company}`);
  STATUSES.forEach((status) => {
    const option = document.createElement("option");
    option.value = status;
    option.textContent = status;
    select.append(option);
  });
  select.value = application.status;
  select.addEventListener("change", () => {
    application.status = select.value;
    renderApplications();
  });
  label.append(labelText, select);
  return label;
}

How does the status control work?

  • The function creates one option for every value in STATUSES.
  • The selected option reflects the application's current status value.
  • The change listener updates the application object and requests a fresh render.
  • Save app.js.
  • Refresh the browser page.

You should still see the complete empty dashboard. This confirms the browser can load the new function without disrupting the interface.

Does the interface disappear?

Compare every brace and parenthesis in buildStatusSelect() with the code above. A missing closing character prevents the rest of the script from loading.

Help me find a syntax problem in buildStatusSelect().

Each application also needs a card that displays its company and role. The same card provides its status control and delete button.

  • Add this function below buildStatusSelect() in app.js:
function buildApplicationCard(application) {
  const card = document.createElement("article");
  card.className = "application-card";
  const cardCopy = document.createElement("div");
  const companyHeading = document.createElement("h3");
  const roleName = document.createElement("p");
  companyHeading.textContent = application.company;
  roleName.className = "role-name";
  roleName.textContent = application.role;
  cardCopy.append(companyHeading, roleName);
  const cardActions = document.createElement("div");
  cardActions.className = "card-actions";
  const deleteButton = document.createElement("button");
  deleteButton.className = "danger-button";
  deleteButton.type = "button";
  deleteButton.textContent = "Delete";
  deleteButton.setAttribute("aria-label", `Delete ${application.company} application`);
  deleteButton.addEventListener("click", () => {
    applications = applications.filter((item) => item.id !== application.id);
    renderApplications();
    formMessage.textContent = `${application.company} was removed.`;
  });
  cardActions.append(buildStatusSelect(application), deleteButton);
  card.append(cardCopy, cardActions);
  return card;
}

How does deletion work?

Each card combines the company's details with controls for changing or deleting the application. The delete listener uses filter() to keep every object whose id does not match the selected card.

The function renders the updated array and shows a removal message. The card disappears from the interface immediately.

  • Save app.js.
  • Refresh the browser page.

You should see the empty dashboard load with its form and statistics intact. The card builder is now ready for the first application object.

Does the page stop loading?

Check the long aria-label line for matching backticks. Confirm that the delete listener closes before the final append calls.

Help me debug buildApplicationCard().

The dashboard statistics come from the complete applications array. Counting from that array keeps each number aligned with the cards.

  • Add this statistics function below buildApplicationCard() in app.js:
function updateStats() {
  totalCount.textContent = applications.length;
  interviewingCount.textContent = applications.filter((application) => application.status === "Interviewing").length;
  offerCount.textContent = applications.filter((application) => application.status === "Offer").length;
}

How are the totals calculated?

The total comes directly from applications.length. The other counts use filter() to select applications with a matching status.

One render function now needs to rebuild the visible list and refresh those totals. It also controls whether the empty-state message is shown.

  • Add this rendering function below updateStats() in app.js:
function renderApplications() {
  applicationList.innerHTML = "";
  emptyState.hidden = applications.length > 0;
  applications.forEach((application) => applicationList.append(buildApplicationCard(application)));
  updateStats();
}

What happens during a render?

  • The function clears the existing card list so stale cards cannot remain.
  • The empty state stays visible only when applications has no items.
  • Every current application becomes a fresh card before updateStats() recalculates the dashboard.
  • Save app.js.
  • Refresh the browser page.

You should see the empty-state message and three zero-valued statistics. The complete rendering pipeline now loads with the page.

Are the starting values missing?

Confirm that applicationList and emptyState match the references at the top of app.js. Check that updateStats() appears before renderApplications().

Help me debug my rendering functions.

Submit applications and test memory

The final event listener turns each valid form submission into an application object. Every object receives a unique id plus the company, role, and selected status.

  • Add this submit listener below renderApplications() in app.js:
form.addEventListener("submit", (event) => {
  event.preventDefault();
  const company = companyInput.value.trim();
  const role = roleInput.value.trim();
  if (!company || !role) {
    formMessage.textContent = "Enter both a company and a role.";
    return;
  }
  applications.unshift({ id: `${Date.now()}-${Math.random().toString(16).slice(2)}`, company, role, status: statusInput.value });
  renderApplications();
  form.reset();
  statusInput.value = "Applied";
  companyInput.focus();
  formMessage.textContent = `${company} was added.`;
});

renderApplications();

What does the submit listener do?

  • The listener prevents the browser's normal form submission so the page stays in place.
  • The trimmed company and role values must both contain text before an object is created.
  • The new object is placed at the start of applications before the interface renders again.
  • The final renderApplications() call prepares the empty dashboard as soon as the script loads.
  • Save app.js.
  • Refresh the browser page.

✔️ Awesome, I've got everything!

Great. Double check that app.js is saved and the tracker still displays its empty dashboard.

ⓧ I'd like to double check the full code

Compare your complete app.js file with this version:

const STATUSES = ["Applied", "Interviewing", "Offer"];

const form = document.querySelector("#application-form");
const companyInput = document.querySelector("#company");
const roleInput = document.querySelector("#role");
const statusInput = document.querySelector("#status");
const formMessage = document.querySelector("#form-message");
const applicationList = document.querySelector("#application-list");
const emptyState = document.querySelector("#empty-state");
const totalCount = document.querySelector("#total-count");
const interviewingCount = document.querySelector("#interviewing-count");
const offerCount = document.querySelector("#offer-count");

let applications = [];

function buildStatusSelect(application) {
  const label = document.createElement("label");
  label.className = "status-control";
  const labelText = document.createElement("span");
  labelText.textContent = "Status";
  const select = document.createElement("select");
  select.setAttribute("aria-label", `Status for ${application.company}`);
  STATUSES.forEach((status) => {
    const option = document.createElement("option");
    option.value = status;
    option.textContent = status;
    select.append(option);
  });
  select.value = application.status;
  select.addEventListener("change", () => {
    application.status = select.value;
    renderApplications();
  });
  label.append(labelText, select);
  return label;
}

function buildApplicationCard(application) {
  const card = document.createElement("article");
  card.className = "application-card";
  const cardCopy = document.createElement("div");
  const companyHeading = document.createElement("h3");
  const roleName = document.createElement("p");
  companyHeading.textContent = application.company;
  roleName.className = "role-name";
  roleName.textContent = application.role;
  cardCopy.append(companyHeading, roleName);
  const cardActions = document.createElement("div");
  cardActions.className = "card-actions";
  const deleteButton = document.createElement("button");
  deleteButton.className = "danger-button";
  deleteButton.type = "button";
  deleteButton.textContent = "Delete";
  deleteButton.setAttribute("aria-label", `Delete ${application.company} application`);
  deleteButton.addEventListener("click", () => {
    applications = applications.filter((item) => item.id !== application.id);
    renderApplications();
    formMessage.textContent = `${application.company} was removed.`;
  });
  cardActions.append(buildStatusSelect(application), deleteButton);
  card.append(cardCopy, cardActions);
  return card;
}

function updateStats() {
  totalCount.textContent = applications.length;
  interviewingCount.textContent = applications.filter((application) => application.status === "Interviewing").length;
  offerCount.textContent = applications.filter((application) => application.status === "Offer").length;
}

function renderApplications() {
  applicationList.innerHTML = "";
  emptyState.hidden = applications.length > 0;
  applications.forEach((application) => applicationList.append(buildApplicationCard(application)));
  updateStats();
}

form.addEventListener("submit", (event) => {
  event.preventDefault();
  const company = companyInput.value.trim();
  const role = roleInput.value.trim();
  if (!company || !role) {
    formMessage.textContent = "Enter both a company and a role.";
    return;
  }
  applications.unshift({ id: `${Date.now()}-${Math.random().toString(16).slice(2)}`, company, role, status: statusInput.value });
  renderApplications();
  form.reset();
  statusInput.value = "Applied";
  companyInput.focus();
  formMessage.textContent = `${company} was added.`;
});

renderApplications();

Before you add anything, predict how the totals should change when two applications are added. Keep that prediction in mind as you test the form.

  • Enter Example Company in the Company field.
  • Enter Junior Web Developer in the Role field.
  • Select Applied in the Status field.
  • Click Add application.

You should see an Example Company card with its Junior Web Developer role. The total should change to 1.

  • Enter Northstar Studio in the Company field.
  • Enter UI Developer in the Role field.
  • Select Applied in the Status field.
  • Click Add application.

You should now see two cards. The total should show 2.

  • Change the Northstar Studio card's status to Interviewing.

The interviewing statistic should change to 1 while the total remains 2.

  • Click Delete on the Example Company card.

You should see only the Northstar Studio card. The dashboard should show a total of 1 and an interviewing count of 1.

Don't see the cards update?

Confirm that the script reference uses app.js with the same spelling as the file. Check that the submit listener appears above the final renderApplications(); line.

Help me debug my interactive application cards.

Before you refresh, do you think the remaining application will survive the reload? Make your prediction now.

  • Reload the browser page.

The Northstar Studio card disappears. All three statistics return to 0 because applications starts as a fresh empty array whenever the script loads.

Why did the application disappear?

The array lives in the page's temporary memory. Reloading the page starts the script again and runs let applications = [];.

This data loss is the intended result of the test. You have now exposed the exact limitation that persistent storage needs to solve.

Your tracker now adds, updates, deletes, validates, renders, and counts applications during a browser session. Next, you will record this working version with Git and publish it through GitHub Pages.

Publish the First Working Version

Your tracker now turns form input into live application cards. It updates status counts without a page reload.

The working app still lives only on your computer. An employer needs a public link that opens from any browser.

Git records a snapshot of the working project. GitHub stores that snapshot in a public repository.

GitHub Pages serves the project from a public HTTPS address. That stable website origin prepares the tracker for persistent browser storage.

In this step, get ready to:
  • Document the tracker in a README file and record the first Git commit.
  • Publish the project source in a public GitHub repository.
  • Deploy the tracker with GitHub Pages and verify its current reload behavior.
Create the README and first commit

The README introduces your project to anyone viewing its source. Its Markdown sections explain the tracker’s purpose and features.

  • Select the New File icon in Visual Studio Code’s Explorer sidebar.
  • Name the file README.md.
  • Add the project overview by pasting this exact Markdown into README.md:
# Job Application Tracker

A responsive browser app for recording job applications, updating their progress, and preserving the list between visits.

## Features

- Add a company, role, and application status
- Update an application from Applied to Interviewing or Offer
- Delete applications that are no longer active
- View live totals for all applications, interviews, and offers
- Preserve application data with browser storage

## Built with

- HTML
- CSS
- JavaScript
- GitHub Pages

## Live demo

Add your GitHub Pages URL here after publishing.

## Run locally

Open `index.html` to preview the interface. Use the deployed GitHub Pages site when testing persistent browser storage because storage behavior for local files can vary by browser.

## What I learned

This project demonstrates semantic page structure, responsive styling, DOM events, array rendering, persistent browser state, Git version control, and static deployment.

What Does This README Cover?

The feature list tells visitors what they can do with the finished tracker. The technology list shows how you built it.

The live demo section holds a placeholder for the public address. The learning section connects the finished app to practical front-end skills.

  • Save README.md.
  • Confirm that README.md appears beside index.html in the Explorer sidebar.

README Missing From the Explorer?

Check that the filename is exactly README.md. Confirm that Visual Studio Code created it inside the open job-application-tracker folder.

Help me find a README file that is missing from the Visual Studio Code Explorer.

✔️ Awesome, I've got everything!

Your saved README.md now documents the tracker for repository visitors.

ⓧ I'd like to double check the full code

Compare your complete README.md with this reference file.

# Job Application Tracker

A responsive browser app for recording job applications, updating their progress, and preserving the list between visits.

## Features

- Add a company, role, and application status
- Update an application from Applied to Interviewing or Offer
- Delete applications that are no longer active
- View live totals for all applications, interviews, and offers
- Preserve application data with browser storage

## Built with

- HTML
- CSS
- JavaScript
- GitHub Pages

## Live demo

Add your GitHub Pages URL here after publishing.

## Run locally

Open `index.html` to preview the interface. Use the deployed GitHub Pages site when testing persistent browser storage because storage behavior for local files can vary by browser.

## What I learned

This project demonstrates semantic page structure, responsive styling, DOM events, array rendering, persistent browser state, Git version control, and static deployment.

How to Use This Reference

Check each heading and bullet against your saved file. Matching this reference gives the public repository a complete project overview.

  • Select Terminal from the Visual Studio Code top menu.
  • Select New Terminal.

The terminal prompt should point to the open job-application-tracker folder.

  • Initialize the folder as a Git repository with a main branch by running:
git init -b main

What Does This Command Do?

The command creates Git’s local repository data inside job-application-tracker. It names the initial branch main.

You should see confirmation that an empty Git repository was initialized on the main branch.

  • Stage the project files and create the initial commit by running:
git add .
git commit -m "Build job application tracker"

What Do These Commands Record?

  • The first command stages every project file in the current folder.
  • The second command records those staged files with the message Build job application tracker.

That is your first project snapshot recorded. Git now has a local history containing the working tracker and its README.

Commit Not Completing?

Confirm that the terminal prompt points to job-application-tracker. Your Git name and email from the setup step must also remain configured.

Help me troubleshoot my first Git commit.

Create the public repository and push

A remote repository gives your local commit an online destination. The public repository also makes your source code available beside the live demo.

  • Return to GitHub in the browser where you remain signed in.
  • Select the plus menu in the top navigation.
  • Select New repository.
  • Enter job-application-tracker in the Repository name field.

Why Use an Empty Public Repository?

An empty repository avoids conflicts when you push the commit that already exists on your computer. GitHub Pages also requires a public repository for this project on GitHub Free.

A public repository makes every committed file visible. This project contains no credentials or private data, so its source is safe to publish.

  • Select Public for the repository visibility.
  • Leave the README initialization option unselected.
  • Keep the gitignore option set to None.
  • Keep the license option set to None.
  • Select Create repository.
  • Copy the HTTPS repository URL shown on the new repository page.
  • Record the copied address here: your repository HTTPS URL.
  • Add the repository as origin by replacing REMOTE-URL with your repository HTTPS URL in this command:
git remote add origin REMOTE-URL

What Does Origin Mean?

The name origin is the local label for your GitHub repository. Future push commands use that short label to find the online destination.

  • Verify the saved remote address by running:
git remote -v

What Does This Check Show?

This command displays the addresses assigned to origin. It confirms that Git knows where to send your commit.

You should see two lines for origin that use your repository address.

Your first HTTPS push can open a browser sign-in. That prompt is GitHub confirming who may publish this code.

  • Push the main branch to GitHub by running:
git push -u origin main

What Does the First Push Do?

The command uploads your initial commit to the public repository. It also connects the local main branch to the remote branch for future pushes.

When the push completes, Git reports that the local branch now tracks the remote branch.

  • Return to the new repository in your browser.
  • Refresh the repository page.

You should see index.html, styles.css, app.js, and README.md in the public repository.

Push Not Reaching GitHub?

  • Complete the browser sign-in if Git Credential Manager requests authentication.
  • Check that the repository address shown by the remote verification belongs to job-application-tracker.
  • Confirm that the GitHub repository is still empty before repeating the first push.

Help me fix a Git push that is not reaching my repository.

Deploy the tracker with GitHub Pages

Branch-based publishing turns the files on main into a website. The repository’s top-level index.html becomes the page GitHub Pages serves.

  • Return to the job-application-tracker repository page.
  • Select Settings from the repository navigation.
  • Select Pages from the settings sidebar.
  • Set Source to Deploy from a branch under Build and deployment.
  • Choose main as the publishing branch.
  • Choose /(root) as the publishing folder.
  • Select Save.

Publishing can take up to 10 minutes. The wait is GitHub Pages building the public site from your repository.

Before you open the deployed tracker, do you expect a newly added application to survive a page reload?

  • Refresh the Pages settings area until Visit site appears.
  • Select Visit site.
  • Copy the public HTTPS address from the browser address bar.
  • Record the copied address here: your GitHub Pages URL.

You should see the same two-column Job Application Tracker from your local preview. The form and zero-valued statistics should be visible.

  • Enter Example Company in the Company field.
  • Enter Junior Web Developer in the Role field.
  • Keep Applied selected in the Status field.
  • Select Add application.

You should see the application card on the live site. The total count should change to 1.

  • Reload the live page with your browser’s refresh button.

The card disappears after the reload. The dashboard returns to zero because the deployed app still keeps applications only in memory.

What Did This Deployment Prove?

The public URL runs the same tracker code stored in your GitHub repository. Anyone with the address can now open the project.

The reload result confirms the remaining limitation. The site has a stable origin, but the array still starts empty whenever the page loads.

Your first working version is now public through GitHub Pages. Next up, you’ll make its applications survive every reload.

Make Applications Survive a Reload

Your tracker is live on GitHub Pages. Its controls already turn form entries into application cards with live statistics.

A reload still wipes every application from the page. In this step, browser storage gives the live tracker persistent state across visits.

In this step, get ready to:
  • Load saved applications when the tracker starts.
  • Save the complete application list after every state change.
  • Publish the persistence update to the live site.
Store and restore applications

The browser stores text values through localStorage. Your tracker converts its application array to JSON before saving it.

The storage key gives the saved array a stable name. The loading function then restores that array when the page starts.

  • Select app.js in the Visual Studio Code Explorer sidebar.
  • Find const STATUSES = ["Applied", "Interviewing", "Offer"]; at the top of the file.
  • Add the storage key directly below that line by pasting:
const STORAGE_KEY = "jobApplications";

Why This Key Matters

The STORAGE_KEY constant keeps the storage name in one place. Both storage functions use the same jobApplications entry.

  • Find function buildStatusSelect(application) {.
  • Insert the loading function directly above it by pasting:
function loadApplications() {
  try {
    const savedApplications = localStorage.getItem(STORAGE_KEY);
    if (!savedApplications) return [];
    const parsedApplications = JSON.parse(savedApplications);
    return Array.isArray(parsedApplications) ? parsedApplications : [];
  } catch (error) {
    console.error("Could not load saved applications.", error);
    return [];
  }
}

What Does This Function Do?

  • The storage lookup retrieves the text saved under STORAGE_KEY.
  • The parser turns the saved text back into a JavaScript array.
  • The array check protects the tracker from unexpected stored data.
  • The error handler returns an empty array when loading fails.
  • Keep your cursor above function buildStatusSelect(application) {.
  • Insert the saving function below loadApplications() by pasting:
function saveApplications() {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(applications));
  } catch (error) {
    console.error("Could not save applications.", error);
  }
}

What Does This Function Do?

The saveApplications() function converts the complete array to text. It writes that text under the same storage key used by the loading function.

The error handler keeps a storage failure from stopping the rest of the tracker.

  • Find let applications = []; near the top of app.js.
  • Replace that line with:
let applications = loadApplications();

How Startup Changes

The application array now starts with the result from loadApplications(). A first-time visitor still receives an empty array.

  • Save app.js.
Save every state change

Loading handles the start of a visit. Persistence also requires a save after each action that changes the applications array.

  • Find the change listener inside buildStatusSelect().
  • Replace that listener with this version:
select.addEventListener("change", () => {
    application.status = select.value;
    saveApplications();
    renderApplications();
  });

How Status Updates Persist

The selected status updates the application object first. The save then captures that updated object before the interface renders again.

  • Find the delete button listener inside buildApplicationCard().
  • Replace that listener with this version:
deleteButton.addEventListener("click", () => {
    applications = applications.filter((item) => item.id !== application.id);
    saveApplications();
    renderApplications();
    formMessage.textContent = `${application.company} was removed.`;
  });

How Deletions Persist

The filter creates an array without the deleted application. Saving that smaller array prevents the removed card from returning after a reload.

  • Find the object creation inside the form submit listener.
  • Replace the object creation plus its following interface updates with:
applications.unshift({
    id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
    company,
    role,
    status: statusInput.value,
  });

  saveApplications();
  renderApplications();
  form.reset();
  statusInput.value = "Applied";
  companyInput.focus();
  formMessage.textContent = `${company} was added.`;

How New Applications Persist

The new object enters the start of the array. The immediate save records the new application before the form resets.

  • Save app.js.
  • Compare your completed file with the reference below.

✔️ Awesome, I've got everything!

Great. Confirm that app.js is saved with loading plus saving for create, update, and delete actions.

ⓧ I'd like to double check the full code

Compare your complete app.js file with this reference:

const STATUSES = ["Applied", "Interviewing", "Offer"];
const STORAGE_KEY = "jobApplications";

const form = document.querySelector("#application-form");
const companyInput = document.querySelector("#company");
const roleInput = document.querySelector("#role");
const statusInput = document.querySelector("#status");
const formMessage = document.querySelector("#form-message");
const applicationList = document.querySelector("#application-list");
const emptyState = document.querySelector("#empty-state");
const totalCount = document.querySelector("#total-count");
const interviewingCount = document.querySelector("#interviewing-count");
const offerCount = document.querySelector("#offer-count");

let applications = loadApplications();

function loadApplications() {
  try {
    const savedApplications = localStorage.getItem(STORAGE_KEY);
    if (!savedApplications) return [];
    const parsedApplications = JSON.parse(savedApplications);
    return Array.isArray(parsedApplications) ? parsedApplications : [];
  } catch (error) {
    console.error("Could not load saved applications.", error);
    return [];
  }
}

function saveApplications() {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(applications));
  } catch (error) {
    console.error("Could not save applications.", error);
  }
}

function buildStatusSelect(application) {
  const label = document.createElement("label");
  label.className = "status-control";
  const labelText = document.createElement("span");
  labelText.textContent = "Status";
  const select = document.createElement("select");
  select.setAttribute("aria-label", `Status for ${application.company}`);
  STATUSES.forEach((status) => {
    const option = document.createElement("option");
    option.value = status;
    option.textContent = status;
    select.append(option);
  });
  select.value = application.status;
  select.addEventListener("change", () => {
    application.status = select.value;
    saveApplications();
    renderApplications();
  });
  label.append(labelText, select);
  return label;
}

function buildApplicationCard(application) {
  const card = document.createElement("article");
  card.className = "application-card";
  const cardCopy = document.createElement("div");
  const companyHeading = document.createElement("h3");
  const roleName = document.createElement("p");
  companyHeading.textContent = application.company;
  roleName.className = "role-name";
  roleName.textContent = application.role;
  cardCopy.append(companyHeading, roleName);
  const cardActions = document.createElement("div");
  cardActions.className = "card-actions";
  const deleteButton = document.createElement("button");
  deleteButton.className = "danger-button";
  deleteButton.type = "button";
  deleteButton.textContent = "Delete";
  deleteButton.setAttribute("aria-label", `Delete ${application.company} application`);
  deleteButton.addEventListener("click", () => {
    applications = applications.filter((item) => item.id !== application.id);
    saveApplications();
    renderApplications();
    formMessage.textContent = `${application.company} was removed.`;
  });
  cardActions.append(buildStatusSelect(application), deleteButton);
  card.append(cardCopy, cardActions);
  return card;
}

function updateStats() {
  totalCount.textContent = applications.length;
  interviewingCount.textContent = applications.filter((application) => application.status === "Interviewing").length;
  offerCount.textContent = applications.filter((application) => application.status === "Offer").length;
}

function renderApplications() {
  applicationList.innerHTML = "";
  emptyState.hidden = applications.length > 0;
  applications.forEach((application) => applicationList.append(buildApplicationCard(application)));
  updateStats();
}

form.addEventListener("submit", (event) => {
  event.preventDefault();
  const company = companyInput.value.trim();
  const role = roleInput.value.trim();
  if (!company || !role) {
    formMessage.textContent = "Enter both a company and a role.";
    return;
  }
  applications.unshift({ id: `${Date.now()}-${Math.random().toString(16).slice(2)}`, company, role, status: statusInput.value });
  saveApplications();
  renderApplications();
  form.reset();
  statusInput.value = "Applied";
  companyInput.focus();
  formMessage.textContent = `${company} was added.`;
});

renderApplications();

What Should Match?

Check the storage key plus both storage functions first. Then confirm that create, update, and delete actions each call saveApplications().

Seeing a JavaScript Error?

Check that STORAGE_KEY is declared before loadApplications() runs. Confirm that each function closes with the same braces and parentheses shown in the reference.

Ask for help with the exact browser error and your current app.js structure.

Publish and verify persistence

Your local code now saves every mutation. A new Git commit records the persistence milestone before the update reaches the public site.

  • Return to the Visual Studio Code terminal for job-application-tracker.
  • Stage, commit, and push the persistence update by running:
git add .
git commit -m "Persist application data"
git push origin main

What Do These Commands Do?

  • The first command stages the updated app.js file.
  • The second command records the change as Persist application data.
  • The final command sends the new commit to origin/main.

Push Not Completing?

Complete the browser sign-in if Git Credential Manager requests authentication. Return to the terminal after the browser confirms access.

Get help diagnosing the push output before changing your remote.

Publishing can take up to 10 minutes after the push. The wait means GitHub Pages is building the new commit.

  • Return to the job-application-tracker repository on GitHub.
  • Select Settings in the repository navigation.
  • Select Pages in the sidebar.
  • Select Visit site when the latest deployment is ready.
  • Enter Example Company in the Company field.
  • Enter Junior Web Developer in the Role field.
  • Select Applied in the Status field.
  • Select Add application.
  • Change the application card status to Interviewing.

Before you reload, do you expect the application card plus its updated statistic to return?

  • Reload the live GitHub Pages site.

You should see the application return with Interviewing selected. The total and interviewing statistics should both show 1.

  • Select Delete on the saved application card.

Before you reload again, do you expect the deleted application to stay gone?

  • Reload the live GitHub Pages site again.

You should see the empty-state message. All three statistics should show 0.

That closes the tracker’s biggest gap. Your public app now preserves creation, status updates, statistics, and deletions across reloads.

Secret mission

Filter Applications by Status

A growing application list becomes harder to scan. Add a status filter that shows one stage at a time while preserving the complete saved list and its overall statistics.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep the tracker online, pause the live site, or delete the project entirely. Your public GitHub repository and GitHub Pages deployment cost $0 on the selected GitHub Free path.

Resources you used:

  • Public job-application-tracker repository on GitHub.
  • Public GitHub Pages deployment.
  • Local job-application-tracker folder on your Windows device.
  • Application data saved in browser storage under jobApplications for the public site's origin.

Keep everything running

No action is needed. Choose this if you want the source code and live tracker to remain available.

  • Keep the public job-application-tracker repository available for reviewing the source code.
  • Keep the GitHub Pages deployment published for sharing the working tracker.
  • Keep the local job-application-tracker folder available for future changes.
  • Keep the saved application data available in your current browser profile.

Pause - I'll come back to this later

Unpublish the live site to pause public access. Your repository contents and local folder stay available for later.

  • Return to the job-application-tracker repository on GitHub.
  • Select Settings in the repository navigation.
  • Select Pages in the settings sidebar.
  • Open the GitHub Pages deployment menu.
  • Select Unpublish site.

That gives you a clean pause. The public site is offline while your source code remains available.

Delete - I don't want to use this again

Remove every project resource if you no longer need the tracker.

Deleting the repository is destructive. GitHub may allow some deleted repositories to be restored within 90 days, so treat this action as permanent.

  • Return to the deployed GitHub Pages site to remove its saved application data.
  • Select All statuses from the status filter.
  • Select Delete on each application card until none remain.
  • Reload the page.

You'll see all three statistics at zero. You'll also see No applications yet. Add your first role above.

  • Return to the job-application-tracker repository on GitHub.
  • Select Settings in the repository navigation.
  • Select Pages in the settings sidebar.
  • Open the GitHub Pages deployment menu.
  • Select Unpublish site.

The public deployment is now removed. The site URL no longer serves the tracker.

  • Select General in the settings sidebar.
  • Scroll to Danger Zone.
  • Select Delete this repository.
  • Acknowledge each warning from GitHub.
  • Type job-application-tracker when GitHub asks for the repository name.
  • Confirm the repository deletion.

Your public repository is gone. Its GitHub Pages deployment can no longer return.

  • Press the Windows key to open Windows search.
  • Type File Explorer.
  • Press Enter.
  • Search your Windows device for job-application-tracker.
  • Right-click the matching folder.
  • Select Delete.
  • Empty the Recycle Bin to remove the folder completely.

Cleanup complete. The tracker no longer has a local folder, a public repository, a live deployment, or saved application entries.

Nice Work!

Nice Work!

You did it! You built a responsive Job Application Tracker that preserves job-search progress across reloads on a public GitHub Pages site.

You've learned how to:

  • Build a responsive interface with semantic HTML. Use responsive CSS to shift the layout from two columns to one on narrow screens.
  • Turn validated form input into application cards with DOM events and array rendering. Status changes appear immediately. Deletions update the list immediately. Dashboard statistics update at the same time.
  • Use browser storage to preserve applications across reloads. Record project changes with Git. Push readable source code to GitHub. Deploy the working app through GitHub Pages.
  • Complete the Secret Mission by adding a non-destructive status filter based on derived state. The overall statistics remain unchanged. The complete saved application list remains intact.

Ready to quiz yourself?