Build a Job Hunt Command Center

Build a React job tracker and publish it as portfolio proof of work.

Introduction

30 Second Summary

Recruiters often see evidence of your skills before they meet you. Clear proof makes a stronger case than a list of claims.

In this project, you will build a responsive job application dashboard with React and TypeScript. You will turn it into shareable proof of work through a public GitHub repository and a live GitHub Pages deployment.

What You'll Build

You share a live dashboard that tracks an opportunity from first save to final offer while preserving every update across refreshes.

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

  • A responsive application pipeline where each new opportunity immediately changes the live stage totals.
  • Instant search and stage filters that narrow the visible cards. Browser persistence keeps additions, updates, and deletions after a refresh.
  • A public demo you can send to recruiters. Its README and Git history show how the application evolved.
  • Secret Mission: Add a newest-first sorting control and ship it as a separate enhancement commit.

Are there any prerequisites?

You need a Windows computer with internet access. You also need a GitHub account and access to Claude Code.

Before We Start

Before any hands-on work begins, this checkpoint locks in the proof your finished project should provide. A recruiter should see typed React development, deliberate AI-assisted planning, careful review, independent verification, meaningful Git history, and a deployed job-application dashboard.

Set Up the Proof-of-Work Repository

Your dashboard becomes credible proof when its history is public from the first file. A GitHub repository gives recruiters a record they can inspect.

Your Windows environment also needs compatible development tools. This step proves the full toolchain can serve a typed starter page.

In this step, get ready to:
  • Verify your local development tools.
  • Create a public repository and clone it to your computer.
  • Create the starter files and run the local page.
Verify your development tools

Node.js runs the Vite development server. Git records how your project changes over time.

Claude Code helps you plan the file structure before making edits. Each tool must work from Windows PowerShell before you create the repository.

  • Press the Windows key to open Windows search.
  • Type PowerShell into the search field.
  • Press Enter to open Windows PowerShell.
  • Check your installed Node.js version by running this command:
node --version

How Do You Read the Version?

The command prints the active Node.js version. Vite supports Node.js 20.19+ or 22.12+.

✔️ I see v24.21.0 LTS or a supported version

Your Node.js version can run this project. That is the first part of your local toolchain ready.

ⓧ I see an older version

Your installed version falls below Vite's supported range. Install Node.js v24.21.0 LTS before continuing.

  • Open the official Node.js download page in your browser.
  • Download the Windows installer for Node.js v24.21.0 LTS.
  • Run the downloaded installer.
  • Accept the Windows permission request if it appears.
  • Keep the installer's default components selected.
  • Close Windows PowerShell after the installation finishes.
  • Open Windows PowerShell again through Windows search.
  • Return to the version check above to confirm the new version.

ⓧ Command not found

Node.js is missing from your current Windows environment. The official installer adds Node.js plus npm.

  • Open the official Node.js download page in your browser.
  • Download the Windows installer for Node.js v24.21.0 LTS.
  • Run the downloaded installer.
  • Accept the Windows permission request if it appears.
  • Keep the installer's default components selected.
  • Close Windows PowerShell after the installation finishes.
  • Open Windows PowerShell again through Windows search.
  • Return to the version check above to confirm Node.js is available.
  • Check whether Claude Code is available by running this command:
claude --version

What Does This Check Prove?

A working installation prints a Claude Code version number. That confirms Windows PowerShell can start the assistant later in this step.

✔️ I see a Claude Code version

Claude Code is available from Windows PowerShell. You can use it inside the repository.

ⓧ Command not found

Claude Code needs its official Windows installation before you continue.

  • Install Claude Code through WinGet by running this command:
winget install Anthropic.ClaudeCode

What Does This Command Do?

WinGet downloads the official Claude Code package. It also registers the claude command with Windows.

  • Approve the installation request if Windows displays one.
  • Close Windows PowerShell after the installation finishes.
  • Open Windows PowerShell again through Windows search.
  • Return to the Claude Code version check above to confirm the installation.
  • Check your installed Git version by running this command:
git --version

What Does This Check Prove?

Git prints the version Windows PowerShell can use. This project targets Git for Windows 2.56.0 or later.

✔️ I see version 2.56.0 or higher

Git is ready to clone your repository. Your commits can now become part of the proof-of-work history.

ⓧ I see an older version

Your Git installation needs an update before you clone the repository.

  • Open the official Git for Windows installer page in your browser.
  • Download the latest maintained x64 installer.
  • Run the downloaded installer.
  • Keep the default selections unless your workplace requires specific settings.
  • Close Windows PowerShell after the installation finishes.
  • Open Windows PowerShell again through Windows search.
  • Return to the Git version check above to confirm the update.

ⓧ Command not found

Git for Windows is missing from your current environment. Install it before the repository is cloned.

  • Open the official Git for Windows installer page in your browser.
  • Download the latest maintained x64 installer.
  • Run the downloaded installer.
  • Keep the default selections unless your workplace requires specific settings.
  • Close Windows PowerShell after the installation finishes.
  • Open Windows PowerShell again through Windows search.
  • Return to the Git version check above to confirm Git is available.

Still Missing a Command?

Close every open PowerShell window after an installation. A fresh window reloads the command locations registered with Windows.

If the command remains unavailable, use this troubleshooting prompt: Help me diagnose why Node.js, Git, or Claude Code is unavailable in Windows PowerShell after installation.

Create and clone the public repository

A public repository makes your source code visible from the beginning. Its README creates the first tracked file for Git to clone.

  • Open GitHub's new repository page in your browser.
  • Sign in to GitHub if the page asks you to authenticate.
  • Enter job-hunt-command-center as the repository name.
  • Select Public as the repository visibility.
  • Enable the README option.
  • Click Create repository.
  • Click Code above the repository files.
  • Select HTTPS in the clone options.
  • Click the copy icon beside the HTTPS URL.
  • Store the copied address here: YOUR-REPOSITORY-URL.

Why Start Public?

The public repository holds the evidence behind your finished demo. Keep credentials plus private job-search notes out of every project file.

  • Return to the Windows PowerShell window from the tool checks.
  • Move to your Desktop by running this command:
Set-Location ~/Desktop

What Does This Command Do?

PowerShell changes its current location to your Desktop. The cloned repository folder will be easy to find there.

  • Clone the public repository and enter its folder by running these commands:
git clone [[YOUR_REPOSITORY_URL="YOUR-REPOSITORY-URL"]]
Set-Location job-hunt-command-center

What Do These Commands Do?

  • The first command copies the public repository into a new job-hunt-command-center folder.
  • The second command moves PowerShell into that cloned folder.

Good progress. Your local folder now shares history with the public repository.

  • Verify the repository's remote address by running this command:
git remote -v

What Should the Remote Show?

The output lists origin beside your copied GitHub URL. That link allows later commits to reach the public repository.

Clone or Remote Check Failed?

Confirm that PowerShell is using the repository's HTTPS URL. Make sure the URL belongs to job-hunt-command-center.

Use this prompt if the repository still cannot be cloned: Help me diagnose why my public GitHub repository will not clone through HTTPS.

Create and run the starter

React renders the starter interface. TypeScript checks the application's data contracts as the dashboard grows.

The project instructions make your AI collaboration reviewable. Claude Code reads CLAUDE.md at the start of later sessions.

  • Start Claude Code inside the cloned repository by running this command:
claude

What Does This Command Do?

The command starts an interactive Claude Code session in job-hunt-command-center. Requests in this session apply to the cloned repository.

  • Create the project instructions by pasting this request into Claude Code:
Create CLAUDE.md with exactly this content. Do not edit any other file.

# Project Instructions

## Goal

Build a clear, accessible job application dashboard that can serve as employment-focused proof of work.

## Working agreement

- Explain the plan before editing files.
- Keep the application focused on one useful workflow.
- Use React and TypeScript without adding unapproved dependencies.
- Replace arrays and objects instead of mutating React state.
- Preserve visible labels, keyboard focus styles, and responsive behavior.
- Run `npm run check` and `npm run build` before calling a change complete.
- Summarize changed files, manual checks, and remaining limitations after each task.

Why Create This File First?

The working agreement defines how Claude Code should collaborate with you. Planning plus verification become part of the development process from the first implementation request.

That instruction file now protects the quality of later edits. Your next request uses it before any application code changes.

  • Ask Claude Code to read the instructions and propose the file plan by pasting this request:
