Build a Job Application Tracker
Build and publish a persistent job application tracker with JavaScript.
Introduction
30 Second Summary
Job hunting can scatter opportunities across emails, notes, and open tabs. After a few applications, remembering each status becomes difficult.
In this project, you will build a Job Application Tracker with HTML, CSS, and JavaScript. You will publish it with GitHub Pages so employers can open the app from a public link.
What You'll Build
Open your public tracker to see every saved job application return after a refresh.
By the end of this project, you'll have:
- A job application form where you enter a company, role, and status.
- Readable application cards that appear as soon as you submit the form. The total updates with each new entry.
- A public tracker that employers can open from a shared link. Saved applications return when the page reloads.
- Secret Mission: Add a status filter that shows Applied, Interview, Offer, or Rejected applications.
Are there any prerequisites?
You need a Mac, an email address, and an internet connection. No coding experience is required.
Before We Start
Before the hands-on work begins, lock in your goal: you are building a Job Application Tracker that demonstrates your software-development skills through a live interactive project.
Set Up Your Development Workspace
Your Job Application Tracker needs a reliable home for its files. It also needs an online identity that you can use when you publish it.
In this step, you'll install Visual Studio Code. You'll create a verified GitHub account.
Finally, you'll create your first HTML file. Safari will confirm that your workspace can display a local web page.
In this step, get ready to:
- Install Visual Studio Code on your Mac.
- Prepare a verified free GitHub account.
- Create the project folder and confirm its first page in Safari.
Install Visual Studio Code
A code editor gives you one place to create project files and update their contents. Visual Studio Code will be your editor throughout this project.
- Open the Visual Studio Code macOS setup page in Safari.
- Use the macOS download link to download Visual Studio Code.
- Open the downloaded .dmg file from Finder.
- Drag Visual Studio Code.app into the Applications folder.
- Press Cmd+Space to open the macOS search bar.
- Type Visual Studio Code and press Enter to launch it.
You'll see the Visual Studio Code window open. That's your first setup win. Your editor is ready for the project files.
Visual Studio Code Not Opening?
- Check that the Visual Studio Code download finished before opening the .dmg file.
- Confirm that Visual Studio Code.app appears in your Applications folder.
- Still stuck? Help me troubleshoot Visual Studio Code installation on my Mac.
Create a free GitHub account
GitHub will host your project files when you publish the tracker. A verified email address also unlocks basic account tasks such as creating a repository.
- Return to Safari.
- Open the GitHub sign-up page.
- Follow the on-screen prompts to create your free personal account.
- Open the verification message in your email inbox.
- Use the message's verification link to verify your email address.
- Return to GitHub in Safari.
- Open your profile from the signed-in account menu.
You'll see your GitHub profile while signed in. Your verified account is now ready to host the tracker later in the project.
Verification Message Missing?
- Check the spam or junk folder for the verification message.
- Confirm that the email address entered during sign-up has no typing mistakes.
- Still stuck? Help me complete my GitHub email verification.
Create the first project page
Your project folder keeps every tracker file together. The first page contains one heading that proves Visual Studio Code can save a file that Safari can display.
- Switch back to Visual Studio Code.
- Use Visual Studio Code's file menu to begin opening a folder.
- Select the Documents folder in the folder picker.
- Use the new-folder control to create job-application-tracker inside Documents.
- Select the job-application-tracker folder.
- Confirm the selection so Visual Studio Code opens the folder.
- Use the new-file control in the Explorer sidebar to create index.html.
- Add the setup heading by entering this code:
<h1>Setup works</h1>
What Does This Code Do?
- The <h1> element marks the page's main heading.
- The text inside the element gives Safari the words to display.
- Save index.html from Visual Studio Code's file menu.
- Switch to Finder.
- Open your Documents folder.
- Open the job-application-tracker folder.
Before you open the file, what heading do you expect Safari to show?
- Open index.html by double-clicking it in Finder.
You'll see Setup works in Safari. Your editor, project folder, HTML file, and browser now work together.
Setup Heading Not Showing?
- Check that index.html contains the complete heading shown above.
- Save index.html again if Visual Studio Code still shows unsaved changes.
- Drag index.html from Finder into the Safari window if another browser opens the file.
- Still stuck? Help me open my local HTML file in Safari.
✔️ Awesome, I've got everything!
Great work. Double check that index.html is saved inside Documents/job-application-tracker.
ⓧ I'd like to double check the full code
The complete index.html file should match this exactly.
<h1>Setup works</h1>
Your development workspace is ready. Next, you'll turn this single heading into the visible Job Application Tracker interface.
Build the Tracker Interface
Your setup page now opens in Safari. A single heading cannot show what the finished tracker does.
This step uses HTML to give the tracker meaningful structure. CSS then turns that structure into a responsive interface inside Visual Studio Code.
In this step, get ready to:
- Build the static tracker structure.
- Style the form and application area.
- Verify the layout at desktop and narrow widths.
Build the page structure
Semantic elements divide the page into a header and two focused sections. This structure gives every form control a clear purpose before interactivity is added.
- In index.html, select the existing setup heading.
- Replace the selected heading with this page shell:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Job Application Tracker</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<main>
<header>
<p class="eyebrow">Portfolio Project</p>
<h1>Job Application Tracker</h1>
<p>Keep every opportunity and its current status in one place.</p>
</header>
</main>
</body>
</html>
What Does This Structure Do?
- The document metadata gives the page a title and prepares it for narrow screens.
- The stylesheet link connects this structure to the visual rules you create later.
- The header introduces the tracker with a project label and a clear purpose.
- Save index.html in Visual Studio Code.
- Switch back to the local page in Safari.
- Reload the page.
You should see the Portfolio Project label above the Job Application Tracker heading. The introductory sentence appears below it.
Still Seeing the Setup Heading?
- Confirm that the old setup heading is no longer present in index.html.
- Confirm that you saved the file before reloading Safari.
- Still stuck? Help me find why Safari still shows my old setup heading.
The next section collects the company name and role. Its status menu gives each application a stage in the hiring process.
- In index.html, place your cursor below </header>.
- Add the application form with this code:
<section aria-labelledby="add-application-heading">
<h2 id="add-application-heading">Add an application</h2>
<form id="application-form">
<label for="company">Company</label>
<input id="company" name="company" type="text" required />
<label for="role">Role</label>
<input id="role" name="role" type="text" required />
<label for="status">Status</label>
<select id="status" name="status" required>
<option value="Applied">Applied</option>
<option value="Interview">Interview</option>
<option value="Offer">Offer</option>
<option value="Rejected">Rejected</option>
</select>
<button type="submit">Add application</button>
</form>
</section>
How Is the Form Organized?
- Each label connects to its matching control through the for and id values.
- The required text inputs collect the company and role.
- The select menu limits the status to four consistent values.
- The submit button gives the form a clear completion action.
- Save index.html.
- Switch back to Safari.
- Reload the local page.
You should see Company and Role text fields. You should also see a Status menu plus an Add application button.
Form Controls Missing?
- Check that the new section sits inside <main>.
- Check that every opening form element has the matching closing element shown above.
- Still stuck? Help me fix the missing controls in my tracker form.
The application area provides a visible count and a dedicated container for future cards. The next step uses these identifiers to render real data.
- In index.html, place your cursor below the form section.
- Add the application summary with this code:
<section aria-labelledby="applications-heading">
<div class="summary">
<h2 id="applications-heading">Applications</h2>
<p><span id="application-count">0</span> tracked</p>
</div>
<div id="applications-list" aria-live="polite"></div>
</section>
What Does the Application Area Hold?
- The count starts at 0 because no applications have been added.
- The applications-list container provides a destination for the cards you render later.
- The live-region setting prepares the list to announce future updates.
- Save index.html.
- Switch back to Safari.
- Reload the local page.
You should see an Applications heading with a count of 0 tracked. The empty area below it is ready to hold application cards.
Applications Section Missing?
- Confirm that the application section appears before </main>.
- Check that the summary container closes before the application list begins.
- Still stuck? Help me repair my Applications section.
✔️ Awesome, I've got everything!
Your index.html now contains the complete static tracker structure.
ⓧ I'd like to double check the full code
- Compare your saved index.html with this complete reference:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Job Application Tracker</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<main>
<header>
<p class="eyebrow">Portfolio Project</p>
<h1>Job Application Tracker</h1>
<p>Keep every opportunity and its current status in one place.</p>
</header>
<section aria-labelledby="add-application-heading">
<h2 id="add-application-heading">Add an application</h2>
<form id="application-form">
<label for="company">Company</label>
<input id="company" name="company" type="text" required />
<label for="role">Role</label>
<input id="role" name="role" type="text" required />
<label for="status">Status</label>
<select id="status" name="status" required>
<option value="Applied">Applied</option>
<option value="Interview">Interview</option>
<option value="Offer">Offer</option>
<option value="Rejected">Rejected</option>
</select>
<button type="submit">Add application</button>
</form>
</section>
<section aria-labelledby="applications-heading">
<div class="summary">
<h2 id="applications-heading">Applications</h2>
<p><span id="application-count">0</span> tracked</p>
</div>
<div id="applications-list" aria-live="polite"></div>
</section>
</main>
</body>
</html>
Style the responsive interface
The structure communicates what each control does. Styling now creates visual hierarchy and keeps the tracker usable when the browser window becomes narrow.
- In the Visual Studio Code file sidebar, select the file creation icon beside job-application-tracker.
- Enter styles.css as the file name.
- Add the page foundation with this code:
:root {
color: #172033;
background: #eef3f8;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
}
main {
width: min(900px, calc(100% - 2rem));
margin: 0 auto;
padding: 3rem 0;
}
header {
margin-bottom: 2rem;
}
How Does the Page Foundation Work?
- The root styles establish the page colors and font family.
- The universal box model keeps element widths predictable.
- The main container stays centered with space along both sides.
- The header margin separates the introduction from the form.
- Save styles.css.
- Switch back to Safari.
- Reload the local page.
You should see a pale blue background. The tracker content should sit inside a centered column.
Page Still Has a White Background?
- Confirm that the file is named styles.css.
- Confirm that styles.css is beside index.html in the same folder.
- Still stuck? Help me connect my tracker page to styles.css.
- Add the heading and panel styles below your existing CSS:
h1,
h2,
p {
margin-top: 0;
}
h1 {
margin-bottom: 0.5rem;
font-size: clamp(2rem, 6vw, 3.5rem);
}
.eyebrow {
margin-bottom: 0.5rem;
color: #3157d5;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
section {
margin-bottom: 1.5rem;
padding: 1.5rem;
border: 1px solid #d6deea;
border-radius: 1rem;
background: #ffffff;
box-shadow: 0 12px 35px rgba(23, 32, 51, 0.08);
}
How Does the Visual Hierarchy Work?
- The main heading scales with the available browser width.
- The eyebrow uses color and uppercase letters to identify the page as a portfolio project.
- Each section becomes a white panel with spacing and a soft shadow.
- Save styles.css.
- Switch back to Safari.
- Reload the local page.
You should see the form and Applications area inside separate white panels. The Portfolio Project label should appear in blue uppercase text.
Panels Still Look Like Plain Text?
- Check that the section selector includes both braces.
- Check that the new rules appear below the earlier header styles.
- Still stuck? Help me find why my tracker sections are not styled as panels.
- Add the form layout and control foundation below the panel styles:
form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem 1rem;
}
label {
align-self: end;
font-weight: 700;
}
input,
select,
button {
width: 100%;
min-height: 2.75rem;
border-radius: 0.65rem;
font: inherit;
}
input,
select {
border: 1px solid #b9c4d4;
padding: 0.7rem 0.8rem;
background: #ffffff;
}
How Is the Form Laid Out?
- The form grid creates two equal columns for the desktop layout.
- The gap keeps labels and controls visually separated.
- The shared sizing rules give every control a consistent width and height.
- The input styles add internal spacing plus a visible border.
- Save styles.css.
- Switch back to Safari.
- Reload the local page.
You should see the form arranged in two columns. The text fields and status menu should have matching dimensions.
Controls Not Lining Up?
- Confirm that the form rule uses two 1fr columns.
- Check that the shared rule includes input, select, and button.
- Still stuck? Help me fix the alignment of my tracker form controls.
- Add the button and summary styles below the control rules:
button {
grid-column: 1 / -1;
margin-top: 0.5rem;
border: 0;
padding: 0.75rem 1rem;
color: #ffffff;
background: #3157d5;
font-weight: 700;
cursor: pointer;
}
button:hover {
background: #2444ad;
}
.summary {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
}
#applications-list {
display: grid;
gap: 0.75rem;
}
What Do These Styles Emphasize?
- The button spans the full grid so the form ends with one clear action.
- The hover color gives the button visual feedback.
- The summary places the heading opposite the tracked count.
- The application list prepares consistent spacing between future cards.
- Save styles.css.
- Switch back to Safari.
- Reload the local page.
You should see a blue Add application button across the full form width. The Applications heading should sit opposite the count.
Button Not Spanning the Form?
- Confirm that the button rule contains grid-column: 1 / -1.
- Check that the button rule closes before the hover rule begins.
- Still stuck? Help me fix the tracker button and summary layout.
Application cards arrive in the next step. These rules define their appearance now so every new card immediately fits the interface.
- Add the application-card styles below the list rule:
#applications-list article {
padding: 1rem;
border-left: 0.35rem solid #3157d5;
border-radius: 0.6rem;
background: #f5f7fb;
}
#applications-list h3 {
margin: 0 0 0.35rem;
}
#applications-list p {
margin-bottom: 0.25rem;
}
How Will Application Cards Look?
- Each future card gets a pale background and a blue accent border.
- Rounded corners keep the cards consistent with the surrounding panels.
- Compact heading and paragraph margins keep each card readable.
- Save styles.css.
- Confirm that the saved file still ends with the application-list paragraph rule.
The card styles are now ready for the application data you add in the next step.
Card Rules Showing an Error?
- Confirm that every application-list selector begins with #applications-list.
- Check that each selector has its own complete pair of braces.
- Still stuck? Help me check my future application-card styles.
A responsive layout adapts the same interface to a smaller viewing area. The form changes from two columns to one when the available width reaches 620px or less.
- Add the narrow-screen rules at the bottom of styles.css.
@media (max-width: 620px) {
main {
padding: 1.5rem 0;
}
form {
grid-template-columns: 1fr;
}
.summary {
align-items: flex-start;
flex-direction: column;
}
}
How Does the Narrow Layout Adapt?
- The page uses less vertical padding when space becomes limited.
- The form switches to one column so every control retains enough width.
- The summary stacks its heading above the tracked count.
- Save styles.css.
- Switch back to Safari.
- Reload the local page.
- Make the Safari window about as wide as a phone screen.
You should see the form controls stack into one column. The tracked count should move below the Applications heading.
Layout Still Has Two Columns?
- Make the Safari window narrower until the media query can apply.
- Confirm that the media query sits outside every earlier CSS rule.
- Still stuck? Help me debug the narrow tracker layout.
✔️ Awesome, I've got everything!
Your styles.css now contains the complete responsive tracker styling.
ⓧ I'd like to double check the full code
- Compare your saved styles.css with this complete reference:
:root {
color: #172033;
background: #eef3f8;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
}
main {
width: min(900px, calc(100% - 2rem));
margin: 0 auto;
padding: 3rem 0;
}
header {
margin-bottom: 2rem;
}
h1,
h2,
p {
margin-top: 0;
}
h1 {
margin-bottom: 0.5rem;
font-size: clamp(2rem, 6vw, 3.5rem);
}
.eyebrow {
margin-bottom: 0.5rem;
color: #3157d5;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
section {
margin-bottom: 1.5rem;
padding: 1.5rem;
border: 1px solid #d6deea;
border-radius: 1rem;
background: #ffffff;
box-shadow: 0 12px 35px rgba(23, 32, 51, 0.08);
}
form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem 1rem;
}
label {
align-self: end;
font-weight: 700;
}
input,
select,
button {
width: 100%;
min-height: 2.75rem;
border-radius: 0.65rem;
font: inherit;
}
input,
select {
border: 1px solid #b9c4d4;
padding: 0.7rem 0.8rem;
background: #ffffff;
}
button {
grid-column: 1 / -1;
margin-top: 0.5rem;
border: 0;
padding: 0.75rem 1rem;
color: #ffffff;
background: #3157d5;
font-weight: 700;
cursor: pointer;
}
button:hover {
background: #2444ad;
}
.summary {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
}
#applications-list {
display: grid;
gap: 0.75rem;
}
#applications-list article {
padding: 1rem;
border-left: 0.35rem solid #3157d5;
border-radius: 0.6rem;
background: #f5f7fb;
}
#applications-list h3 {
margin: 0 0 0.35rem;
}
#applications-list p {
margin-bottom: 0.25rem;
}
@media (max-width: 620px) {
main {
padding: 1.5rem 0;
}
form {
grid-template-columns: 1fr;
}
.summary {
align-items: flex-start;
flex-direction: column;
}
}
Verify the finished static interface
Before you reload, which controls do you expect to see inside the finished tracker?
- Return to the local index.html page in Safari.
- Reload the page.
- Make the Safari window wide enough for a desktop layout.
You should see the styled Job Application Tracker with Company, Role, and Status controls. You should also see the Add application button plus an empty Applications section showing 0 tracked.
Before you resize the window, which parts of the interface do you expect to stack vertically?
- Make the Safari window about as wide as a phone screen.
You should see the form switch to one column. The Applications heading should appear above the tracked count.
That is the visual foundation complete. Your tracker now communicates the full product idea at both desktop and narrow widths.
Your static tracker is ready for behavior. Next up, you will make the form create application cards and update the count.
Add Interactive Application Tracking
Your styled tracker now communicates the product idea. However, the form cannot record an application or update the page yet.
In this step, JavaScript will connect the form to the DOM so each submission becomes a visible card. The entries will live in an array while you test what a browser refresh does to them.
In this step, get ready to:
- Connect the tracker interface to JavaScript.
- Render application objects as cards with a live count.
- Submit an application and test what happens after a refresh.
Connect the JavaScript file
The form elements already have identifiers such as application-form and company. JavaScript can use those identifiers to find the matching elements on the page.
- In the left sidebar of Visual Studio Code, select the file-creation icon beside job-application-tracker.
- Name the new file script.js.
- Add references to the form controls and application area by pasting this code:
const form = document.querySelector("#application-form");
const companyInput = document.querySelector("#company");
const roleInput = document.querySelector("#role");
const statusInput = document.querySelector("#status");
const applicationCount = document.querySelector("#application-count");
const applicationsList = document.querySelector("#applications-list");
let applications = [];
What does this code do?
- Each document.querySelector() call finds the first page element that matches its identifier.
- The named constants keep direct references to the form controls and output areas.
- The applications array starts empty because the learner has not submitted an application yet.
- Save script.js by pressing Cmd+S.
- Confirm that script.js is listed beside index.html and styles.css in the left sidebar.
- Switch back to index.html.
- Find the existing <link rel="stylesheet" href="styles.css" /> line.
- Connect the new file by adding this line directly below the stylesheet link:
<script src="script.js" defer></script>
How does the script connection work?
The src attribute tells the page to load script.js. The defer attribute waits for the page structure to be available before running the script.
- Save index.html by pressing Cmd+S.
- Return to Safari.
- Reload the local tracker by pressing Cmd+R.
You should still see the styled form with an empty Applications area. This confirms that Safari can load the page with the connected script.
Does the tracker look different?
- Confirm that script.js is saved inside the same job-application-tracker folder as index.html.
- Check that the script line sits inside the <head> section directly below the stylesheet link.
- Still stuck? Help me connect script.js to my local Job Application Tracker.
Render the application list
Rendering turns the current applications array into page content. The same function can rebuild the list whenever the array changes.
- Switch back to script.js.
- Place the rendering function below let applications = []; by pasting this code:
function renderApplications() {
applicationsList.replaceChildren();
applicationCount.textContent = applications.length;
if (applications.length === 0) {
const emptyMessage = document.createElement("p");
emptyMessage.textContent = "No applications yet. Add your first one above.";
applicationsList.appendChild(emptyMessage);
return;
}
for (const application of applications) {
const card = document.createElement("article");
const companyHeading = document.createElement("h3");
const roleText = document.createElement("p");
const statusText = document.createElement("p");
companyHeading.textContent = application.company;
roleText.textContent = application.role;
statusText.textContent = `Status: ${application.status}`;
card.appendChild(companyHeading);
card.appendChild(roleText);
card.appendChild(statusText);
applicationsList.appendChild(card);
}
}
How does rendering work?
- The replaceChildren() call clears the previous output before rebuilding the list.
- The applicationCount element receives the current array length.
- An empty array produces a helpful message instead of a blank section.
- Each application becomes an article containing company text, role text, and status text.
- The textContent property inserts submitted values as text instead of interpreting them as page markup.
- Run the renderer when the page starts by adding this line at the bottom of script.js:
renderApplications();
Why call the renderer now?
The initial call draws the current array as soon as the script runs. Because the array starts empty, the Applications area receives its empty-state message.
- Save script.js by pressing Cmd+S.
- Return to Safari.
- Reload the local tracker by pressing Cmd+R.
You should see No applications yet. Add your first one above. beneath the Applications heading. The renderer has produced its first visible result.
Missing the empty message?
- Confirm that renderApplications(); appears at the bottom of script.js.
- Check that applications-list is spelled the same way in index.html and the matching selector.
- Still stuck? Help me debug why renderApplications() does not show the empty message.
Handle form submissions and test memory
A submit event lets the script respond when you use the form. The listener will collect the entered values and create one application object for the array.
- In script.js, find the closing brace of renderApplications().
- Insert the form listener above the final renderApplications(); call by pasting this code:
form.addEventListener("submit", (event) => {
event.preventDefault();
const company = companyInput.value.trim();
const role = roleInput.value.trim();
if (!company || !role) {
return;
}
const newApplication = {
company,
role,
status: statusInput.value,
};
applications = [newApplication, ...applications];
renderApplications();
companyInput.value = "";
roleInput.value = "";
statusInput.value = "Applied";
companyInput.focus();
});
What happens after a submission?
- The addEventListener() method runs the listener whenever the form emits a submit event.
- The preventDefault() call stops the form from reloading the page immediately.
- The trim() calls remove extra whitespace from the company and role values.
- The newApplication object groups the company, role, and selected status.
- The updated array places the newest object first before the renderer rebuilds the cards.
- The final lines reset the form and return focus to the company input.
- Save script.js by pressing Cmd+S.
- Return to Safari.
- Reload the local tracker by pressing Cmd+R.
- Enter Northstar Labs in the Company field.
- Enter Junior Developer in the Role field.
- Select Interview in the Status field.
- Click Add application.
That is your first working tracker interaction. You should see a Northstar Labs card with the Junior Developer role, an Interview status, and a count of 1 tracked.
Does the form clear without adding a card?
- Check that renderApplications(); appears immediately after the array update inside the listener.
- Confirm that the selectors at the top of script.js match the identifiers in index.html.
- Still stuck? Help me debug why my submitted application does not render as a card.
Your two edited files should now match the complete interactive version below.
✔️ Awesome, I've got everything!
Your index.html file now loads script.js. Your script now renders the array and handles form submissions.
ⓧ I'd like to double check the full code
- Compare your index.html file with this complete version:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Job Application Tracker</title>
<link rel="stylesheet" href="styles.css" />
<script src="script.js" defer></script>
</head>
<body>
<main>
<header>
<p class="eyebrow">Portfolio Project</p>
<h1>Job Application Tracker</h1>
<p>Keep every opportunity and its current status in one place.</p>
</header>
<section aria-labelledby="add-application-heading">
<h2 id="add-application-heading">Add an application</h2>
<form id="application-form">
<label for="company">Company</label>
<input id="company" name="company" type="text" required />
<label for="role">Role</label>
<input id="role" name="role" type="text" required />
<label for="status">Status</label>
<select id="status" name="status" required>
<option value="Applied">Applied</option>
<option value="Interview">Interview</option>
<option value="Offer">Offer</option>
<option value="Rejected">Rejected</option>
</select>
<button type="submit">Add application</button>
</form>
</section>
<section aria-labelledby="applications-heading">
<div class="summary">
<h2 id="applications-heading">Applications</h2>
<p><span id="application-count">0</span> tracked</p>
</div>
<div id="applications-list" aria-live="polite"></div>
</section>
</main>
</body>
</html>
- Compare your script.js file with this complete version:
const form = document.querySelector("#application-form");
const companyInput = document.querySelector("#company");
const roleInput = document.querySelector("#role");
const statusInput = document.querySelector("#status");
const applicationCount = document.querySelector("#application-count");
const applicationsList = document.querySelector("#applications-list");
let applications = [];
function renderApplications() {
applicationsList.replaceChildren();
applicationCount.textContent = applications.length;
if (applications.length === 0) {
const emptyMessage = document.createElement("p");
emptyMessage.textContent = "No applications yet. Add your first one above.";
applicationsList.appendChild(emptyMessage);
return;
}
for (const application of applications) {
const card = document.createElement("article");
const companyHeading = document.createElement("h3");
const roleText = document.createElement("p");
const statusText = document.createElement("p");
companyHeading.textContent = application.company;
roleText.textContent = application.role;
statusText.textContent = `Status: ${application.status}`;
card.appendChild(companyHeading);
card.appendChild(roleText);
card.appendChild(statusText);
applicationsList.appendChild(card);
}
}
form.addEventListener("submit", (event) => {
event.preventDefault();
const company = companyInput.value.trim();
const role = roleInput.value.trim();
if (!company || !role) {
return;
}
const newApplication = {
company,
role,
status: statusInput.value,
};
applications = [newApplication, ...applications];
renderApplications();
companyInput.value = "";
roleInput.value = "";
statusInput.value = "Applied";
companyInput.focus();
});
renderApplications();
Your styles.css file remains unchanged from the previous step.
Before you refresh, do you think the Northstar Labs card will remain in the tracker?
- Refresh Safari by pressing Cmd+R.
The application card disappears. The count returns to 0 tracked and the empty message returns.
This shortfall is intentional. Refreshing runs script.js again, so the in-memory applications array starts empty again.
Does the card remain after refreshing?
- Confirm that Safari is displaying the local index.html file from the job-application-tracker folder.
- Check that let applications = []; still appears near the top of script.js.
- Still stuck? Help me verify why an in-memory JavaScript array should reset after refreshing Safari.
Your tracker can now collect applications and render them as cards. Next up, you'll publish this memory-only version so an employer can open it from a public link.
Publish the Memory-Only App
Your tracker runs in Safari on your Mac. An employer cannot open a file stored inside your Documents folder.
A public GitHub repository gives your source code a shareable home. GitHub Pages turns that repository into a public website for the current version of your tracker.
In this step, get ready to:
- Create a public GitHub repository with a README.
- Record the tracker files in a repository commit.
- Verify the public tracker's memory-only behavior.
Create the public repository
A repository stores the files that make up your project. This public repository is included with GitHub Free.
Making the repository public means anyone with its link can inspect your files. That visibility lets an employer review the work behind your tracker.
- Return to the signed-in GitHub page in Safari from earlier.
- Click New repository.
- Choose your personal account in the Owner field.
- Enter job-application-tracker in the repository name field.
- Select Public as the repository visibility.
- Turn on Add README.
- Click Create repository.
You should see the job-application-tracker repository with a README on its default branch. You now have a public home for your first browser app.
Why Add a README?
A README gives the repository a visible landing page before you upload the tracker. It also ensures the repository begins with a default branch that GitHub Pages can publish.
Repository Not Created?
- Confirm that your email address is verified in your GitHub account.
- Check that the repository name is exactly job-application-tracker.
- Still stuck? Help me create my public GitHub repository.
Upload the tracker files
A commit records a saved snapshot of your project in the repository. This snapshot contains the working memory-only tracker from the previous step.
- Click Add file on the repository page.
- Click Upload files.
- Click the upload area to open the macOS file picker.
- Select Documents in the file picker sidebar.
- Open the job-application-tracker folder.
- Select index.html, styles.css, and script.js while holding Cmd.
- Click Open in the file picker.
You should see all three filenames in GitHub's upload area. Each file is now ready to become part of the repository.
- Enter Publish memory-only tracker in the Commit message field.
- Complete the commit to the current branch with the commit control at the bottom of the page.
The repository should now list README, index.html, styles.css, and script.js. Your complete memory-only tracker is now committed online.
Missing an Uploaded File?
- Confirm that GitHub lists all three tracker files beside the README.
- Use Add file to upload any missing file from Documents/job-application-tracker.
- Still stuck? Help me complete my tracker file upload.
Deploy and test the public site
GitHub Pages publishes static website files directly from a repository branch. Your index.html file becomes the entry page for the public tracker.
Why Publish This Version Now?
Publishing the current version separates deployment from data persistence. You can prove that the public page works before changing how the tracker stores applications.
- Click Settings in the repository navigation.
- Select Pages in the settings sidebar.
- Find the Build and deployment section.
- Set Source to Deploy from a branch.
- Select the repository's default branch.
- Select the root folder.
- Click Save.
GitHub Pages can take up to 10 minutes to publish this first deployment. A quiet wait on this screen is normal.
- Refresh the Pages settings screen until Visit site appears.
- Click Visit site.
You should see the styled Job Application Tracker at a public GitHub Pages URL. The form should show its Company, Role, and Status controls.
Public Site Not Ready?
- Wait for the first deployment to finish before refreshing the Pages settings screen again.
- Confirm that index.html appears beside README on the repository's default branch.
- Still stuck? Help me troubleshoot my GitHub Pages deployment.
A successful deployment must support the same form interaction as your local tracker. The public page now gives you a separate place to test that behavior.
- Enter NextWork Labs in the Company field.
- Enter Junior Developer in the Role field.
- Select Applied in the Status field.
- Click Add application.
You should see a card for NextWork Labs. The tracked count should increase to 1.
Before you refresh Safari, predict whether the card will stay visible after a fresh page load.
- Refresh the public tracker with Safari's reload control.
The NextWork Labs card disappears. The tracked count returns to 0.
Why Did the Card Disappear?
The applications array exists only in the current JavaScript page session. Refreshing starts the script again with an empty array.
The uploaded script.js has no localStorage calls. This public refresh test exposes the storage gap that the next step solves.
Your tracker now has a public source repository and a live website. Next up, you'll make its applications survive every refresh.
Save Applications in Browser Storage
Your public tracker now gives employers a link they can open. The refresh test exposed one remaining gap because every card disappears when Safari reloads the page.
The localStorage API gives the live site browser storage that persists across browser sessions. In this step, you'll serialize the applications array into JSON before restoring it when the page starts.
In this step, get ready to:
- Add browser storage write and restore functions.
- Commit the persistent script to GitHub.
- Confirm the live GitHub Pages tracker survives a Safari refresh.
Store each new application
Browser storage saves text values. The saveApplications() function converts the applications array into a JSON string before storing it under the jobApplications key.
- In Safari, return to the public job-application-tracker repository.
- Select script.js from the repository file list.
- Open the file editor.
- Find let applications = []; near the top of the file.
- Add the storage function directly below that line by entering this code:
function saveApplications() {
localStorage.setItem("jobApplications", JSON.stringify(applications));
}
How does saving work?
- The JSON.stringify(applications) call converts the array of application objects into one text value.
- The localStorage.setItem() call stores that text under the jobApplications key.
- The same key lets the tracker retrieve the saved value during a future page load.
- Switch back to the local script.js file in Visual Studio Code from earlier.
- Find let applications = []; in the local file.
- Add the same saveApplications() function directly below that line.
Both copies of script.js now know how to serialize the current application list.
- Return to the GitHub file editor.
- Find the applications = [newApplication, ...applications]; line inside the submit listener.
- Select that line plus the following renderApplications(); line.
- Replace the selected section with this code:
applications = [newApplication, ...applications];
saveApplications();
renderApplications();
Why save at this point?
The array receives the new object before saveApplications() runs. The stored JSON therefore includes the newest application before the interface renders it.
- Switch back to the local script.js file.
- Apply the same three-line replacement inside its submit listener.
- Save the local script.js file.
Both editors should now show saveApplications(); between the array update and renderApplications();.
Saving call in the wrong place?
- Check that saveApplications() appears after the new object enters the applications array.
- Check that the call remains inside the submit listener before renderApplications().
- Still stuck? Help me place saveApplications() correctly in my submit listener.
Restore saved applications
Saving creates the persistent value. The tracker also needs to retrieve that value before its first render so the count and cards start from the stored array.
- Return to the GitHub script.js editor.
- Find the closing brace of saveApplications().
- Add the loading function directly below it by entering this code:
function loadApplications() {
const savedApplications = localStorage.getItem("jobApplications");
if (savedApplications) {
applications = JSON.parse(savedApplications);
}
}
How does loading work?
- The localStorage.getItem() call retrieves the text stored under jobApplications.
- The if check protects the first visit when no saved value exists.
- The JSON.parse(savedApplications) call reconstructs the array of application objects.
- Switch back to the local script.js file.
- Add the same loadApplications() function directly below saveApplications().
Both copies can now rebuild the in-memory array from the saved JSON value.
- Return to the bottom of the GitHub file editor.
- Find the final renderApplications(); line.
- Replace that final line with this two-line startup sequence:
loadApplications();
renderApplications();
Why does the order matter?
The loadApplications() call fills the array before renderApplications() reads it. The first render can then display the restored cards and update applicationCount.
- Switch back to the bottom of the local script.js file.
- Replace its final renderApplications(); line with the same two-line startup sequence.
- Save the local script.js file.
Double-check the complete persistent script before committing the GitHub copy.
✔️ Awesome, I've got everything!
Great. Your local file is saved and the GitHub editor contains the same persistence logic.
ⓧ I'd like to double check the full code
The complete script.js file should match this reference.
const form = document.querySelector("#application-form");
const companyInput = document.querySelector("#company");
const roleInput = document.querySelector("#role");
const statusInput = document.querySelector("#status");
const applicationCount = document.querySelector("#application-count");
const applicationsList = document.querySelector("#applications-list");
let applications = [];
function saveApplications() {
localStorage.setItem("jobApplications", JSON.stringify(applications));
}
function loadApplications() {
const savedApplications = localStorage.getItem("jobApplications");
if (savedApplications) {
applications = JSON.parse(savedApplications);
}
}
function renderApplications() {
applicationsList.replaceChildren();
applicationCount.textContent = applications.length;
if (applications.length === 0) {
const emptyMessage = document.createElement("p");
emptyMessage.textContent = "No applications yet. Add your first one above.";
applicationsList.appendChild(emptyMessage);
return;
}
for (const application of applications) {
const card = document.createElement("article");
const companyHeading = document.createElement("h3");
const roleText = document.createElement("p");
const statusText = document.createElement("p");
companyHeading.textContent = application.company;
roleText.textContent = application.role;
statusText.textContent = `Status: ${application.status}`;
card.appendChild(companyHeading);
card.appendChild(roleText);
card.appendChild(statusText);
applicationsList.appendChild(card);
}
}
form.addEventListener("submit", (event) => {
event.preventDefault();
const company = companyInput.value.trim();
const role = roleInput.value.trim();
if (!company || !role) {
return;
}
const newApplication = {
company,
role,
status: statusInput.value,
};
applications = [newApplication, ...applications];
saveApplications();
renderApplications();
companyInput.value = "";
roleInput.value = "";
statusInput.value = "Applied";
companyInput.focus();
});
loadApplications();
renderApplications();
Does one copy look different?
- Compare the placement of saveApplications() and loadApplications() against the full-file reference.
- Check that both storage calls use the exact jobApplications key.
- Still stuck? Help me compare my persistent script.js with the completed version.
Commit and verify persistence
The GitHub editor still holds the updated script as an uncommitted change. Committing it starts a new GitHub Pages deployment from the repository's default branch.
- Select Commit changes in the GitHub file editor.
- Enter Save applications in browser storage in the Commit message field.
- Complete the commit to the current branch.
A short wait here is normal because GitHub Pages can take up to 10 minutes to publish the new commit.
- Return to the repository's Settings area.
- Select Pages from the sidebar.
- Wait until the latest deployment is ready.
- Select Visit site to load the updated tracker.
- Enter Northstar Labs in the Company field.
- Enter Junior Developer in the Role field.
- Choose Applied from the Status menu.
- Select Add application.
You'll see a Northstar Labs card and a tracked count of 1.
Before you refresh Safari, do you expect the card and count to disappear or return?
- Refresh the live site in Safari.
You'll see the same Northstar Labs card and a tracked count of 1 after the page reloads. This proves that the live tracker restored jobApplications from browser storage.
Does the application disappear?
- Confirm the latest repository commit includes saveApplications() and loadApplications().
- Allow the GitHub Pages deployment up to 10 minutes to finish.
- Reload the live site after the deployment completes.
- Still stuck? Help me troubleshoot why my saved application disappears after refresh.
You closed the tracker's persistence gap. Its cards and application count now return whenever Safari reloads the live site.
Secret mission
Filter Applications by Status
A busy tracker becomes harder to scan as applications pile up. Add a Show filter that focuses the live site on one status while preserving every saved application.
Clean Up Your Resources
Clean Up Your Resources
Your tracker uses GitHub Free with a public repository, so keeping it online creates no ongoing project costs. Choose whether to keep the tracker public, unpublish the site for now, or remove the project resources.
Resources you used:
- The public job-application-tracker repository containing your committed project files.
- The live GitHub Pages deployment running the persistent status-filter version of the tracker.
- The saved jobApplications browser data stored in localStorage for the live site.
Keep everything running
No action needed. Choose this if you want employers to keep accessing the working app and its source code.
- Your public repository keeps index.html, styles.css, and script.js available for review.
- Your GitHub Pages link continues serving the filter-enabled tracker.
- Your saved applications remain available in Safari for the live site's address.
Pause - I'll come back to this later
Unpublish the live site while keeping every committed file in the repository. Your saved browser data remains available if you publish the site again at the same address.
- Switch back to the job-application-tracker repository in Safari.
- Select Settings in the repository navigation.
- Select Pages in the left sidebar.
- Select Unpublish site to remove the current deployment.
Your public tracker is now offline. Your committed files stay ready in the repository for your return.
Delete - I don't want to use this again
Repository deletion is the final option. Clear the browser data first so the remaining actions remove the public site and its GitHub source.
- Open Safari's settings for stored website data.
- Find the domain from your live GitHub Pages address.
- Remove the stored data for that domain.
- Return to the live Job Application Tracker.
- Reload the page to verify that the saved applications are gone.
You'll see the count return to 0 with no application cards. Your browser copy of the project data is now cleared.
- Return to the job-application-tracker repository in Safari.
- Select Settings in the repository navigation.
- Select Pages in the left sidebar.
- Select Unpublish site to remove the GitHub Pages deployment.
- Select General in the left sidebar.
- Scroll to Danger Zone near the bottom of the page.
- Select Delete this repository.
- Complete the confirmation steps shown by GitHub.
Cleanup complete. Your public deployment, source repository, and saved browser data have been removed.
Nice Work!
Nice Work!
You did it! Your Job Application Tracker is live at a public link.
You've learned how to:
- Create a responsive Job Application Tracker with HTML structure plus CSS styling. Commit the source to a public GitHub repository. Publish the app through GitHub Pages for an employer-shareable link.
- Use JavaScript events to turn form entries into objects stored in an array. Render each application through the DOM so cards plus the live count update immediately.
- Expose the limit of memory-only state by refreshing the app. Use localStorage with JSON to restore the same applications across browser sessions.
- Complete the Secret Mission by adding a status filter. Show only Applied, Interview, Offer, or Rejected cards before restoring the full list with All.
Ready to quiz yourself?