Read CLAUDE.md before editing any other file.

Explain the plan for creating .npmrc, .gitignore, package.json, tsconfig.json, index.html, src/main.tsx, src/App.tsx, and src/styles.css.

Include the purpose of every file. Do not edit the files yet.

What Should Claude Code Do?

Claude Code should return a file-by-file plan without changing the repository. This pause gives you a chance to inspect the scope before approving edits.

  • Review the proposed plan for the exact files named in your request.
  • Approve the implementation by pasting this request:
Approved. Create the planned starter files.

Use package-lock=false in .npmrc.
Ignore node_modules/, dist/, and *.log in .gitignore.
Use a private ESM package named job-hunt-command-center.
Pin React and React DOM to 19.3.0.
Pin TypeScript to 7.0.2.
Pin Vite to 8.3.4.
Add dev, check, build, and preview scripts from the plan.
Use strict TypeScript settings with an ES2022 target.
Use Bundler module resolution and React JSX support.
Create the root element and /src/main.tsx module script in index.html.
Validate rootElement before rendering App in src/main.tsx.
Render a starter page titled Job Hunt Command Center from src/App.tsx.
Create src/styles.css as the starter stylesheet.
Do not alter README.md.
Summarize every created file after the edits.

What Does This Request Control?

The request pins every direct dependency plus the required scripts. It also limits the starter to the setup files needed for the first local page.

Claude Code should finish with a changed-file summary. Review that summary before leaving the session.

  • Press Ctrl+C to leave Claude Code after you have reviewed its summary.
  • Install the dependencies from package.json by running this command:
npm install

What Does This Command Do?

npm reads package.json and installs the pinned packages into node_modules. The package-lock=false setting prevents a package-lock.json file from being written.

The dependency installation is complete when PowerShell returns to its prompt without an installation error.

Dependency Installation Failed?

Confirm that PowerShell is inside the job-hunt-command-center folder. Confirm that package.json exists in that folder.

Use this prompt for package-specific help: Help me diagnose why npm install fails in my job-hunt-command-center repository.

Before you start the server, what page title do you expect the browser to show?

  • Start the Vite development server by running this command:
npm run dev

What Does This Command Do?

npm runs the dev script from package.json. Vite serves the project locally while this PowerShell process remains active.

Keep this window running while you inspect the page. Press Ctrl+C when you later want to stop the server.

  • Open http://localhost:5173 in your browser.
  • Confirm that the page displays Job Hunt Command Center.

You have the first visible result. The public repository now runs as a typed local React project.

Starter Page Missing?

Check that the development server still occupies the PowerShell window. Confirm that your browser uses the exact local address shown above.

Use this prompt if the page remains unavailable: Help me diagnose why my Vite starter does not load at localhost.

✔️ Awesome, I've got everything!

Your dependencies are installed. Keep the development server running while you compare the browser with the starter page.

ⓧ I'd like to double check the full code

Compare each starter file with these references. Keep the initial README.md created by GitHub.

package-lock=false
node_modules/
dist/
*.log
{
  "name": "job-hunt-command-center",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "check": "tsc --noEmit",
    "build": "tsc --noEmit && vite build --base=./",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "19.3.0",
    "react-dom": "19.3.0"
  },
  "devDependencies": {
    "@types/react": "19.3.0",
    "@types/react-dom": "19.3.0",
    "typescript": "7.0.2",
    "vite": "8.3.4"
  }
}
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "react-jsx",
    "strict": true,
    "isolatedModules": true,
    "noEmit": true,
    "skipLibCheck": true,
    "types": ["vite/client"]
  },
  "include": ["src"]
}
<!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 job application dashboard built as employment-focused proof of work."
    />
    <title>Job Hunt Command Center</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
import { createRoot } from 'react-dom/client';
import App from './App';
import './styles.css';

const rootElement = document.getElementById('root');

if (!rootElement) {
  throw new Error('Root element not found');
}

createRoot(rootElement).render(<App />);
export default function App() {
  return (
    <>
      <header className="hero">
        <div className="hero-content">
          <p className="eyebrow">Portfolio proof of work</p>
          <h1>Job Hunt Command Center</h1>
          <p className="hero-copy">
            Track every opportunity, keep momentum visible, and know what to do next.
          </p>
        </div>
      </header>
    </>
  );
}
:root {
  font-family:
    Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
    sans-serif;
  color: #1e293b;
  background: #f1f5f9;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
  min-height: 100vh;
}
# Project Instructions

## Goal

Build a clear, accessible job application dashboard that can serve as employment-focused proof of work.

## Working agreement

- Explain the plan before editing files.
- Keep the application focused on one useful workflow.
- Use React and TypeScript without adding unapproved dependencies.
- Replace arrays and objects instead of mutating React state.
- Preserve visible labels, keyboard focus styles, and responsive behavior.
- Run `npm run check` and `npm run build` before calling a change complete.
- Summarize changed files, manual checks, and remaining limitations after each task.

Your environment is reproducible. Your public repository also has its first working page. Next, you will turn that starter into a responsive application dashboard.

Build the First Visible Dashboard

Your starter page proves that the React toolchain runs. The single heading still gives a recruiter little evidence of what you can build.

This step creates an early visual win with a responsive job-application dashboard. TypeScript keeps every seeded record consistent while the interface gives you something concrete to improve.

In this step, get ready to:
  • Review a typed application model before any files change.
  • Build the dashboard hero, live stage totals, and seeded application cards.
  • Verify the responsive result and record it in Git history.
Review the typed data model

A typed data model defines the information every application must carry. Claude Code can propose that structure while you decide whether it matches the dashboard's scope.

What should the proposal contain?

  • The Status union contains Saved, Applied, Interview, and Offer.
  • The JobApplication type contains id, company, role, appliedOn, and status.
  • The statuses array supplies the four pipeline stages.
  • The starterApplications array contains records for Northstar Labs and Brightside Studio.
  • Return to the Claude Code session from earlier.
  • Ask Claude Code to propose the smallest typed data model for the dashboard.
  • Tell Claude Code to stop after explaining its proposal.
  • Check that the proposed status union contains all four required stages.
  • Check that the proposed application type contains all five required fields.
  • Check that the proposal includes both seeded application records.
  • Approve the proposal once every requirement matches.

That review puts you in control of the implementation. Claude Code now has an approved model instead of an open-ended request.

Build the dashboard interface

The dashboard derives each stage total from the same array that supplies its cards. One source of data keeps the visible counts connected to the records below them.

What should Claude Code build?

  • The hero presents the Job Hunt Command Center title and its purpose.
  • The status grid calculates one total for each value in statuses.
  • The application grid displays the company, role, and application date from each seeded record.
  • The stylesheet supports desktop, tablet, and narrow-screen layouts.
  • The focus styles keep keyboard navigation visible.
  • Ask Claude Code to implement the approved dashboard structure in src/App.tsx.
  • Inspect the changes to src/App.tsx before accepting them.
  • Ask Claude Code to replace the starter styles in src/styles.css with the responsive visual system.
  • Inspect the changes to src/styles.css before accepting them.
  • Switch back to the browser tab serving http://localhost:5173.
  • Refresh the dashboard page.

You should see a blue hero above four total cards. You should also see cards for Northstar Labs and Brightside Studio.

Dashboard not updating?

  • Confirm that Claude Code edited src/App.tsx instead of creating another component file.
  • Confirm that src/main.tsx still imports ./styles.css.
  • Confirm that the development server from the previous step is still running in PowerShell.

Ask for help checking the dashboard files.

✔️ My dashboard component matches

Your typed records, totals, and seeded cards are in place. Keep this file saved.

ⓧ I'd like to double check the full code

Compare your complete src/App.tsx file with this reference.

type Status = 'Saved' | 'Applied' | 'Interview' | 'Offer';

type JobApplication = {
  id: string;
  company: string;
  role: string;
  appliedOn: string;
  status: Status;
};

const statuses: Status[] = ['Saved', 'Applied', 'Interview', 'Offer'];

const starterApplications: JobApplication[] = [
  {
    id: 'starter-1',
    company: 'Northstar Labs',
    role: 'Junior Frontend Developer',
    appliedOn: '2026-10-08',
    status: 'Applied',
  },
  {
    id: 'starter-2',
    company: 'Brightside Studio',
    role: 'Web Developer',
    appliedOn: '2026-10-11',
    status: 'Interview',
  },
];

export default function App() {
  return (
    <>
      <header className="hero">
        <div className="hero-content">
          <p className="eyebrow">Portfolio proof of work</p>
          <h1>Job Hunt Command Center</h1>
          <p className="hero-copy">
            Track every opportunity, keep momentum visible, and know what to do next.
          </p>
        </div>
      </header>

      <main className="app-shell">
        <section className="stats-grid" aria-label="Application totals by status">
          {statuses.map((currentStatus) => (
            <article className="stat-card" key={currentStatus}>
              <span>{currentStatus}</span>
              <strong>
                {
                  starterApplications.filter(
                    (application) => application.status === currentStatus,
                  ).length
                }
              </strong>
            </article>
          ))}
        </section>

        <section className="panel">
          <div className="section-heading toolbar-heading">
            <div>
              <p className="eyebrow">Your pipeline</p>
              <h2>Applications</h2>
            </div>
            <p className="result-count" aria-live="polite">
              {starterApplications.length} shown
            </p>
          </div>

          <div className="application-grid">
            {starterApplications.map((application) => (
              <article className="job-card" key={application.id}>
                <div>
                  <p className="company-name">{application.company}</p>
                  <h3>{application.role}</h3>
                  <p className="application-date">
                    Applied {application.appliedOn}
                  </p>
                </div>
              </article>
            ))}
          </div>
        </section>
      </main>
    </>
  );
}

What does this file do?

  • The types define the allowed shape of every seeded application.
  • The first mapping operation calculates one total for each stage.
  • The second mapping operation turns each seeded record into a visible card.

✔️ My responsive styles match

Your dashboard has the complete responsive visual system. Keep src/styles.css saved.

ⓧ I'd like to double check the full code

Compare your complete src/styles.css file with this reference.

:root {
  font-family:
    Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
    sans-serif;
  color: #1e293b;
  background: #f1f5f9;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
  min-height: 100vh;
}

button,
input,
select {
  font: inherit;
}

button,
select {
  cursor: pointer;
}

.hero {
  color: #ffffff;
  background:
    radial-gradient(circle at top right, rgba(56, 189, 248, 0.35), transparent 35%),
    linear-gradient(135deg, #0f172a, #1e3a8a);
  padding: 4rem 1.25rem 6rem;
}

.hero-content,
.app-shell {
  width: min(1100px, 100%);
  margin: 0 auto;
}

.hero h1 {
  max-width: 720px;
  margin: 0.25rem 0 0.75rem;
  font-size: clamp(2.25rem, 7vw, 4.5rem);
  line-height: 1;
  letter-spacing: -0.05em;
}

.hero-copy {
  max-width: 620px;
  margin: 0;
  color: #cbd5e1;
  font-size: 1.1rem;
  line-height: 1.7;
}

.eyebrow {
  margin: 0;
  color: #0284c7;
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.hero .eyebrow {
  color: #7dd3fc;
}

.app-shell {
  display: grid;
  gap: 1.5rem;
  padding: 0 1.25rem 4rem;
  transform: translateY(-3rem);
}

.stats-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1rem;
}

.stat-card,
.panel {
  border: 1px solid #e2e8f0;
  border-radius: 1rem;
  background: #ffffff;
  box-shadow: 0 18px 45px rgba(15, 23, 42, 0.08);
}

.stat-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.25rem;
}

.stat-card span {
  color: #64748b;
  font-weight: 700;
}

.stat-card strong {
  color: #0f172a;
  font-size: 1.75rem;
}

.panel {
  padding: clamp(1.25rem, 4vw, 2rem);
}

.section-heading {
  display: flex;
  align-items: end;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.25rem;
}

.section-heading h2 {
  margin: 0.2rem 0 0;
  color: #0f172a;
  font-size: 1.5rem;
}

.form-grid,
.toolbar {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1rem;
  align-items: end;
}

.toolbar {
  grid-template-columns: 2fr 1fr;
  margin-bottom: 1.25rem;
}

label {
  display: grid;
  gap: 0.45rem;
  color: #334155;
  font-size: 0.85rem;
  font-weight: 700;
}

input,
select {
  width: 100%;
  min-height: 2.8rem;
  border: 1px solid #cbd5e1;
  border-radius: 0.65rem;
  background: #ffffff;
  color: #0f172a;
  padding: 0.65rem 0.75rem;
}

input:focus,
select:focus,
button:focus-visible {
  outline: 3px solid rgba(14, 165, 233, 0.35);
  outline-offset: 2px;
}

.primary-button {
  min-height: 2.8rem;
  border: 0;
  border-radius: 0.65rem;
  background: #0369a1;
  color: #ffffff;
  padding: 0.7rem 1rem;
  font-weight: 800;
}

.primary-button:hover {
  background: #075985;
}

.result-count {
  margin: 0;
  color: #64748b;
  font-weight: 700;
}

.application-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
}

.job-card {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 1rem;
  border: 1px solid #e2e8f0;
  border-radius: 0.85rem;
  background: #f8fafc;
  padding: 1.25rem;
}

.company-name {
  margin: 0 0 0.3rem;
  color: #0369a1;
  font-size: 0.85rem;
  font-weight: 800;
  text-transform: uppercase;
}

.job-card h3 {
  margin: 0;
  color: #0f172a;
  font-size: 1.1rem;
}

.application-date {
  margin: 0.55rem 0 0;
  color: #64748b;
  font-size: 0.9rem;
}

.stage-control {
  min-width: 130px;
}

.delete-button {
  justify-self: start;
  border: 0;
  background: transparent;
  color: #b91c1c;
  padding: 0;
  font-weight: 800;
}

.empty-state {
  border: 1px dashed #94a3b8;
  border-radius: 0.85rem;
  padding: 2.5rem 1rem;
  text-align: center;
}

.empty-state h3,
.empty-state p {
  margin: 0;
}

.empty-state p {
  margin-top: 0.4rem;
  color: #64748b;
}

@media (max-width: 820px) {
  .stats-grid,
  .form-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .application-grid {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 560px) {
  .hero {
    padding-top: 3rem;
  }

  .stats-grid,
  .form-grid,
  .toolbar {
    grid-template-columns: 1fr;
  }

  .section-heading,
  .job-card {
    align-items: stretch;
    grid-template-columns: 1fr;
  }

  .section-heading {
    display: grid;
  }
}

What does this stylesheet do?

  • The shared colors and spacing give the dashboard one visual system.
  • The grid rules place totals and applications in responsive columns.
  • The two media queries adapt the layout for tablet and narrow browser widths.
  • The focus rules make keyboard position visible on interactive controls.
Verify and publish the milestone

A visual result can still hide a type mismatch. The project check asks TypeScript to validate the component without generating build files.

Before you run the check, do you expect every card and count to satisfy the typed model.

  • Validate the dashboard types by running this command in the repository's PowerShell terminal:
npm run check

What does this check do?

The script runs the TypeScript compiler with --noEmit. It reports type problems without creating output files.

The command should return to the PowerShell prompt without TypeScript errors. That quiet result confirms the dashboard satisfies the project's strict type rules.

Type check failing?

  • Compare each property in starterApplications with the JobApplication type.
  • Check that every seeded status exactly matches one value in the Status union.
  • Check the closing parentheses around both mapping operations in src/App.tsx.

Ask for help with the TypeScript check.

The Vite development server keeps its PowerShell window busy while it serves the page. Restarting it gives you a clean final check at the same local address.

Before you restart it, do you expect the updated dashboard to load at the same local address.

  • Return to the PowerShell terminal running the development server from earlier.
  • Press Ctrl+C to stop the current server.
  • Start the updated dashboard by running this command:
npm run dev

What does this command do?

The development script starts Vite and serves the current project files. Leave this PowerShell process running while you review the dashboard.

  • Switch back to the browser tab for http://localhost:5173.
  • Refresh the page.

You should see two seeded application cards and four stage totals. Applied should show 1 while Interview should show 1.

  • Narrow the browser window below 560px by dragging its edge.

You should see the four totals stack into one column. The application cards should also remain readable in one column.

A meaningful Git commit preserves this visible milestone as part of the project's development story. The public history can now show how the interface evolved.

  • Stage, commit, and push the verified dashboard by running these commands:
git add .
git commit -m "Build responsive application dashboard"
git push origin main

What do these commands record?

  • The first command stages the dashboard files for the next commit.
  • The second command records the responsive dashboard milestone with a descriptive message.
  • The third command sends the commit to the public repository's main branch.

PowerShell should report that the commit was created and the push completed. Your public repository now contains the dashboard milestone.

Push not completing?

  • Confirm that PowerShell is still inside the cloned job-hunt-command-center repository.
  • Confirm that the repository still has its origin remote from the previous step.
  • Complete any GitHub authentication prompt that appears during the push.

Ask for help diagnosing the push.

  • Return to the public GitHub repository page from earlier.
  • Refresh the repository page.

You should see Build responsive application dashboard as the latest commit message. This confirms that your first substantial proof-of-work milestone is public.

You now have a responsive dashboard that shows typed records and live stage totals. Next, you will add applications and filters so the interface responds to real input.

Add Applications and Filters

The seeded cards prove that your responsive layout works. A real job hunt needs the dashboard to accept opportunities as your pipeline changes.

In this step, React state turns the dashboard into an interactive tracker. You will test each interaction before challenging the page with a refresh.

In this step, get ready to:
  • Add applications through controlled form fields.
  • Narrow applications with combined search-plus-stage filtering.
  • Test the dashboard's page-refresh behavior.
Add controlled application fields

A controlled form keeps each field value in component state. The submitted values then become a new application record.

Claude Code can implement this change from a reviewed plan while preserving the dashboard you already tested.

What should the interaction include?

  • The applications state starts with starterApplications.
  • The company, role, appliedOn, and status values control the form fields.
  • Each submission creates a nextApplication record with a unique ID.
  • The new record is prepended with an immutable array update.
  • Each card changes stage with map().
  • Each card can be removed with filter().
  • Return to the Claude Code session for the job-hunt-command-center repository.
  • Ask Claude Code to inspect src/App.tsx before proposing the smallest plan for controlled form state plus immutable card updates.
  • Review the proposal for the state names listed above.
  • Ask Claude Code to apply the approved plan to src/App.tsx.
  • Verify the edited file in PowerShell by running this command:
npm run check

What does this command check?

The check script runs the TypeScript compiler without creating output files. It catches incompatible state values and event-handler mistakes before you test the form.

Good progress. TypeScript now accepts the form state and immutable card updates.

Type check failing?

  • Check that every application stage matches the Status union exactly.
  • Check that the application date uses the appliedOn property.
  • Check that useState is imported from React.

Help me diagnose the TypeScript errors in my controlled React form and immutable application updates.

The browser provides the quickest proof that the form updates your dashboard. Your development server keeps this feedback loop close to each change.

  • Return to the PowerShell terminal from earlier.
  • Start the development server if it is not already running by using this command:
npm run dev

What does this command start?

The development script starts the Vite server. It serves the current dashboard for local browser testing.

  • Return to http://localhost:5173 in your browser.
  • Enter Signal Harbor in the Company field.
  • Enter Frontend Developer in the Role field.
  • Enter 2026-10-14 in the Application date field.
  • Select Saved in the Stage field.
  • Click Add application.

You will see a third card for Signal Harbor. The Saved total increases to 1.

  • Select Interview from the Stage control on the Signal Harbor card.

The Signal Harbor card now shows Interview. The Interview total increases to 2.

Application not appearing?

  • Confirm that every required form field contains a value.
  • Check that the form calls event.preventDefault() before creating the record.
  • Check that setApplications returns a new array containing nextApplication.

Help me find why my controlled form does not add a new application card.

Build combined filtering

A filtered view derives the visible cards from the full applications array. The original records stay available while the search query or stage selection changes.

  • Return to Claude Code in the job-hunt-command-center repository.
  • Ask Claude Code to propose a case-insensitive search plus stage-filter plan for src/App.tsx.
  • Review the proposal for a derived filteredApplications array.
  • Ask Claude Code to apply the approved filtering plan.
  • Verify the completed interaction in PowerShell by running this command:
npm run check

What is being verified now?

The type check now covers the search state and stage-filter state. It also verifies the values passed through each controlled filter.

The check passes with the complete interaction in place. Your dashboard can now create records and derive a focused view from them.

Filtering code not passing the check?

  • Confirm that statusFilter accepts Status | 'All'.
  • Confirm that the stage comparison uses application.status.
  • Confirm that the search comparison converts both values to lowercase.

Help me debug the typed search and stage filtering in my React dashboard.

✔️ Awesome, I've got everything!

Your src/App.tsx file now contains the form, card actions, search, and stage filtering. Save the file before testing the complete flow.

ⓧ I'd like to double check the full code

Compare your src/App.tsx file with this complete version:

import { useState } from 'react';

type Status = 'Saved' | 'Applied' | 'Interview' | 'Offer';

type JobApplication = {
  id: string;
  company: string;
  role: string;
  appliedOn: string;
  status: Status;
};

const statuses: Status[] = ['Saved', 'Applied', 'Interview', 'Offer'];

const starterApplications: JobApplication[] = [
  {
    id: 'starter-1',
    company: 'Northstar Labs',
    role: 'Junior Frontend Developer',
    appliedOn: '2026-10-08',
    status: 'Applied',
  },
  {
    id: 'starter-2',
    company: 'Brightside Studio',
    role: 'Web Developer',
    appliedOn: '2026-10-11',
    status: 'Interview',
  },
];

export default function App() {
  const [applications, setApplications] =
    useState<JobApplication[]>(starterApplications);
  const [company, setCompany] = useState('');
  const [role, setRole] = useState('');
  const [appliedOn, setAppliedOn] = useState('');
  const [status, setStatus] = useState<Status>('Applied');
  const [search, setSearch] = useState('');
  const [statusFilter, setStatusFilter] = useState<Status | 'All'>('All');

  const filteredApplications = applications.filter((application) => {
    const query = search.trim().toLowerCase();
    const matchesSearch =
      application.company.toLowerCase().includes(query) ||
      application.role.toLowerCase().includes(query);
    const matchesStatus =
      statusFilter === 'All' || application.status === statusFilter;

    return matchesSearch && matchesStatus;
  });

  return (
    <>
      <header className="hero">
        <div className="hero-content">
          <p className="eyebrow">Portfolio proof of work</p>
          <h1>Job Hunt Command Center</h1>
          <p className="hero-copy">
            Track every opportunity, keep momentum visible, and know what to do next.
          </p>
        </div>
      </header>

      <main className="app-shell">
        <section className="stats-grid" aria-label="Application totals by status">
          {statuses.map((currentStatus) => (
            <article className="stat-card" key={currentStatus}>
              <span>{currentStatus}</span>
              <strong>
                {
                  applications.filter(
                    (application) => application.status === currentStatus,
                  ).length
                }
              </strong>
            </article>
          ))}
        </section>

        <section className="panel">
          <div className="section-heading">
            <div>
              <p className="eyebrow">Add an opportunity</p>
              <h2>Keep the pipeline moving</h2>
            </div>
          </div>

          <form
            className="form-grid"
            onSubmit={(event) => {
              event.preventDefault();

              if (!company.trim() || !role.trim() || !appliedOn) {
                return;
              }

              const nextApplication: JobApplication = {
                id: `${Date.now()}-${company.trim()}`,
                company: company.trim(),
                role: role.trim(),
                appliedOn,
                status,
              };

              setApplications((current) => [nextApplication, ...current]);
              setCompany('');
              setRole('');
              setAppliedOn('');
              setStatus('Applied');
            }}
          >
            <label>
              Company
              <input
                required
                value={company}
                onChange={(event) => setCompany(event.target.value)}
                placeholder="Example Company"
              />
            </label>

            <label>
              Role
              <input
                required
                value={role}
                onChange={(event) => setRole(event.target.value)}
                placeholder="Junior Web Developer"
              />
            </label>

            <label>
              Application date
              <input
                required
                type="date"
                value={appliedOn}
                onChange={(event) => setAppliedOn(event.target.value)}
              />
            </label>

            <label>
              Stage
              <select
                value={status}
                onChange={(event) => setStatus(event.target.value as Status)}
              >
                {statuses.map((currentStatus) => (
                  <option key={currentStatus} value={currentStatus}>
                    {currentStatus}
                  </option>
                ))}
              </select>
            </label>

            <button className="primary-button" type="submit">
              Add application
            </button>
          </form>
        </section>

        <section className="panel">
          <div className="section-heading toolbar-heading">
            <div>
              <p className="eyebrow">Your pipeline</p>
              <h2>Applications</h2>
            </div>
            <p className="result-count" aria-live="polite">
              {filteredApplications.length} shown
            </p>
          </div>

          <div className="toolbar">
            <label>
              Search
              <input
                type="search"
                value={search}
                onChange={(event) => setSearch(event.target.value)}
                placeholder="Company or role"
              />
            </label>

            <label>
              Filter by stage
              <select
                value={statusFilter}
                onChange={(event) =>
                  setStatusFilter(event.target.value as Status | 'All')
                }
              >
                <option value="All">All stages</option>
                {statuses.map((currentStatus) => (
                  <option key={currentStatus} value={currentStatus}>
                    {currentStatus}
                  </option>
                ))}
              </select>
            </label>
          </div>

          {filteredApplications.length === 0 ? (
            <div className="empty-state">
              <h3>No applications match</h3>
              <p>Change the search or filter, or add a new opportunity.</p>
            </div>
          ) : (
            <div className="application-grid">
              {filteredApplications.map((application) => (
                <article className="job-card" key={application.id}>
                  <div>
                    <p className="company-name">{application.company}</p>
                    <h3>{application.role}</h3>
                    <p className="application-date">
                      Applied {application.appliedOn}
                    </p>
                  </div>

                  <label className="stage-control">
                    Stage
                    <select
                      value={application.status}
                      onChange={(event) => {
                        const nextStatus = event.target.value as Status;
                        setApplications((current) =>
                          current.map((item) =>
                            item.id === application.id
                              ? { ...item, status: nextStatus }
                              : item,
                          ),
                        );
                      }}
                    >
                      {statuses.map((currentStatus) => (
                        <option key={currentStatus} value={currentStatus}>
                          {currentStatus}
                        </option>
                      ))}
                    </select>
                  </label>

                  <button
                    className="delete-button"
                    type="button"
                    onClick={() =>
                      setApplications((current) =>
                        current.filter((item) => item.id !== application.id),
                      )
                    }
                  >
                    Delete
                  </button>
                </article>
              ))}
            </div>
          )}
        </section>
      </main>
    </>
  );
}

How should I use this reference?

Compare the state declarations and derived filter first. Then compare the form submission and card controls.

The finished file keeps all application updates immutable. A fresh array is returned after every add, stage change, or deletion.

  • Return to the dashboard in your browser.
  • Enter signal harbor in the Search field.
  • Select Interview from the Filter by stage control.

You will see one result for Signal Harbor. The lowercase query still matches the company name.

  • Replace the search query with Northstar.

You will see the No applications match empty state. Northstar has an Applied stage, so it does not satisfy the active Interview filter.

  • Clear the Search field.
  • Select All stages from the Filter by stage control.

All three application cards are visible again. Your search and stage controls now work separately or together.

Expose the refresh limitation

A fresh page load is the final stress test for this version. It reveals which changes survive after the current page session ends.

  • Click Delete on the Northstar Labs card.

The Northstar Labs card disappears. The Applied total falls to 0.

Before you refresh, do you expect Signal Harbor to remain in the list?

  • Refresh http://localhost:5173 in your browser.

The refresh exposed the gap

You will see only the original two seeded applications. Signal Harbor is gone.

The deleted Northstar Labs card has returned. React initializes the component state from starterApplications after each page load.

This version has no browser persistence. The interactions work during the current page session only.

You have reproduced the limitation on the main path. Record this version so your repository shows the working interaction before its persistence upgrade.

  • Save the intentionally limited milestone to the remote repository by running these commands:
git add .
git commit -m "Add application form and filters"
git push origin main

What do these commands record?

Git stages the changed application file before creating a named snapshot. The push adds that snapshot to the public repository history.

This commit documents a useful but temporary version of the tracker. Its refresh limitation is now part of the development story.

Your terminal should confirm that the commit was created and sent to the remote main branch. That public history now shows the form and filtering milestone.

Commit or push failing?

  • Confirm that PowerShell is still inside the job-hunt-command-center repository.
  • Check that src/App.tsx was saved before staging the changes.
  • Confirm that your GitHub sign-in is still active before pushing.

Help me diagnose why my application form and filters commit will not push to GitHub.

Your dashboard now accepts real applications and narrows the pipeline on demand. Next, you will turn the refresh failure into a persistent browser experience.

Persist the Application Tracker

Your dashboard now accepts applications. It also searches, filters, updates, and deletes them. The refresh test exposed the remaining weakness: every change disappears when the page reloads.

React state lives in the current page session. This step connects that state to browser storage so the tracker can restore a user's work after a refresh.

In this step, get ready to:
  • Ask Claude Code to diagnose the refresh failure before editing the application.
  • Add guarded loading and saving through local storage.
  • Verify the persistent behavior before committing the fix.
Diagnose the refresh failure

A diagnosis separates the cause from the solution. Claude Code should first explain why the in-memory array resets when the browser reloads.

  • Switch back to the Claude Code session in the cloned repository.
  • Ask Claude Code to diagnose why newly added applications disappear after a browser refresh without editing any files.
  • Review its diagnosis for the distinction between temporary React state and persistent browser storage.
  • Ask Claude Code to implement guarded local storage helpers, a lazy state initializer, and one immutable commit helper in src/App.tsx.
  • Allow the requested edit if Claude Code asks for permission.
  • Ask Claude Code to summarize the changed code and the manual checks that remain.

Why diagnose before editing?

The earlier refresh proved that the application array existed only in memory. Diagnosing that boundary first gives the persistence code a clear job: save every accepted update and restore it during startup.

Review the persistence changes

The storage key gives this app one stable location inside the browser's origin-specific storage. The loading helper reads that location during startup.

  • In src/App.tsx, inspect the line directly below the statuses declaration.
  • Confirm the storage key matches this line:
const storageKey = 'job-hunt-command-center.applications';

What does the storage key do?

The storageKey value identifies the saved application array. Reusing one key lets the loader and saver access the same browser entry.

  • Inspect the new loadApplications() helper below starterApplications.
  • Confirm the helper matches this code:
function loadApplications(): JobApplication[] {
  try {
    const savedApplications = window.localStorage.getItem(storageKey);
    return savedApplications
      ? (JSON.parse(savedApplications) as JobApplication[])
      : starterApplications;
  } catch {
    return starterApplications;
  }
}

How does loading stay safe?

  • The call to window.localStorage.getItem(storageKey) reads the saved JSON string.
  • The conditional expression parses existing records or returns starterApplications when no saved data exists.
  • The catch block keeps the dashboard usable when stored data cannot be read or parsed.
  • Inspect the new saveApplications() helper below loadApplications().
  • Confirm the helper matches this code:
function saveApplications(applications: JobApplication[]) {
  try {
    window.localStorage.setItem(storageKey, JSON.stringify(applications));
  } catch {
    // Keep the app usable when browser storage is unavailable.
  }
}

How does saving stay safe?

The helper converts the application array into JSON before writing it. Its guarded write leaves the rest of the dashboard usable if the browser blocks storage.

A lazy initializer lets React call the loader once while creating the initial application state. The stored records become the first rendered array when they exist.

  • Inspect the first state declaration inside App().
  • Confirm applications uses this lazy initializer:
const [applications, setApplications] = useState<JobApplication[]>(loadApplications);

Why pass the function directly?

Passing loadApplications to useState makes React use it as a lazy initializer. The app reads storage while establishing its initial state.

Every accepted change now passes through one commit helper. That helper calculates a replacement array before updating React state and saving the same result.

  • Inspect the new commitApplications() helper below filteredApplications.
  • Confirm the helper matches this code:
function commitApplications(
    update: (current: JobApplication[]) => JobApplication[],
  ) {
    setApplications((current) => {
      const next = update(current);
      saveApplications(next);
      return next;
    });
  }

What does the commit helper protect?

  • The update callback receives the current application array.
  • The next variable holds the replacement array created by the requested change.
  • The helper saves the replacement array before returning it as the next React state.
  • Confirm the form submission calls commitApplications() with a prepended replacement array.
  • Confirm each stage change calls commitApplications() with an array produced by map().
  • Confirm each deletion calls commitApplications() with an array produced by filter().

Why preserve immutable updates?

The form creates a new array with spread syntax. Stage changes use map(). Deletions use filter().

Each path gives React and local storage the same replacement array. The saved data stays aligned with the dashboard.

✔️ Awesome, I've got everything!

Your persistence path now loads saved applications and commits every immutable update to browser storage.

ⓧ I'd like to double check the full code

Compare your complete src/App.tsx file with this reference:

import { useState } from 'react';

type Status = 'Saved' | 'Applied' | 'Interview' | 'Offer';

type JobApplication = {
  id: string;
  company: string;
  role: string;
  appliedOn: string;
  status: Status;
};

const statuses: Status[] = ['Saved', 'Applied', 'Interview', 'Offer'];
const storageKey = 'job-hunt-command-center.applications';

const starterApplications: JobApplication[] = [
  {
    id: 'starter-1',
    company: 'Northstar Labs',
    role: 'Junior Frontend Developer',
    appliedOn: '2026-10-08',
    status: 'Applied',
  },
  {
    id: 'starter-2',
    company: 'Brightside Studio',
    role: 'Web Developer',
    appliedOn: '2026-10-11',
    status: 'Interview',
  },
];

function loadApplications(): JobApplication[] {
  try {
    const savedApplications = window.localStorage.getItem(storageKey);
    return savedApplications
      ? (JSON.parse(savedApplications) as JobApplication[])
      : starterApplications;
  } catch {
    return starterApplications;
  }
}

function saveApplications(applications: JobApplication[]) {
  try {
    window.localStorage.setItem(storageKey, JSON.stringify(applications));
  } catch {
    // Keep the app usable when browser storage is unavailable.
  }
}

export default function App() {
  const [applications, setApplications] = useState<JobApplication[]>(loadApplications);
  const [company, setCompany] = useState('');
  const [role, setRole] = useState('');
  const [appliedOn, setAppliedOn] = useState('');
  const [status, setStatus] = useState<Status>('Applied');
  const [search, setSearch] = useState('');
  const [statusFilter, setStatusFilter] = useState<Status | 'All'>('All');

  const filteredApplications = applications.filter((application) => {
    const query = search.trim().toLowerCase();
    const matchesSearch =
      application.company.toLowerCase().includes(query) ||
      application.role.toLowerCase().includes(query);
    const matchesStatus =
      statusFilter === 'All' || application.status === statusFilter;

    return matchesSearch && matchesStatus;
  });

  function commitApplications(
    update: (current: JobApplication[]) => JobApplication[],
  ) {
    setApplications((current) => {
      const next = update(current);
      saveApplications(next);
      return next;
    });
  }

  return (
    <>
      <header className="hero">
        <div className="hero-content">
          <p className="eyebrow">Portfolio proof of work</p>
          <h1>Job Hunt Command Center</h1>
          <p className="hero-copy">
            Track every opportunity, keep momentum visible, and know what to do next.
          </p>
        </div>
      </header>

      <main className="app-shell">
        <section className="stats-grid" aria-label="Application totals by status">
          {statuses.map((currentStatus) => (
            <article className="stat-card" key={currentStatus}>
              <span>{currentStatus}</span>
              <strong>
                {
                  applications.filter(
                    (application) => application.status === currentStatus,
                  ).length
                }
              </strong>
            </article>
          ))}
        </section>

        <section className="panel">
          <div className="section-heading">
            <div>
              <p className="eyebrow">Add an opportunity</p>
              <h2>Keep the pipeline moving</h2>
            </div>
          </div>

          <form
            className="form-grid"
            onSubmit={(event) => {
              event.preventDefault();

              if (!company.trim() || !role.trim() || !appliedOn) {
                return;
              }

              const nextApplication: JobApplication = {
                id: `${Date.now()}-${company.trim()}`,
                company: company.trim(),
                role: role.trim(),
                appliedOn,
                status,
              };

              commitApplications((current) => [nextApplication, ...current]);
              setCompany('');
              setRole('');
              setAppliedOn('');
              setStatus('Applied');
            }}
          >
            <label>
              Company
              <input
                required
                value={company}
                onChange={(event) => setCompany(event.target.value)}
                placeholder="Example Company"
              />
            </label>

            <label>
              Role
              <input
                required
                value={role}
                onChange={(event) => setRole(event.target.value)}
                placeholder="Junior Web Developer"
              />
            </label>

            <label>
              Application date
              <input
                required
                type="date"
                value={appliedOn}
                onChange={(event) => setAppliedOn(event.target.value)}
              />
            </label>

            <label>
              Stage
              <select
                value={status}
                onChange={(event) => setStatus(event.target.value as Status)}
              >
                {statuses.map((currentStatus) => (
                  <option key={currentStatus} value={currentStatus}>
                    {currentStatus}
                  </option>
                ))}
              </select>
            </label>

            <button className="primary-button" type="submit">
              Add application
            </button>
          </form>
        </section>

        <section className="panel">
          <div className="section-heading toolbar-heading">
            <div>
              <p className="eyebrow">Your pipeline</p>
              <h2>Applications</h2>
            </div>
            <p className="result-count" aria-live="polite">
              {filteredApplications.length} shown
            </p>
          </div>

          <div className="toolbar">
            <label>
              Search
              <input
                type="search"
                value={search}
                onChange={(event) => setSearch(event.target.value)}
                placeholder="Company or role"
              />
            </label>

            <label>
              Filter by stage
              <select
                value={statusFilter}
                onChange={(event) =>
                  setStatusFilter(event.target.value as Status | 'All')
                }
              >
                <option value="All">All stages</option>
                {statuses.map((currentStatus) => (
                  <option key={currentStatus} value={currentStatus}>
                    {currentStatus}
                  </option>
                ))}
              </select>
            </label>
          </div>

          {filteredApplications.length === 0 ? (
            <div className="empty-state">
              <h3>No applications match</h3>
              <p>Change the search or filter, or add a new opportunity.</p>
            </div>
          ) : (
            <div className="application-grid">
              {filteredApplications.map((application) => (
                <article className="job-card" key={application.id}>
                  <div>
                    <p className="company-name">{application.company}</p>
                    <h3>{application.role}</h3>
                    <p className="application-date">
                      Applied {application.appliedOn}
                    </p>
                  </div>

                  <label className="stage-control">
                    Stage
                    <select
                      value={application.status}
                      onChange={(event) => {
                        const nextStatus = event.target.value as Status;
                        commitApplications((current) =>
                          current.map((item) =>
                            item.id === application.id
                              ? { ...item, status: nextStatus }
                              : item,
                          ),
                        );
                      }}
                    >
                      {statuses.map((currentStatus) => (
                        <option key={currentStatus} value={currentStatus}>
                          {currentStatus}
                        </option>
                      ))}
                    </select>
                  </label>

                  <button
                    className="delete-button"
                    type="button"
                    onClick={() =>
                      commitApplications((current) =>
                        current.filter((item) => item.id !== application.id),
                      )
                    }
                  >
                    Delete
                  </button>
                </article>
              ))}
            </div>
          )}
        </section>
      </main>
    </>
  );
}
Prove the data survives and ship the fix

The browser test now checks the full persistence cycle. You will prove that additions, stage changes, and deletions all survive a reload.

  • Return to PowerShell in the cloned job-hunt-command-center repository.
  • Start the development server by running this command:
npm run dev

What does this command do?

The command starts the Vite development server. Keep this PowerShell process running while you test the dashboard at http://localhost:5173.

Development server not starting?

Confirm PowerShell is inside the cloned job-hunt-command-center repository. The folder must contain package.json.

If a dependency is missing, compare your installed project files with the setup from earlier. Help me diagnose why the development server does not start.

Before you refresh, which of your next changes do you expect the dashboard to remember? Hold that prediction while you test the app.

  • Open http://localhost:5173 in your browser.
  • Enter Persistence Labs in the Company field.
  • Enter Storage Engineer in the Role field.
  • Choose 2026-10-14 in the Application date field.
  • Select Applied in the Stage field.
  • Click Add application.
  • Refresh the browser page.

That is the first persistence win. You should still see the Persistence Labs card after the refresh.

  • Change the card's Stage control to Interview.
  • Refresh the browser page.
  • Click Delete on the Persistence Labs card.
  • Refresh the browser page again.

You should see the stage remain Interview after the first refresh. The deleted card should remain absent after the final refresh.

Changes disappearing after refresh?

  • Check that applications uses loadApplications without calling it inside useState.
  • Check that form submission calls commitApplications() instead of setApplications() directly.
  • Check that stage changes and deletions also call commitApplications().

Help me trace why my application changes do not survive a browser refresh.

The user flow now works across refreshes. The final technical check asks TypeScript to inspect the source before Vite creates the production output.

Before you run the checks, do you expect the persistence changes to satisfy the type checker? Keep your prediction in mind while both commands run.

  • Return to an available PowerShell prompt in the cloned repository.
  • Run the type check and production build with these commands:
npm run check
npm run build

What do these checks prove?

  • The first command checks the TypeScript source without generating JavaScript output.
  • The second command repeats the type check before building the production site into dist.
  • The generated dist folder stays out of Git because .gitignore already excludes it.

Both commands should finish successfully. PowerShell should report a completed production build with output generated in dist.

Type check or build failing?

  • Compare every JobApplication return type with the reference code above.
  • Check that commitApplications() returns next from the state callback.
  • Check that every opening brace and parenthesis around the new helpers has a matching closing character.

Help me diagnose the TypeScript or Vite build failure in my persistence changes.

  • Stage the persistence changes and save the milestone by running these commands:
git add .
git commit -m "Persist job applications in browser storage"
git push origin main

What does this Git checkpoint capture?

The commit records the transition from temporary state to a persistent application tracker. The push adds that milestone to the public repository's development history.

Commit or push not completing?

Confirm the commands run inside the cloned repository. A push also needs the GitHub authentication established earlier.

Help me diagnose why my persistence commit does not reach the main branch.

Your tracker now keeps additions, stage changes, and deletions across browser refreshes. Next, you will turn this working repository into a documented live demo.

Publish the Evidence

Your React dashboard now preserves every addition, stage change, and deletion across refreshes. Its progressive commit history also shows how you diagnosed the original data-loss problem.

A recruiter cannot evaluate code that only runs on your computer. This step turns your public GitHub repository into documented proof of work with a live demo.

In this step, get ready to:
  • Write a recruiter-facing README that explains the project and your development approach.
  • Add an automated GitHub Actions workflow for GitHub Pages.
  • Push the deployment workflow and verify the public site.
Document the project

A strong README gives a reviewer the context that the interface cannot show by itself. It explains the problem, technical choices, verification process, and your role in the work.

  • Replace the initial contents of README.md with the project overview below:
# Job Hunt Command Center

A responsive job application dashboard built as employment-focused proof of work.

## Problem

Job searches become difficult to manage when opportunities, dates, and stages are scattered across notes. This app keeps that pipeline in one visible place.

## Features

- Add company, role, date, and application stage
- View live totals for each stage
- Search by company or role
- Filter by application stage
- Update a stage or delete an application
- Keep data after a browser refresh
- Use the dashboard on desktop and narrow screens

## Stack

- React
- TypeScript
- Vite
- Browser local storage
- GitHub Actions
- GitHub Pages

What Does This Section Explain?

The problem section connects the dashboard to a recognizable job-search challenge. The features section turns the interface into concrete capabilities.

The stack section names the technologies behind those capabilities. A reviewer can quickly connect your implementation choices to the finished product.

  • Save README.md.
  • Open your editor's rendered Markdown preview.

You should see the project title followed by clear Problem, Features, and Stack sections.

Does the README Look Like Plain Text?

Check that each heading starts with the same number of # characters shown above. Make sure each feature and stack item starts with a hyphen.

Ask for help with the Markdown structure if the preview still looks wrong: Why is my README Markdown rendering as plain text?

The next section makes the project reproducible. It gives reviewers the same commands and checks you used before publishing.

  • Continue README.md by adding the local run instructions and verification checklist below the Stack section:
## Run locally

```powershell
npm install
npm run dev
```

## Verify

```powershell
npm run check
npm run build
```

Manual checks:

1. Add an application.
2. Search for the company or role.
3. Filter to its stage.
4. Change the stage and refresh.
5. Delete the application and refresh again.
6. Narrow the browser and confirm the layout remains usable.

Why Include Verification Steps?

The local commands make the repository reproducible. The checklist records the behaviors you manually exercised after building the dashboard.

Together, they show that the project was reviewed through automated checks and direct interaction.

  • Save README.md.
  • Refresh the rendered Markdown preview.

You should see two formatted command blocks followed by a six-item manual checklist.

Are the Commands Missing Their Code Boxes?

Check that each command group has an opening and closing line of three backticks. Keep the word powershell on the opening line.

Use this prompt if the fenced blocks still render incorrectly: Help me fix the fenced command blocks in my README.

The final sections explain where to find the deployed app and how you collaborated during development. Honest attribution makes your decision-making visible.

  • Complete README.md by adding the sections below the manual checklist:
## Live demo

Add the GitHub Pages URL here after the first successful deployment.

## Development approach

Claude Code assisted with planning, implementation, explanation, and review. I approved the scope, inspected each change, exercised the user flow, and ran the type check and production build before publishing.

## What this proves

This repository demonstrates typed UI development, state management, persistence, responsive CSS, Git history, documentation, and automated deployment.

What Do These Sections Prove?

The development approach describes Claude Code as a collaborator within a review process. It keeps your decisions and verification work visible.

The final section connects TypeScript, Vite, responsive design, browser persistence, and deployment to practical proof-of-work outcomes.

  • Save README.md.
  • Refresh the rendered Markdown preview.

You should see a Live demo placeholder followed by Development approach and What this proves sections. Your README now tells the complete story behind the dashboard.

Is a README Section Missing?

Compare the section order with the full-file reference below. Check that each section appears once.

Ask for a structural review if a section is duplicated or out of order: Help me compare my README structure with the required section order.

✔️ Awesome, I've got everything!

Great. Double check that README.md is saved and every section appears in the rendered preview.

ⓧ I'd like to double check the full code

Compare your current README.md with this complete pre-deployment version:

# Job Hunt Command Center

A responsive job application dashboard built as employment-focused proof of work.

## Problem

Job searches become difficult to manage when opportunities, dates, and stages are scattered across notes. This app keeps that pipeline in one visible place.

## Features

- Add company, role, date, and application stage
- View live totals for each stage
- Search by company or role
- Filter by application stage
- Update a stage or delete an application
- Keep data after a browser refresh
- Use the dashboard on desktop and narrow screens

## Stack

- React
- TypeScript
- Vite
- Browser local storage
- GitHub Actions
- GitHub Pages

## Run locally

```powershell
npm install
npm run dev
```

## Verify

```powershell
npm run check
npm run build
```

Manual checks:

1. Add an application.
2. Search for the company or role.
3. Filter to its stage.
4. Change the stage and refresh.
5. Delete the application and refresh again.
6. Narrow the browser and confirm the layout remains usable.

## Live demo

Add the GitHub Pages URL here after the first successful deployment.

## Development approach

Claude Code assisted with planning, implementation, explanation, and review. I approved the scope, inspected each change, exercised the user flow, and ran the type check and production build before publishing.

## What this proves

This repository demonstrates typed UI development, state management, persistence, responsive CSS, Git history, documentation, and automated deployment.

How to Use This Reference

Compare the headings, lists, code fences, and paragraphs in order. The Live demo placeholder stays in place until the first deployment produces your public URL.

Create the deployment workflow

A YAML workflow turns each push to main into a repeatable deployment. It checks out the repository, prepares the runtime, builds the project, and publishes the generated dist output.

  • Create .github/workflows/deploy.yml inside the job-hunt-command-center folder using your code editor.
  • Add the workflow trigger, permissions, and build preparation by copying this first section:
name: Deploy to GitHub Pages

on:
  push:
    branches:
      - main

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6
      - name: Setup Node.js
        uses: actions/setup-node@v7
        with:
          node-version: '24'
      - name: Configure Pages
        uses: actions/configure-pages@v5
      - name: Install dependencies
        run: npm install
      - name: Build
        run: npm run build

What Does This Workflow Section Do?

  • The push trigger starts the workflow whenever new work reaches main.
  • The permissions allow the workflow to read the repository and publish through GitHub Pages.
  • The setup steps check out the source and prepare Node.js version 24.
  • The final two steps install the pinned packages and run the production build.
  • Save .github/workflows/deploy.yml.
  • Confirm that your editor lists the file under .github/workflows.

You should see deploy.yml nested inside the two folders. The saved file now contains the full build job up to production output generation.

Is the Workflow File in the Wrong Folder?

Check that the complete path is .github/workflows/deploy.yml. GitHub only discovers workflow files stored in that folder.

Use this prompt if the nested path is unclear: Help me confirm where my GitHub Actions workflow file is stored.

The build job creates the deployable files. The remaining job uploads those files and publishes them through GitHub Pages.

  • Complete .github/workflows/deploy.yml by adding this section below the Build step:
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v4
        with:
          path: './dist'

  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

How Does the Deployment Finish?

The upload step packages the generated ./dist directory as the Pages artifact. The deploy job waits for the build job before publishing that artifact.

The deployment environment also exposes the resulting page URL. That URL becomes the public entry point for your dashboard.

  • Save .github/workflows/deploy.yml.
  • Inspect the file indentation from top to bottom.

You should see the build and deploy jobs aligned beneath jobs. The deploy job should contain its own environment, runner, dependency, and steps.

Does the YAML Indentation Look Uneven?

Use spaces consistently. Align sibling keys at the same indentation level.

Ask for an indentation review before committing if the structure is unclear: Help me check the indentation in my Pages workflow.

✔️ Awesome, I've got everything!

Great. Double check that .github/workflows/deploy.yml is saved in the workflow directory.

ⓧ I'd like to double check the full code

Compare your workflow with the complete file below:

name: Deploy to GitHub Pages

on:
  push:
    branches:
      - main

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6
      - name: Setup Node.js
        uses: actions/setup-node@v7
        with:
          node-version: '24'
      - name: Configure Pages
        uses: actions/configure-pages@v5
      - name: Install dependencies
        run: npm install
      - name: Build
        run: npm run build
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v4
        with:
          path: './dist'

  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

How to Check the Workflow

Compare the trigger, permissions, action versions, build commands, artifact path, and deploy job. YAML indentation determines which settings belong to each job.

Publish to GitHub Pages

Publishing makes the repository and dashboard available to anyone with the links. Keep private company notes, credentials, and personal job-search details out of the source code.

  • Stage the documentation and workflow changes by running this command in PowerShell:
git add .

What Does This Command Do?

This stages the updated README and new deployment workflow for the next commit. Your ignored dependencies and generated build output stay outside the commit.

  • Record the documentation and deployment milestone by running:
git commit -m "Document and deploy the project"

What Does This Commit Capture?

This creates a named point in the repository history for the recruiter-facing documentation and automated deployment workflow.

  • Send the commit to the public repository by running:
git push origin main

What Happens After the Push?

The push updates the remote main branch. It also triggers the deployment workflow stored in your repository.

Your documentation and workflow are now visible in the public repository. Next, you need to enable Pages as the publishing destination.

  • Return to your job-hunt-command-center repository on GitHub.
  • Select Settings in the repository navigation.
  • Select Pages in the repository settings sidebar.
  • Choose GitHub Actions as the publishing source.

The repository is now configured to accept deployments from your custom workflow.

  • Return to the main repository page.
  • Select Actions in the repository navigation.
  • Open the latest Deploy to GitHub Pages workflow run.

GitHub installs the dependencies before running the production build. Wait until the build and deploy jobs both show successful completion.

Did the Workflow Fail?

Open the failed job and identify the first step that did not complete. A build failure usually points to a project check, while a deployment failure points to the Pages configuration or workflow permissions.

Use the failed step name when asking for help: Help me diagnose my failed GitHub Pages workflow.

What Has the Workflow Proven?

A successful run proves that GitHub can install the pinned dependencies and create the production build. It also proves that the generated site can be uploaded and deployed automatically.

  • Return to Settings in the repository navigation.
  • Select Pages in the repository settings sidebar.
  • Record the published address here: your GitHub Pages URL.

The published address connects your repository documentation to the live dashboard. Add it to the README so a reviewer can move directly from the code to the working product.

  • Switch back to README.md in your code editor.
  • Find the sentence Add the GitHub Pages URL here after the first successful deployment..
  • Replace that sentence with your GitHub Pages URL.
  • Save README.md.
  • Stage the final live-demo link by running:
git add .

What Is Being Staged Now?

This stages the README update containing the deployed site address. The repository documentation can now direct reviewers to the live app.

  • Record the completed documentation by running:
git commit -m "Document and deploy the project"

Why Record the URL Update?

The commit adds the final connection between the repository and deployed product. Anyone reading the README can now reach both parts of your proof of work.

  • Push the live-demo link to main by running:
git push origin main

What Does the Final Push Trigger?

The updated README reaches the public repository. The push also starts another deployment run from the documented source state.

  • Return to the repository's Actions view.
  • Open the newest Deploy to GitHub Pages workflow run.
  • Wait for the build and deploy jobs to complete successfully.

Before you open the published link, do you expect the deployed dashboard to preserve the responsive interface and seeded application cards from your local build?

  • Open your GitHub Pages URL in your browser.

You should see the Job Hunt Command Center hero, four stage totals, two seeded applications, the application form, and the search controls. Narrow the browser to confirm that the cards and form still adapt to the available width.

Does the Published Site Fail to Load?

Confirm that the latest workflow completed successfully. Check that you copied the published Pages address from the same repository.

Ask for help with the visible symptom and workflow state: Help me diagnose why my deployed GitHub Pages dashboard is not loading.

You have turned a local dashboard into public proof of work. Your repository now shows the problem, implementation, verification process, development history, and live result.

Secret mission

Add Newest-First Sorting

Add a sorting control that brings the most recent application dates to the top. You will sort a copied list so the persisted application order stays intact.

Clean Up Your Resources

Clean Up Your Resources

This project's public GitHub repository, GitHub Actions workflow, and GitHub Pages site have no ongoing cost. Decide whether to keep the demo live, pause its public deployment, or delete the repository and browser-local data.

Resources you used:

  • A public job-hunt-command-center GitHub repository containing source code and commit history.
  • The GitHub Actions workflow stored at .github/workflows/deploy.yml.
  • A public GitHub Pages deployment for the dashboard.
  • Origin-specific browser local storage containing application data.

Keep everything running

No action is required. This option keeps your repository available for anyone who needs to review the project.

  • Leave the job-hunt-command-center repository public.
  • Keep the deployment workflow active for future pushes to main.
  • Retain the browser-local application data for your next visit.

Pause - I'll come back to this later

This option removes the public demo while keeping the repository ready for later work. Your source code and commit history remain available.

  • Stop pushing new commits to main.
  • Return to the repository's Pages settings.
  • Disable the public deployment.
  • Revisit the live site URL.

You should no longer see the public dashboard. The repository and deployment workflow remain ready for your return.

Delete - I don't want to use this again

This option removes every listed resource. Repository deletion is permanent, so this path is for a clean break.

  • Return to the live GitHub Pages site from this project.
  • Open your browser's site data controls for the current page.
  • Clear the stored site data for this origin.
  • Refresh the dashboard.

That clears the local copy. You should see only the seeded Northstar Labs and Brightside Studio applications.

  • Return to the job-hunt-command-center repository on GitHub.
  • Go to the repository settings page.
  • Use the repository deletion control to delete job-hunt-command-center.
  • Confirm the deletion when GitHub asks.
  • Revisit the former repository URL.
  • Revisit the former GitHub Pages URL.

That completes the full teardown. The repository should no longer be available. Its workflow and Pages deployment are removed with it.

Nice Work!

Nice Work!

You did it. Your live Job Hunt Command Center now turns a real job search into public proof of your web-development skills.

You've learned how to:

  • Build a responsive job application dashboard with React. Use TypeScript to model every record. Create controlled forms for new applications. Derive live views from search terms plus stage filters.
  • Reproduce a deliberate refresh failure. Diagnose temporary component state. Fix the gap with browser local storage. Preserve safe updates through immutable array replacements.
  • Direct Claude Code through a review-first development loop. Record the app's evolution in meaningful Git commits. Present the evidence through a recruiter-facing README. Publish verified builds with GitHub Actions to GitHub Pages.
  • Secret Mission: Added a newest-first sorting control. Derived the sorted view from a copied filtered array. Kept persisted application order unchanged.

Ready to quiz yourself?