Build a Persistent Fitness Tracker

Build a local fitness tracker with browser storage and a daily dashboard.

Introduction

30 Second Summary

Workout details are easy to forget once the day gets busy. A quick record makes it easier to see how today's movement adds up.

In this project, you will build FitLog, a responsive fitness diary that runs locally in Safari. Its daily dashboard summarizes a workout history that returns after a browser refresh.

What You'll Build

Your finished FitLog app turns a workout entry into an updated history row plus a refreshed daily dashboard in Safari.

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

  • A responsive workout form for recording an activity with its date, duration, and calorie estimate.
  • A daily dashboard that calculates today's workout count, active minutes, and calories. A progress bar tracks the 30-minute goal.
  • A persistent workout history that returns after a browser refresh through localStorage. Built-in controls delete one workout or clear the complete log.
  • Secret Mission: Add an activity filter that shows one workout type without changing the dashboard totals.

Are there any prerequisites?

You should be comfortable writing HTML, CSS, and JavaScript.

Step 1 walks you through setting up Visual Studio Code, Node.js, and Vite on your Mac.

Before We Start

This checkpoint locks in FitLog's focused scope before any setup begins. You are building a local fitness diary for manual workout tracking in one browser.

Set Up the Local Web Project

FitLog needs a dependable browser address before it can preserve workouts. Browser storage opened from a file: URL has undefined behavior.

A local server from Vite gives FitLog a reliable origin. You will prepare the editor plus the runtime that powers that server.

In this step, get ready to:
  • Install Visual Studio Code plus the required Node.js release.
  • Create the fitlog folder with its Vite configuration.
  • Run the development server and view the starter page in Safari.
Install the Local Tools

Visual Studio Code gives you one place to edit FitLog. Node.js supplies the local tools that run Vite.

  • Visit the official Visual Studio Code download page.
  • Download the macOS disk image.
  • Open the downloaded .dmg file.
  • Drag Visual Studio Code.app into the Applications folder.
  • Press Cmd+Space to open Spotlight.
  • Type Visual Studio Code into the search field.
  • Press Enter to open the editor.

Your editor is installed. It is ready to hold every FitLog file you create.

Why Visual Studio Code?

Android Studio focuses on Android app development. FitLog runs as a standalone browser app.

Visual Studio Code keeps this project focused on HTML plus CSS plus JavaScript. The smaller toolchain keeps the browser persistence lesson visible.

  • Press Cmd+Shift+P inside Visual Studio Code.
  • Enter Shell Command: Install 'code' command in PATH into the search field.
  • Select the matching result.
  • Press Cmd+Space to open Spotlight.
  • Type Terminal into the search field.
  • Press Enter to open Terminal.
  • Check the active Node.js version by running this command:
node --version

What Does This Command Check?

Node.js prints the active runtime version. This result determines which installation path matches your Mac.

✔️ I see the required version

Your terminal prints v24.20.0. This Node.js release also includes npm 11.19.0.

ⓧ I see an older version

Your current Node.js version does not match FitLog. Install Node.js 24.20.0 LTS before continuing.

  • Download the official Node.js 24.20.0 macOS installer.
  • Open the downloaded installer package.
  • Complete the installation prompts.
  • Check the updated Node.js version by running this command:
node --version

What Should You See?

Terminal should print v24.20.0. The pinned runtime is now active.

ⓧ Command not found

Node.js is not installed yet. The official macOS package installs Node.js 24.20.0 with npm 11.19.0.

  • Download the official Node.js 24.20.0 macOS installer.
  • Open the downloaded installer package.
  • Complete the installation prompts.
  • Check the Node.js installation by running this command:
node --version

What Should You See?

Terminal should print v24.20.0. Your Mac now has the required runtime.

Node.js Still Not Ready?

  • Confirm that you downloaded the macOS installer for Node.js 24.20.0.
  • Close Terminal after the installation.
  • Reopen Terminal through Spotlight before checking the version again.

Still blocked? Help me troubleshoot my Node.js installation. You can also share the symptoms with the NextWork community.

You have cleared the machine setup. Visual Studio Code now works with FitLog's exact Node.js release.

Create the FitLog Project

The project needs its own folder on your Desktop. Its package.json file pins Vite to 8.2.2.

  • Switch back to the Terminal window from earlier.
  • Move to your Desktop by running this command:
cd ~/Desktop

What Does This Command Do?

The cd command changes Terminal's current folder. The ~ character represents your Mac home folder.

  • Create the fitlog folder and open it in Visual Studio Code by running these commands:
mkdir fitlog
cd fitlog
code .

What Do These Commands Do?

  • The first command creates the fitlog folder on your Desktop.
  • The second command moves Terminal into that folder.
  • The final command opens the current folder in Visual Studio Code.

You should see fitlog at the top of the Explorer view. This confirms that the correct folder is open.

Folder Did Not Open?

  • Confirm that the Terminal path ends with Desktop/fitlog.
  • Run the Visual Studio Code shell command from the earlier setup if Terminal cannot find code.

Need help with the folder? Help me open my fitlog folder in Visual Studio Code.

  • Select the Explorer view in the Activity Bar.
  • Select the New File... button.
  • Enter package.json as the file name.
  • Press Enter to create the file.

The new package.json tab opens in the editor. The file also appears in the Explorer view.

  • Add the Vite project configuration by copying this code into package.json:
{
  "name": "fitlog",
  "private": true,
  "version": "1.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "devDependencies": {
    "vite": "8.2.2"
  }
}

What Does This Configuration Do?

  • The name field identifies the project as fitlog.
  • The scripts object defines the Vite development plus build plus preview commands.
  • The devDependencies object pins Vite to 8.2.2.
  • Save package.json by pressing Cmd+S.
  • Confirm that package.json remains visible in the Explorer view.

✔️ Awesome, I've got everything!

Your Vite configuration is saved. Keep the fitlog folder open for the dependency installation.

ⓧ I'd like to double check the full code

{
  "name": "fitlog",
  "private": true,
  "version": "1.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "devDependencies": {
    "vite": "8.2.2"
  }
}

Compare this reference with your saved package.json file. Every key plus value should match.

  • Switch back to the Terminal window from earlier.
  • Install the dependencies from package.json by running this command:
npm install

What Happens During Installation?

npm reads package.json and installs Vite 8.2.2. It also generates package-lock.json for the resolved dependency tree.

Terminal prints the installation results when the download finishes. A returned prompt means the command has completed.

  • Return to the Explorer view in Visual Studio Code.
  • Confirm that package-lock.json appears beside package.json.

The dependency lock file is your visible proof that npm processed the project configuration. Vite 8.2.2 is ready to run.

Dependency Installation Failed?

  • Confirm that Terminal is inside the Desktop/fitlog folder.
  • Check that package.json matches the double-check tab.
  • Confirm that your earlier Node.js check printed v24.20.0.

Still blocked? Help me diagnose my npm installation problem. You can also share the terminal output with the NextWork community.

Launch the Starter Page

Vite uses index.html as the development entry point. A temporary heading gives you a visible checkpoint before you build the workout interface.

  • Select the New File... button in the Explorer view.
  • Enter index.html as the file name.
  • Press Enter to create the file.

The new index.html tab opens in the editor. You can now add the page Vite serves.

  • Add the temporary FitLog page by copying this code into index.html:
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>FitLog</title>
  </head>
  <body>
    <header class="hero">
      <h1>FitLog</h1>
    </header>
  </body>
</html>

What Does This Page Do?

  • The document metadata gives the browser tab its FitLog title.
  • The viewport setting prepares the page for the responsive layout you build next.
  • The heading creates the visible checkpoint for the local development server.
  • Save index.html by pressing Cmd+S.
  • Confirm that index.html appears beside the package files in the Explorer view.

✔️ Awesome, I've got everything!

Your temporary FitLog page is saved. Vite can now serve it from the project folder.

ⓧ I'd like to double check the full code

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>FitLog</title>
  </head>
  <body>
    <header class="hero">
      <h1>FitLog</h1>
    </header>
  </body>
</html>

Compare this reference with your saved index.html file. The visible heading should read FitLog.

  • Switch back to the Terminal window from earlier.
  • Predict which heading Safari will show when the local page loads.
  • 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 index.html from a localhost origin.

The server keeps using this Terminal window while it runs. Press Control+C when you need to stop it.

Terminal displays http://localhost:5173 as the local address. Keep the server running during the browser check.

  • Press Cmd+Space to open Spotlight.
  • Type Safari into the search field.
  • Press Enter to open Safari.
  • Press Cmd+L to select the address field.
  • Enter http://localhost:5173.
  • Press Return to load FitLog.

You should see the FitLog starter heading in Safari. The address remains http://localhost:5173.

That is your first visible win. FitLog now runs from the reliable browser origin its stored workouts will use.

Starter Page Not Loading?

  • Confirm that the Terminal running Vite remains open.
  • Check that Safari uses http://localhost:5173 exactly.
  • Confirm that index.html is saved inside the fitlog folder.

Need help tracing the problem? Help me troubleshoot my FitLog starter page.

Your local web project is ready. Next, you will replace the starter heading with FitLog's workout form plus visible history.

Log the First Workout

Your Vite starter already runs on a reliable local address. FitLog now needs a real interface that turns workout details into visible history.

The first useful version keeps its workout records in browser memory. A refresh gives you a concrete test of what that memory can preserve.

In this step, get ready to:
  • Build the responsive FitLog page structure.
  • Style the dashboard, form, and workout history.
  • Log a workout in memory and test what happens after a refresh.
Build the FitLog page structure

The HTML gives FitLog three visible areas. You get a daily dashboard, a workout form, and a history card.

  • Select `index.html` from the left file sidebar in Visual Studio Code.
  • Replace the temporary starter page with this document shell:
<!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="FitLog is a local browser-based workout tracker."
    />
    <title>FitLog</title>
    <link rel="stylesheet" href="./style.css" />
    <script type="module" src="./main.js"></script>
  </head>
  <body>
    <header class="hero">
      <p class="eyebrow">Local fitness diary</p>
      <h1>FitLog</h1>
      <p>Record a workout and watch today's progress grow.</p>
    </header>

    <main class="layout">
    </main>
  </body>
</html>

What does this structure do?

  • The document metadata gives FitLog a responsive viewport and a browser title.
  • The stylesheet link loads the visual rules from `style.css`.
  • The module script loads FitLog's behavior from `main.js`.
  • The hero introduces the app before the interactive cards appear.
  • Save `index.html`.
  • Return to the Safari tab from earlier.
  • Refresh the page to check the new structure.

You should see the FitLog heading with the text `Local fitness diary` above it.

Still seeing the starter page?

Confirm that you edited `index.html` inside the open `fitlog` folder. Check that the Vite development server from the previous step is still running.

Need another pair of eyes? Help me find why Safari still shows my old FitLog starter page.

The dashboard starts with placeholders because no workouts exist yet. Its labeled progress element gives the browser a meaningful representation of the 30-minute goal.

  • Find the empty `<main class="layout">` element in `index.html`.
  • Paste this dashboard section inside it:
      <section class="card dashboard" aria-labelledby="dashboard-title">
        <p class="eyebrow">Today</p>
        <h2 id="dashboard-title">Daily dashboard</h2>

        <div class="stats">
          <article class="stat">
            <span id="workout-count">0</span>
            <p>Workouts</p>
          </article>
          <article class="stat">
            <span id="minute-count">0</span>
            <p>Active minutes</p>
          </article>
          <article class="stat">
            <span id="calorie-count">0</span>
            <p>Calories</p>
          </article>
        </div>

        <label for="goal-progress">Daily minutes goal</label>
        <progress id="goal-progress" max="30" value="0">0 of 30 minutes</progress>
        <p id="goal-copy" class="goal-copy">0 of 30 minutes</p>
      </section>

How does the dashboard start?

  • The three statistics begin at `0` because the workout list is empty.
  • The progress element uses `30` as its maximum value.
  • The label connects the goal description to the progress indicator.
  • Save `index.html`.
  • Refresh Safari to check the dashboard.

You should see a Daily dashboard with zero workouts, zero active minutes, zero calories, and an empty 30-minute progress indicator.

Dashboard missing?

Make sure the dashboard section sits between the opening and closing `main` tags. A section pasted after `</html>` may render in an unexpected location.

If the structure still looks wrong, help me check where I pasted the FitLog dashboard section.

The workout form collects one complete record at a time. Each input has a visible label so the purpose of every value stays clear.

  • Place the cursor after the dashboard section in `index.html`.
  • Add the first half of the workout form with this code:
      <section class="card" aria-labelledby="form-title">
        <p class="eyebrow">New entry</p>
        <h2 id="form-title">Log a workout</h2>

        <form id="workout-form">
          <div class="form-grid">
            <label>
              Activity
              <select id="activity" required>
                <option value="Walk">Walk</option>
                <option value="Run">Run</option>
                <option value="Cycle">Cycle</option>
                <option value="Strength">Strength</option>
              </select>
            </label>

            <label>
              Date
              <input id="workout-date" type="date" required />
            </label>
          </div>
        </form>
      </section>

What does this form collect?

  • The activity menu limits entries to FitLog's four supported workout types.
  • The date input gives each workout a calendar date.
  • The `required` attributes stop the browser from submitting incomplete values.
  • Save `index.html`.
  • Refresh Safari to check the first two form controls.

You should see an Activity menu and a Date field under the Log a workout heading.

Form controls not visible?

Check that the form section sits after the dashboard section but before the closing `main` tag. Confirm that every opening `section` tag has a matching closing tag.

For help tracing the structure, help me find the mismatched tags in my FitLog form.

  • Find the closing Date label inside `#workout-form`.
  • Replace the remaining form-grid and form closing lines with this completed form ending:
            <label>
              Duration in minutes
              <input id="duration" type="number" min="1" required />
            </label>

            <label>
              Estimated calories
              <input id="calories" type="number" min="0" required />
            </label>
          </div>

          <button class="primary-button" type="submit">Log workout</button>
          <p id="form-status" class="status" role="status" aria-live="polite"></p>
        </form>

Why add a status region?

The duration and calorie fields store numeric workout details. The status region gives the learner immediate feedback after a successful submission.

Its live-region attributes allow assistive technology to announce updated feedback without moving focus.

  • Save `index.html`.
  • Refresh Safari to check the completed form.

You should see four labeled controls and a Log workout button.

Missing the submit button?

Check that the button sits after the closing `form-grid` div. Keep it inside `#workout-form` so submitting the form triggers JavaScript later.

If the form closes too early, help me repair the closing tags around my FitLog submit button.

Workout history needs a clear empty state before the first record exists. The list provides the destination for rows created by JavaScript.

  • Place the cursor after the workout form section in `index.html`.
  • Add the history card with this code:
      <section class="card history" aria-labelledby="history-title">
        <div class="section-heading">
          <div>
            <p class="eyebrow">Saved locally</p>
            <h2 id="history-title">Workout history</h2>
          </div>
          <button id="clear-all" class="secondary-button" type="button">Clear all</button>
        </div>

        <p id="empty-state" class="empty-state">No workouts yet. Add your first entry.</p>
        <ul id="workout-list" class="workout-list" aria-live="polite"></ul>
      </section>

How does history communicate state?

  • The empty-state message explains why the list has no rows.
  • The live workout list receives each rendered record.
  • The Clear all control is present for the complete interface.
  • Save `index.html`.
  • Refresh Safari to check the complete page structure.

You should see the dashboard, form, and Workout history card in that order. The history card should say `No workouts yet. Add your first entry.`

History card outside the page?

Confirm that the history section appears before `</main>`. Check the preceding form section for a missing `</section>` tag.

If the card placement is still off, help me inspect the section boundaries in index.html.

✔️ Awesome, I've got everything!

Your complete FitLog structure is in place. Double-check that `index.html` is saved.

ⓧ I'd like to double check the full code

<!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="FitLog is a local browser-based workout tracker."
    />
    <title>FitLog</title>
    <link rel="stylesheet" href="./style.css" />
    <script type="module" src="./main.js"></script>
  </head>
  <body>
    <header class="hero">
      <p class="eyebrow">Local fitness diary</p>
      <h1>FitLog</h1>
      <p>Record a workout and watch today's progress grow.</p>
    </header>

    <main class="layout">
      <section class="card dashboard" aria-labelledby="dashboard-title">
        <p class="eyebrow">Today</p>
        <h2 id="dashboard-title">Daily dashboard</h2>

        <div class="stats">
          <article class="stat">
            <span id="workout-count">0</span>
            <p>Workouts</p>
          </article>
          <article class="stat">
            <span id="minute-count">0</span>
            <p>Active minutes</p>
          </article>
          <article class="stat">
            <span id="calorie-count">0</span>
            <p>Calories</p>
          </article>
        </div>

        <label for="goal-progress">Daily minutes goal</label>
        <progress id="goal-progress" max="30" value="0">0 of 30 minutes</progress>
        <p id="goal-copy" class="goal-copy">0 of 30 minutes</p>
      </section>

      <section class="card" aria-labelledby="form-title">
        <p class="eyebrow">New entry</p>
        <h2 id="form-title">Log a workout</h2>

        <form id="workout-form">
          <div class="form-grid">
            <label>
              Activity
              <select id="activity" required>
                <option value="Walk">Walk</option>
                <option value="Run">Run</option>
                <option value="Cycle">Cycle</option>
                <option value="Strength">Strength</option>
              </select>
            </label>

            <label>
              Date
              <input id="workout-date" type="date" required />
            </label>

            <label>
              Duration in minutes
              <input id="duration" type="number" min="1" required />
            </label>

            <label>
              Estimated calories
              <input id="calories" type="number" min="0" required />
            </label>
          </div>

          <button class="primary-button" type="submit">Log workout</button>
          <p id="form-status" class="status" role="status" aria-live="polite"></p>
        </form>
      </section>

      <section class="card history" aria-labelledby="history-title">
        <div class="section-heading">
          <div>
            <p class="eyebrow">Saved locally</p>
            <h2 id="history-title">Workout history</h2>
          </div>
          <button id="clear-all" class="secondary-button" type="button">Clear all</button>
        </div>

        <p id="empty-state" class="empty-state">No workouts yet. Add your first entry.</p>
        <ul id="workout-list" class="workout-list" aria-live="polite"></ul>
      </section>
    </main>
  </body>
</html>
Style the responsive FitLog interface

The page structure now contains every control FitLog needs. CSS turns that raw structure into readable cards that adapt to smaller screens.

  • Use Visual Studio Code's left file sidebar to create `style.css` inside the open `fitlog` folder.
  • Add the page colors and sizing rules with this code:
:root {
  font-family: system-ui, sans-serif;
  color: #172033;
  background: #f2f6f3;
  font-synthesis: none;
}

* {
  box-sizing: border-box;
}

body {
  min-width: 320px;
  min-height: 100vh;
  margin: 0;
  background:
    radial-gradient(circle at top left, #d9f6e5 0, transparent 32rem),
    #f2f6f3;
}

What do the foundation styles control?

These rules set FitLog's typography, colors, minimum page size, and background. Universal border-box sizing keeps element dimensions predictable.

  • Save `style.css`.
  • Refresh Safari to check the page background.

You should see a pale green page with a soft green glow in the top-left corner.

Background still white?

Check that `style.css` sits beside `index.html`. Confirm that the stylesheet link in `index.html` still points to `./style.css`.

If the stylesheet is not loading, help me connect style.css to my FitLog page.

  • Add the shared control and page-width styles below the existing rules:
button,
input,
select {
  font: inherit;
}

button {
  cursor: pointer;
}

button:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

.hero,
.layout {
  width: min(1080px, calc(100% - 2rem));
  margin-inline: auto;
}

.hero {
  padding: 3.5rem 0 1.5rem;
}

Why share these rules?

The form controls inherit the page font for a consistent interface. The shared width keeps the hero and cards aligned within the browser window.

  • Save `style.css`.
  • Refresh Safari to check the page width.

You should see the hero and main content aligned with matching space on both sides.

Content touching the browser edge?

Check the comma between `.hero` and `.layout`. Confirm that the shared rule includes both selectors.

For help with the selector, help me fix the shared width on my FitLog layout.

  • Add the hero typography below the current styles:
.hero h1,
.card h2,
.hero p {
  margin-top: 0;
}

.hero h1 {
  margin-bottom: 0.35rem;
  font-size: clamp(2.5rem, 8vw, 5rem);
  line-height: 0.95;
}

.hero > p:last-child {
  max-width: 34rem;
  color: #52605a;
  font-size: 1.05rem;
}

.eyebrow {
  margin-bottom: 0.45rem;
  color: #19724a;
  font-size: 0.78rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

How does the hero create hierarchy?

The large FitLog title anchors the page. Smaller uppercase eyebrow text labels each section without competing with its heading.

  • Save `style.css`.
  • Refresh Safari to check the hero typography.

You should see a large FitLog title with smaller green uppercase labels across the page.

Hero text unchanged?

Check the spelling of `.hero` and `.eyebrow` in both files. CSS class names must match the HTML values exactly.

If the selectors do not match, help me compare my FitLog hero classes.

  • Add the card grid and dashboard layout below the hero rules:
.layout {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
  padding-bottom: 3rem;
}

.card {
  padding: 1.4rem;
  border: 1px solid #d8e2dc;
  border-radius: 1rem;
  background: rgba(255, 255, 255, 0.94);
  box-shadow: 0 1rem 2.5rem rgba(23, 32, 51, 0.07);
}

.dashboard,
.history {
  grid-column: 1 / -1;
}

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

How are the cards arranged?

The main layout uses two columns on wider screens. The dashboard and history span both columns while the statistics form a three-column row.

  • Save `style.css`.
  • Refresh Safari to check the card layout.

You should see white rounded cards with the dashboard and history stretching across the page.

Cards still look like plain text?

Confirm that every page section includes the `card` class. Check that the `.card` rule begins with a full stop.

If the cards remain unstyled, help me match my card classes and selectors.

  • Add the statistic and progress styles below the layout rules:
.stat {
  padding: 1rem;
  border-radius: 0.8rem;
  background: #edf8f1;
}

.stat span {
  display: block;
  color: #125f3c;
  font-size: 1.8rem;
  font-weight: 800;
}

.stat p {
  margin: 0.2rem 0 0;
  color: #52605a;
}

progress {
  width: 100%;
  height: 1rem;
  margin-top: 0.55rem;
  accent-color: #1d8b57;
}

What makes the totals readable?

Each total sits inside a tinted statistic tile. Larger numbers and a full-width progress indicator make today's activity easy to scan.

  • Save `style.css`.
  • Refresh Safari to check the dashboard statistics.

You should see three green-tinted statistic tiles above a full-width progress indicator.

Statistics not forming tiles?

Check that each statistic uses `class="stat"` in `index.html`. Confirm that the `.stat` rule has its closing brace.

For help checking the tiles, help me connect my FitLog stat HTML to its CSS.

  • Add the goal text and form-grid styles below the progress rules:
.goal-copy,
.status {
  color: #52605a;
}

.goal-copy {
  margin: 0.35rem 0 0;
}

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

label {
  display: grid;
  gap: 0.35rem;
  color: #34423b;
  font-weight: 700;
}

How does the form grid help?

The four fields form two columns on wider screens. Grid-based labels keep each field name directly above its matching control.

  • Save `style.css`.
  • Refresh Safari to check the form arrangement.

You should see the four workout fields arranged as a tidy two-column grid.

Form still in one column?

Confirm that the field labels remain inside `<div class="form-grid">`. Check that `.form-grid` uses the exact same class name.

If the grid does not apply, help me inspect my FitLog form-grid structure.

  • Add the input and keyboard-focus styles below the label rules:
input,
select {
  width: 100%;
  padding: 0.75rem 0.8rem;
  border: 1px solid #b9c8bf;
  border-radius: 0.65rem;
  background: #ffffff;
  color: #172033;
}

input:focus,
select:focus,
button:focus-visible {
  outline: 3px solid rgba(29, 139, 87, 0.25);
  outline-offset: 2px;
}

Why include visible focus?

The controls receive consistent spacing and rounded borders. A visible outline shows keyboard users which control currently has focus.

  • Save `style.css`.
  • Refresh Safari to check the form controls.
  • Press the Tab key to move focus through the form.

You should see a soft green outline around the focused field or button.

Focus outline missing?

Use the Tab key so Safari applies keyboard focus. Check that the focus selectors include the colon before `focus` and `focus-visible`.

If focus remains hard to see, help me debug the focus styles in FitLog.

  • Add the three button styles below the focus rules:
.primary-button,
.secondary-button,
.delete-button {
  border: 0;
  border-radius: 0.65rem;
  font-weight: 800;
}

.primary-button {
  width: 100%;
  padding: 0.8rem 1rem;
  background: #1d8b57;
  color: #ffffff;
}

.secondary-button,
.delete-button {
  padding: 0.6rem 0.75rem;
  background: #e8efeb;
  color: #324039;
}

.delete-button {
  color: #8d2d2d;
}

How do the button roles differ?

The green primary button emphasizes workout submission. Secondary controls use a quieter background so they do not compete with the main action.

  • Save `style.css`.
  • Refresh Safari to check the button styling.

You should see a full-width green Log workout button and a smaller gray Clear all button.

Buttons still use browser defaults?

Confirm that the submit button uses `primary-button`. Check that Clear all uses `secondary-button`.

If the class names match, help me find why my FitLog button styles are not applying.

  • Add the status and history-layout styles below the button rules:
.status {
  min-height: 1.25rem;
  margin-bottom: 0;
}

.section-heading,
.workout-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.workout-list {
  display: grid;
  gap: 0.7rem;
  margin: 1rem 0 0;
  padding: 0;
  list-style: none;
}

How is history prepared?

The history heading places its title and Clear all control on one row. The list removes default bullets and reserves consistent space between workout rows.

  • Save `style.css`.
  • Refresh Safari to check the history heading.

You should see Workout history on the left with Clear all aligned on the right.

Clear all below the heading?

Check that the heading content and button both sit inside `<div class="section-heading">`. Confirm that the flex rule includes `.section-heading`.

If alignment still fails, help me inspect my FitLog history heading layout.

  • Add the workout-row and empty-state styles below the history list:
.workout-item {
  padding: 0.9rem 1rem;
  border: 1px solid #dce6e0;
  border-radius: 0.75rem;
}

.workout-info {
  display: grid;
  gap: 0.2rem;
}

.workout-meta,
.empty-state {
  color: #64716b;
}

.empty-state {
  margin-bottom: 0;
}

[hidden] {
  display: none !important;
}

What will each history row show?

Every workout row gets its own border and spacing. The metadata uses a quieter color while the hidden rule lets JavaScript switch the empty message off.

  • Save `style.css`.
  • Refresh Safari to check the empty-state text.

You should see the empty-state message in a muted gray color beneath the history heading.

Empty message styling missing?

Check that the paragraph uses `class="empty-state"`. Confirm that the CSS selector includes the full class name.

If the message remains unchanged, help me match the FitLog empty-state class.

The desktop grid needs a smaller-screen fallback. The final media rule turns every multi-column area into a single column when space becomes tight.

  • Add the mobile layout at the bottom of `style.css`:
@media (max-width: 720px) {
  .layout,
  .form-grid,
  .stats {
    grid-template-columns: 1fr;
  }

  .section-heading,
  .workout-item {
    align-items: stretch;
    flex-direction: column;
  }

  .secondary-button,
  .delete-button {
    width: 100%;
  }
}

What changes on a narrow screen?

Cards, form fields, and statistics collapse into one column. History rows stack vertically while their secondary controls stretch across the available width.

  • Save `style.css`.
  • Narrow the Safari window to test the mobile layout.

You should see one column of cards, vertically stacked statistics, and full-width form controls.

Layout not adapting?

Confirm that the media rule sits after the desktop styles. Check that its opening and closing braces surround every mobile selector.

If the page still overflows, help me debug the FitLog mobile media rule.

✔️ Awesome, I've got everything!

Your FitLog layout is styled for wide and narrow screens. Double-check that `style.css` is saved.

ⓧ I'd like to double check the full code

:root {
  font-family: system-ui, sans-serif;
  color: #172033;
  background: #f2f6f3;
  font-synthesis: none;
}

* {
  box-sizing: border-box;
}

body {
  min-width: 320px;
  min-height: 100vh;
  margin: 0;
  background:
    radial-gradient(circle at top left, #d9f6e5 0, transparent 32rem),
    #f2f6f3;
}

button,
input,
select {
  font: inherit;
}

button {
  cursor: pointer;
}

button:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

.hero,
.layout {
  width: min(1080px, calc(100% - 2rem));
  margin-inline: auto;
}

.hero {
  padding: 3.5rem 0 1.5rem;
}

.hero h1,
.card h2,
.hero p {
  margin-top: 0;
}

.hero h1 {
  margin-bottom: 0.35rem;
  font-size: clamp(2.5rem, 8vw, 5rem);
  line-height: 0.95;
}

.hero > p:last-child {
  max-width: 34rem;
  color: #52605a;
  font-size: 1.05rem;
}

.eyebrow {
  margin-bottom: 0.45rem;
  color: #19724a;
  font-size: 0.78rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

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

.card {
  padding: 1.4rem;
  border: 1px solid #d8e2dc;
  border-radius: 1rem;
  background: rgba(255, 255, 255, 0.94);
  box-shadow: 0 1rem 2.5rem rgba(23, 32, 51, 0.07);
}

.dashboard,
.history {
  grid-column: 1 / -1;
}

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

.stat {
  padding: 1rem;
  border-radius: 0.8rem;
  background: #edf8f1;
}

.stat span {
  display: block;
  color: #125f3c;
  font-size: 1.8rem;
  font-weight: 800;
}

.stat p {
  margin: 0.2rem 0 0;
  color: #52605a;
}

progress {
  width: 100%;
  height: 1rem;
  margin-top: 0.55rem;
  accent-color: #1d8b57;
}

.goal-copy,
.status {
  color: #52605a;
}

.goal-copy {
  margin: 0.35rem 0 0;
}

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

label {
  display: grid;
  gap: 0.35rem;
  color: #34423b;
  font-weight: 700;
}

input,
select {
  width: 100%;
  padding: 0.75rem 0.8rem;
  border: 1px solid #b9c8bf;
  border-radius: 0.65rem;
  background: #ffffff;
  color: #172033;
}

input:focus,
select:focus,
button:focus-visible {
  outline: 3px solid rgba(29, 139, 87, 0.25);
  outline-offset: 2px;
}

.primary-button,
.secondary-button,
.delete-button {
  border: 0;
  border-radius: 0.65rem;
  font-weight: 800;
}

.primary-button {
  width: 100%;
  padding: 0.8rem 1rem;
  background: #1d8b57;
  color: #ffffff;
}

.secondary-button,
.delete-button {
  padding: 0.6rem 0.75rem;
  background: #e8efeb;
  color: #324039;
}

.delete-button {
  color: #8d2d2d;
}

.status {
  min-height: 1.25rem;
  margin-bottom: 0;
}

.section-heading,
.workout-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.workout-list {
  display: grid;
  gap: 0.7rem;
  margin: 1rem 0 0;
  padding: 0;
  list-style: none;
}

.workout-item {
  padding: 0.9rem 1rem;
  border: 1px solid #dce6e0;
  border-radius: 0.75rem;
}

.workout-info {
  display: grid;
  gap: 0.2rem;
}

.workout-meta,
.empty-state {
  color: #64716b;
}

.empty-state {
  margin-bottom: 0;
}

[hidden] {
  display: none !important;
}

@media (max-width: 720px) {
  .layout,
  .form-grid,
  .stats {
    grid-template-columns: 1fr;
  }

  .section-heading,
  .workout-item {
    align-items: stretch;
    flex-direction: column;
  }

  .secondary-button,
  .delete-button {
    width: 100%;
  }
}
Render the first workout in memory

The form needs JavaScript to turn its input values into a workout object. An in-memory array gives the page a source for rendering history during the current session.

  • Use Visual Studio Code's left file sidebar to create `main.js` inside the open `fitlog` folder.
  • Add the DOM references and initial in-memory history with this code:
const form = document.querySelector("#workout-form");
const activityInput = document.querySelector("#activity");
const dateInput = document.querySelector("#workout-date");
const durationInput = document.querySelector("#duration");
const caloriesInput = document.querySelector("#calories");
const statusMessage = document.querySelector("#form-status");
const workoutList = document.querySelector("#workout-list");
const emptyState = document.querySelector("#empty-state");
const clearAllButton = document.querySelector("#clear-all");

let workouts = [];

function renderHistory() {
  workoutList.textContent = "";
  emptyState.hidden = workouts.length > 0;
  clearAllButton.disabled = workouts.length === 0;
}

renderHistory();

What does this starting state do?

  • The DOM references connect JavaScript to the form, feedback area, empty message, list, and Clear all control.
  • The `workouts` array begins empty because no records exist in the current session.
  • The first render keeps the empty message visible and disables Clear all.
  • Save `main.js`.
  • Refresh Safari to run the initial render.

You should see the Clear all button become faded and unavailable while the history is empty.

Clear all still active?

Check that `main.js` sits beside `index.html`. Confirm that the module script in `index.html` points to `./main.js`.

If JavaScript is not running, help me connect main.js to my FitLog page.

Submitting the form should prevent a page navigation, collect the four values, and place one workout at the front of the array. A small date formatter makes each saved date easier to read.

  • Place the submit handler after `let workouts = [];` in `main.js`.
  • Place the date formatter after the submit handler by adding this code:
form.addEventListener("submit", (event) => {
  event.preventDefault();

  const workout = {
    activity: activityInput.value,
    date: dateInput.value,
    duration: Number(durationInput.value),
    calories: Number(caloriesInput.value),
  };

  workouts.unshift(workout);
  renderHistory();

  form.reset();
  statusMessage.textContent = `${workout.activity} workout logged.`;
});

function formatDate(dateString) {
  return new Date(`${dateString}T00:00:00`).toLocaleDateString(undefined, {
    year: "numeric",
    month: "short",
    day: "numeric",
  });
}

How does submission become data?

  • The submit handler stops the browser from navigating away from FitLog.
  • The workout object groups the activity, date, duration, and calories into one record.
  • Numeric conversion keeps duration and calories as numbers.
  • The newest workout moves to the front of the array before the interface renders again.
  • Save `main.js`.
  • Refresh Safari to load the submit handler.
  • Select `Walk` from the Activity menu.
  • Choose today's date in the Date field.
  • Enter `20` in Duration in minutes.
  • Enter `120` in Estimated calories.
  • Click Log workout to test the handler.

You should see `Walk workout logged.` beneath the form. The empty message disappears because the array now contains one workout.

Form refreshes the whole page?

Check that the form uses `id="workout-form"`. Confirm that `form.addEventListener` includes `event.preventDefault()` before the workout object.

If submission still navigates away, help me debug the FitLog submit handler.

The handler now captures a workout, but history still needs to create a row for every object in the array. The renderer builds those rows from the current state each time it runs.

  • Find the existing `renderHistory()` function in `main.js`.
  • Replace that function with this completed renderer:
function renderHistory() {
  workoutList.textContent = "";
  emptyState.hidden = workouts.length > 0;
  clearAllButton.disabled = workouts.length === 0;

  workouts.forEach((workout) => {
    const item = document.createElement("li");
    const info = document.createElement("div");
    const title = document.createElement("strong");
    const meta = document.createElement("span");

    item.className = "workout-item";
    info.className = "workout-info";
    meta.className = "workout-meta";

    title.textContent = workout.activity;
    meta.textContent = `${formatDate(workout.date)} | ${workout.duration} min | ${workout.calories} cal`;

    info.append(title, meta);
    item.append(info);
    workoutList.appendChild(item);
  });
}

How does the renderer build history?

  • The renderer clears old list content before rebuilding the current view.
  • Each workout becomes a list item with a title and metadata line.
  • The metadata combines the formatted date, duration, and calories.
  • The row classes connect each generated element to the styles you added earlier.
  • Save `main.js`.
  • Refresh Safari to load the completed renderer.
  • Select `Walk` from the Activity menu.
  • Choose today's date in the Date field.
  • Enter `20` in Duration in minutes.
  • Enter `120` in Estimated calories.
  • Click Log workout to render the record.

You should see a bordered Walk row in Workout history. Its metadata should show the selected date, `20 min`, and `120 cal`.

Workout row missing?

Check that `renderHistory()` still runs after `workouts.unshift(workout)`. Confirm that the list uses `id="workout-list"` in `index.html`.

If the status changes but no row appears, help me debug the FitLog history renderer.

Before you refresh, do you think the workout row can survive when its only source is the current in-memory array?

  • Refresh the open FitLog page in Safari.

The Walk row disappears. You should see `No workouts yet. Add your first entry.` again because the refreshed script creates a new empty `workouts` array.

Why did the workout disappear?

The array exists only while this page instance is running. Refreshing the page starts the JavaScript again, so `let workouts = [];` creates fresh empty state.

That disappearing row is useful evidence. You have found the exact persistence gap FitLog needs to solve next.

✔️ Awesome, I've got everything!

Your in-memory FitLog can submit and render workouts. Double-check that `main.js` is saved.

ⓧ I'd like to double check the full code

const form = document.querySelector("#workout-form");
const activityInput = document.querySelector("#activity");
const dateInput = document.querySelector("#workout-date");
const durationInput = document.querySelector("#duration");
const caloriesInput = document.querySelector("#calories");
const statusMessage = document.querySelector("#form-status");
const workoutList = document.querySelector("#workout-list");
const emptyState = document.querySelector("#empty-state");
const clearAllButton = document.querySelector("#clear-all");

let workouts = [];

form.addEventListener("submit", (event) => {
  event.preventDefault();

  const workout = {
    activity: activityInput.value,
    date: dateInput.value,
    duration: Number(durationInput.value),
    calories: Number(caloriesInput.value),
  };

  workouts.unshift(workout);
  renderHistory();

  form.reset();
  statusMessage.textContent = `${workout.activity} workout logged.`;
});

function formatDate(dateString) {
  return new Date(`${dateString}T00:00:00`).toLocaleDateString(undefined, {
    year: "numeric",
    month: "short",
    day: "numeric",
  });
}

function renderHistory() {
  workoutList.textContent = "";
  emptyState.hidden = workouts.length > 0;
  clearAllButton.disabled = workouts.length === 0;

  workouts.forEach((workout) => {
    const item = document.createElement("li");
    const info = document.createElement("div");
    const title = document.createElement("strong");
    const meta = document.createElement("span");

    item.className = "workout-item";
    info.className = "workout-info";
    meta.className = "workout-meta";

    title.textContent = workout.activity;
    meta.textContent = `${formatDate(workout.date)} | ${workout.duration} min | ${workout.calories} cal`;

    info.append(title, meta);
    item.append(info);
    workoutList.appendChild(item);
  });
}

renderHistory();

Your responsive tracker now turns form input into visible workout history. Next, you will preserve that history so a Safari refresh can rebuild the same rows.

Persist Workout History

Your first FitLog workout appeared in the history list. Refreshing Safari then erased the row from memory.

The disappearing entry proves that memory alone is insufficient. FitLog needs storage that survives a page refresh.

In this step, you'll save the workout array in the browser. FitLog will restore that saved array whenever the page loads.

In this step, get ready to:
  • Create helpers that read and write workout data.
  • Connect browser storage to the workout flow.
  • Refresh FitLog and prove that the workout history returns.
Create the storage helpers

Web Storage gives each browser origin a local key-value store. FitLog uses localStorage under http://localhost:5173.

The store accepts strings. JSON serialization converts the workout array into a string that the browser can save.

  • Find the first DOM reference in main.js:
const form = document.querySelector("#workout-form");

What are you locating?

This line marks the beginning of the DOM references. The storage key belongs directly above it so configuration stays easy to find.

  • Replace that line with these two lines:
const STORAGE_KEY = "fitlog.workouts";
const form = document.querySelector("#workout-form");

What does the storage key do?

The STORAGE_KEY constant gives FitLog one consistent name for its saved workout array. Every read or write uses the same key.

  • Save main.js.
  • Refresh Safari.

FitLog should reload with the form and history card in place. Good, the new storage key has not disrupted the running app.

Did FitLog stop loading?

  • Check that STORAGE_KEY uses uppercase letters in both words.
  • Check that the value includes matching quotation marks around fitlog.workouts.

Still stuck? Help me check the new storage key.

FitLog now has a key for its data. The next helpers use that key to load a valid array or save the current one.

  • Add the storage helpers below the form submit handler in main.js using this code:
function loadWorkouts() {
  const storedWorkouts = window.localStorage.getItem(STORAGE_KEY);

  if (!storedWorkouts) {
    return [];
  }

  try {
    const parsedWorkouts = JSON.parse(storedWorkouts);
    return Array.isArray(parsedWorkouts) ? parsedWorkouts : [];
  } catch {
    return [];
  }
}

function saveWorkouts() {
  window.localStorage.setItem(STORAGE_KEY, JSON.stringify(workouts));
}

What do these helpers do?

  • loadWorkouts() reads the saved string with window.localStorage.getItem(STORAGE_KEY).
  • JSON.parse() converts that string back into JavaScript data.
  • Array.isArray() accepts valid workout arrays. Missing or invalid data produces an empty array.
  • saveWorkouts() converts the current array with JSON.stringify(workouts) before saving it.
  • Save main.js.
  • Refresh Safari.

You should see the FitLog page load normally. This confirms that both helper declarations have valid syntax.

Seeing a blank page?

  • Check that each helper has its closing brace.
  • Check that JSON.parse(storedWorkouts) appears inside the try block.
  • Check that JSON.stringify(workouts) appears inside saveWorkouts().

Need another pair of eyes? Help me debug the storage helpers.

Connect storage to the workout flow

The helpers only affect FitLog when the app calls them. Startup must load the stored array before the history renderer runs.

  • Find the existing let workouts = []; line in main.js.
  • Replace it using this line:
let workouts = loadWorkouts();

How does startup restoration work?

JavaScript calls loadWorkouts() while creating the workouts variable. The history renderer then receives the restored array when the page starts.

  • Save main.js.
  • Refresh Safari.

FitLog should reload with its empty-state message. The startup path can now handle an origin with no saved workouts.

Does the page fail during startup?

  • Check that loadWorkouts has matching uppercase letters in its declaration and call.
  • Check that the line ends with parentheses and a full stop character in the code.

Still seeing a problem? Help me debug the startup call.

Startup can now restore saved records. Each form submission must also update the stored copy.

  • Find workouts.unshift(workout); inside the form submit handler.
  • Add this call directly below it:
saveWorkouts();

Why save after inserting?

The new workout enters the array before saveWorkouts() runs. The serialized string therefore includes the latest submission.

  • Save main.js.
  • Select Walk from the Activity field.
  • Select today's date in the Date field.
  • Enter 20 in the Duration in minutes field.
  • Enter 120 in the Estimated calories field.
  • Click Log workout.

You should see a Walk row containing today's date. The row should also show 20 min and 120 cal.

Is the workout missing?

  • Check that saveWorkouts(); sits below workouts.unshift(workout);.
  • Check that every required form field contains a value.
  • Check that the Log workout button produced a status message.

Need help with the submission? Help me trace the save flow.

Prove the history survives a refresh

A successful submission proves that the current page still works. Refreshing the page tests whether the saved string can rebuild the workout array.

Before you refresh, do you expect the Walk row to disappear or return?

  • Refresh Safari.

You should see the same Walk row return with today's date. You should also see 20 min and 120 cal.

That is the persistence loop working. FitLog now keeps workout history across page refreshes.

What proved persistence?

Refreshing Safari created a new JavaScript runtime with fresh memory. loadWorkouts() rebuilt the array from the string saved under fitlog.workouts.

The restored row came from browser data for the localhost origin. FitLog can now recover its history whenever it starts.

Did the row disappear again?

  • Check that let workouts = loadWorkouts(); replaces the old empty-array initialization.
  • Check that saveWorkouts(); runs after the new workout enters the array.
  • Check that the refreshed page still uses http://localhost:5173.

Still losing the row? Help me diagnose FitLog persistence.

✔️ Awesome, I've got everything!

Your persistent main.js is complete.

ⓧ I'd like to double check the full code

const STORAGE_KEY = "fitlog.workouts";

const form = document.querySelector("#workout-form");
const activityInput = document.querySelector("#activity");
const dateInput = document.querySelector("#workout-date");
const durationInput = document.querySelector("#duration");
const caloriesInput = document.querySelector("#calories");
const statusMessage = document.querySelector("#form-status");
const workoutList = document.querySelector("#workout-list");
const emptyState = document.querySelector("#empty-state");
const clearAllButton = document.querySelector("#clear-all");

let workouts = loadWorkouts();

form.addEventListener("submit", (event) => {
  event.preventDefault();

  const workout = {
    activity: activityInput.value,
    date: dateInput.value,
    duration: Number(durationInput.value),
    calories: Number(caloriesInput.value),
  };

  workouts.unshift(workout);
  saveWorkouts();
  renderHistory();

  form.reset();
  statusMessage.textContent = `${workout.activity} workout logged.`;
});

function loadWorkouts() {
  const storedWorkouts = window.localStorage.getItem(STORAGE_KEY);

  if (!storedWorkouts) {
    return [];
  }

  try {
    const parsedWorkouts = JSON.parse(storedWorkouts);
    return Array.isArray(parsedWorkouts) ? parsedWorkouts : [];
  } catch {
    return [];
  }
}

function saveWorkouts() {
  window.localStorage.setItem(STORAGE_KEY, JSON.stringify(workouts));
}

function formatDate(dateString) {
  return new Date(`${dateString}T00:00:00`).toLocaleDateString(undefined, {
    year: "numeric",
    month: "short",
    day: "numeric",
  });
}

function renderHistory() {
  workoutList.textContent = "";
  emptyState.hidden = workouts.length > 0;
  clearAllButton.disabled = workouts.length === 0;

  workouts.forEach((workout) => {
    const item = document.createElement("li");
    const info = document.createElement("div");
    const title = document.createElement("strong");
    const meta = document.createElement("span");

    item.className = "workout-item";
    info.className = "workout-info";
    meta.className = "workout-meta";

    title.textContent = workout.activity;
    meta.textContent = `${formatDate(workout.date)} | ${workout.duration} min | ${workout.calories} cal`;

    info.append(title, meta);
    item.append(info);
    workoutList.appendChild(item);
  });
}

renderHistory();

Your workout log now survives refreshes. Next, you'll turn those saved records into today's totals and goal progress.

Build the Daily Dashboard

Your saved workouts now return after a refresh because localStorage gives FitLog a persistent source of data. The next challenge is turning those records into information you can use today.

This step adds derived metrics for workout count. It also calculates active minutes. You will calculate calories and progress toward a 30-minute goal. You will finish with controls that correct or remove stored data.

In this step, get ready to:
  • Calculate today's dashboard totals and goal progress.
  • Delete an individual workout from saved history.
  • Clear every workout from FitLog.
Calculate today's totals

The dashboard should include only workouts dated today. A local date key lets FitLog compare each saved workout with the date on your Mac.

  • Switch back to Visual Studio Code.
  • Select main.js in the Explorer sidebar.
  • Add the daily target immediately below the STORAGE_KEY declaration using this reference:
const DAILY_GOAL = 30;

What does this value control?

The DAILY_GOAL constant gives the progress indicator a 30-minute target. Keeping the target in one constant prevents different parts of the dashboard from using conflicting values.

  • Add these dashboard references immediately below the clearAllButton declaration:
const workoutCount = document.querySelector("#workout-count");
const minuteCount = document.querySelector("#minute-count");
const calorieCount = document.querySelector("#calorie-count");
const goalProgress = document.querySelector("#goal-progress");
const goalCopy = document.querySelector("#goal-copy");

What do these references connect?

  • The three count references connect JavaScript to the dashboard's numeric placeholders.
  • The progress references connect JavaScript to the visual goal bar and its readable text.
  • Add this local date helper immediately below saveWorkouts() using the following code:
function getLocalDateKey() {
  const today = new Date();
  const year = today.getFullYear();
  const month = String(today.getMonth() + 1).padStart(2, "0");
  const day = String(today.getDate()).padStart(2, "0");
  return `${year}-${month}-${day}`;
}

How does the date key work?

  • The helper reads the year from your local date.
  • The zero-based month receives one before being padded to two digits.
  • The returned value matches the date format stored by the workout form.
  • Add this date initialization immediately below let workouts = loadWorkouts(); using the following line:
dateInput.value = getLocalDateKey();

Why set the date automatically?

The date field now starts with today's local date. This makes a workout eligible for the daily dashboard without requiring you to find today's date manually.

  • Save main.js.
  • Return to the existing FitLog page in Safari.

You should see today's date in the Date field.

Date field still empty?

  • Check that getLocalDateKey uses the same spelling in the function declaration and the date assignment.
  • Confirm the assignment appears after the dateInput declaration.

Still stuck? Help me fix the empty FitLog date field.

With today's date available, FitLog can derive the dashboard from the saved array. The original workouts array remains unchanged during this calculation.

  • Add renderSummary() immediately above renderHistory() using this code:
function renderSummary() {
  const todayKey = getLocalDateKey();
  const todaysWorkouts = workouts.filter((workout) => workout.date === todayKey);
  const totalMinutes = todaysWorkouts.reduce(
    (total, workout) => total + workout.duration,
    0,
  );
  const totalCalories = todaysWorkouts.reduce(
    (total, workout) => total + workout.calories,
    0,
  );

  workoutCount.textContent = todaysWorkouts.length;
  minuteCount.textContent = totalMinutes;
  calorieCount.textContent = totalCalories;
  goalProgress.value = Math.min(totalMinutes, DAILY_GOAL);
  goalCopy.textContent = `${totalMinutes} of ${DAILY_GOAL} minutes`;
}

What does this function calculate?

  • The filtered array contains only workouts whose stored date matches today's local date.
  • The two reductions total minutes and calories across those workouts.
  • The progress value stops at DAILY_GOAL while the text continues to show the complete minute total.
  • Replace the history-only call at the bottom of main.js with this shared render function:
function render() {
  renderSummary();
  renderHistory();
}

render();

Why use one render function?

The render() function refreshes the dashboard and history from the same workouts array. Every later data change can now update both parts of FitLog through one call.

  • Save main.js.
  • Return to Safari.

You should see the dashboard showing the totals for workouts dated today. An empty history produces zero workouts with zero minutes and zero calories.

  • Replace the existing form submission handler with this completed version:
form.addEventListener("submit", (event) => {
  event.preventDefault();

  const workout = {
    activity: activityInput.value,
    date: dateInput.value,
    duration: Number(durationInput.value),
    calories: Number(caloriesInput.value),
  };

  workouts.unshift(workout);
  saveWorkouts();
  render();

  form.reset();
  dateInput.value = getLocalDateKey();
  statusMessage.textContent = `${workout.activity} workout logged.`;
});

What changed in the submit flow?

  • The handler saves the new workout before calling render().
  • The shared render call updates the history and dashboard from the latest array.
  • The date returns to today's local value after the form resets.

Before you log a workout, which dashboard values do you expect to change when its date is today?

  • Select Walk in the Activity field.
  • Keep today's value in the Date field.
  • Enter 20 in the Duration in minutes field.
  • Enter 120 in the Estimated calories field.
  • Click Log workout.

You should see one workout with 20 active minutes and 120 calories. The goal copy should read 20 of 30 minutes.

That is the dashboard working. One form submission now updates the saved history and today's progress together.

Delete one workout

Saved workout data sometimes needs correcting. Each Delete button must remove the selected object from the original array before FitLog saves and renders again.

  • Add formatDate() immediately below getLocalDateKey() using this code:
function formatDate(dateString) {
  return new Date(`${dateString}T00:00:00`).toLocaleDateString(undefined, {
    year: "numeric",
    month: "short",
    day: "numeric",
  });
}

Why format stored dates?

Workout objects keep a stable date string for comparisons. The formatter converts that value into a readable date when FitLog builds a history row.

  • Find the meta.textContent assignment inside renderHistory().
  • Replace that assignment with this line:
meta.textContent = `${formatDate(workout.date)} | ${workout.duration} min | ${workout.calories} cal`;

What changes in history?

Each row now displays a readable date beside its duration and calorie estimate. The stored date remains suitable for dashboard comparisons.

  • Save main.js.
  • Return to Safari.

The saved workout should now show a readable local date in its history row.

  • Add deleteWorkout() immediately above renderHistory() using this code:
function deleteWorkout(workout) {
  const workoutIndex = workouts.indexOf(workout);

  if (workoutIndex === -1) {
    return;
  }

  workouts.splice(workoutIndex, 1);
  saveWorkouts();
  statusMessage.textContent = `${workout.activity} workout deleted.`;
  render();
}

How does deletion stay accurate?

  • The function finds the selected object inside the original workouts array.
  • The guard stops if that object is no longer present.
  • A successful deletion saves the shortened array before refreshing the interface.
  • Replace the existing workouts.forEach() section inside renderHistory() with this completed row builder:
workouts.forEach((workout) => {
    const item = document.createElement("li");
    const info = document.createElement("div");
    const title = document.createElement("strong");
    const meta = document.createElement("span");
    const deleteButton = document.createElement("button");

    item.className = "workout-item";
    info.className = "workout-info";
    meta.className = "workout-meta";
    deleteButton.className = "delete-button";
    deleteButton.type = "button";

    title.textContent = workout.activity;
    meta.textContent = `${formatDate(workout.date)} | ${workout.duration} min | ${workout.calories} cal`;
    deleteButton.textContent = "Delete";
    deleteButton.setAttribute(
      "aria-label",
      `Delete ${workout.activity} workout from ${formatDate(workout.date)}`,
    );
    deleteButton.addEventListener("click", () => deleteWorkout(workout));

    info.append(title, meta);
    item.append(info, deleteButton);
    workoutList.appendChild(item);
  });

What does the row builder add?

  • Each workout receives its own Delete button.
  • The accessible label identifies the activity and date controlled by the button.
  • The click listener passes the selected workout object to deleteWorkout().
  • Save main.js.
  • Return to Safari.
  • Click Delete beside the workout you logged.

The selected row should disappear. Today's workout count should decrease by one. Its minutes and calories should also be removed from the dashboard.

Delete button not working?

  • Check that the button listener passes workout into deleteWorkout().
  • Confirm that saveWorkouts() runs after workouts.splice().
  • Confirm that render() runs at the end of a successful deletion.

Need a hand? Help me debug the FitLog Delete button.

Clear all workout data

Individual deletion fixes one record. Clear all provides a complete reset by emptying application state and removing FitLog's saved storage entry.

  • Add this click listener immediately below the form submission handler:
clearAllButton.addEventListener("click", () => {
  workouts = [];
  window.localStorage.removeItem(STORAGE_KEY);
  statusMessage.textContent = "Workout history cleared.";
  render();
});

What does Clear all remove?

  • The empty array resets FitLog's in-memory state.
  • The storage call removes only the value under STORAGE_KEY.
  • The final render returns the dashboard and history to their empty state.

Before the final test, which parts of FitLog do you expect to survive a refresh? What should happen after you clear the remaining history?

  • Save main.js.
  • Return to Safari.
  • Add two workouts dated today with different minute and calorie values.
  • Refresh Safari.

Both rows should return after the refresh. The dashboard should rebuild the combined workout count, minutes, calories, and goal progress from saved data.

  • Click Delete beside one workout.

Only the selected row should disappear. The dashboard should return to the values contributed by the remaining workout.

  • Click Clear all.

You should see No workouts yet. Add your first entry. in the history card. The dashboard should show zeros with an empty progress bar.

You have completed the core FitLog app. It now preserves workouts across refreshes and turns today's records into a live dashboard with working cleanup controls.

History returns after Clear all?

  • Check that the listener calls window.localStorage.removeItem(STORAGE_KEY).
  • Confirm that the listener assigns an empty array to workouts.
  • Refresh Safari after clearing to verify that no saved workouts return.

Still seeing old workouts? Help me troubleshoot FitLog's Clear all control.

✔️ Awesome, I've got everything!

Great work. Save main.js one final time.

ⓧ I'd like to double check the full code

const STORAGE_KEY = "fitlog.workouts";
const DAILY_GOAL = 30;

const form = document.querySelector("#workout-form");
const activityInput = document.querySelector("#activity");
const dateInput = document.querySelector("#workout-date");
const durationInput = document.querySelector("#duration");
const caloriesInput = document.querySelector("#calories");
const statusMessage = document.querySelector("#form-status");
const workoutList = document.querySelector("#workout-list");
const emptyState = document.querySelector("#empty-state");
const clearAllButton = document.querySelector("#clear-all");
const workoutCount = document.querySelector("#workout-count");
const minuteCount = document.querySelector("#minute-count");
const calorieCount = document.querySelector("#calorie-count");
const goalProgress = document.querySelector("#goal-progress");
const goalCopy = document.querySelector("#goal-copy");

let workouts = loadWorkouts();
dateInput.value = getLocalDateKey();

form.addEventListener("submit", (event) => {
  event.preventDefault();

  const workout = {
    activity: activityInput.value,
    date: dateInput.value,
    duration: Number(durationInput.value),
    calories: Number(caloriesInput.value),
  };

  workouts.unshift(workout);
  saveWorkouts();
  render();

  form.reset();
  dateInput.value = getLocalDateKey();
  statusMessage.textContent = `${workout.activity} workout logged.`;
});

clearAllButton.addEventListener("click", () => {
  workouts = [];
  window.localStorage.removeItem(STORAGE_KEY);
  statusMessage.textContent = "Workout history cleared.";
  render();
});

function loadWorkouts() {
  const storedWorkouts = window.localStorage.getItem(STORAGE_KEY);

  if (!storedWorkouts) {
    return [];
  }

  try {
    const parsedWorkouts = JSON.parse(storedWorkouts);
    return Array.isArray(parsedWorkouts) ? parsedWorkouts : [];
  } catch {
    return [];
  }
}

function saveWorkouts() {
  window.localStorage.setItem(STORAGE_KEY, JSON.stringify(workouts));
}

function getLocalDateKey() {
  const today = new Date();
  const year = today.getFullYear();
  const month = String(today.getMonth() + 1).padStart(2, "0");
  const day = String(today.getDate()).padStart(2, "0");
  return `${year}-${month}-${day}`;
}

function formatDate(dateString) {
  return new Date(`${dateString}T00:00:00`).toLocaleDateString(undefined, {
    year: "numeric",
    month: "short",
    day: "numeric",
  });
}

function renderSummary() {
  const todayKey = getLocalDateKey();
  const todaysWorkouts = workouts.filter((workout) => workout.date === todayKey);
  const totalMinutes = todaysWorkouts.reduce(
    (total, workout) => total + workout.duration,
    0,
  );
  const totalCalories = todaysWorkouts.reduce(
    (total, workout) => total + workout.calories,
    0,
  );

  workoutCount.textContent = todaysWorkouts.length;
  minuteCount.textContent = totalMinutes;
  calorieCount.textContent = totalCalories;
  goalProgress.value = Math.min(totalMinutes, DAILY_GOAL);
  goalCopy.textContent = `${totalMinutes} of ${DAILY_GOAL} minutes`;
}

function deleteWorkout(workout) {
  const workoutIndex = workouts.indexOf(workout);

  if (workoutIndex === -1) {
    return;
  }

  workouts.splice(workoutIndex, 1);
  saveWorkouts();
  statusMessage.textContent = `${workout.activity} workout deleted.`;
  render();
}

function renderHistory() {
  workoutList.textContent = "";
  emptyState.hidden = workouts.length > 0;
  clearAllButton.disabled = workouts.length === 0;

  workouts.forEach((workout) => {
    const item = document.createElement("li");
    const info = document.createElement("div");
    const title = document.createElement("strong");
    const meta = document.createElement("span");
    const deleteButton = document.createElement("button");

    item.className = "workout-item";
    info.className = "workout-info";
    meta.className = "workout-meta";
    deleteButton.className = "delete-button";
    deleteButton.type = "button";

    title.textContent = workout.activity;
    meta.textContent = `${formatDate(workout.date)} | ${workout.duration} min | ${workout.calories} cal`;
    deleteButton.textContent = "Delete";
    deleteButton.setAttribute(
      "aria-label",
      `Delete ${workout.activity} workout from ${formatDate(workout.date)}`,
    );
    deleteButton.addEventListener("click", () => deleteWorkout(workout));

    info.append(title, meta);
    item.append(info, deleteButton);
    workoutList.appendChild(item);
  });
}

function render() {
  renderSummary();
  renderHistory();
}

render();

Secret mission

Filter History by Activity

A growing workout history takes longer to scan. Add an activity filter that focuses the visible rows without changing the saved workouts or today's dashboard totals.

Clean Up Your Resources

Clean Up Your Resources

FitLog runs entirely on your Mac. There are no ongoing costs or cloud resources to manage.

Decide whether to keep the app running, pause the development server, or remove the project entirely.

Resources you used:

  • The running Vite development server in Visual Studio Code.
  • The local fitlog folder containing the complete project plus its installed dependencies.
  • Workout data saved by Safari in localStorage under fitlog.workouts for http://localhost:5173.

Keep everything running

No action is needed. Choose this if you want to keep logging workouts or experimenting with the activity filter.

  • Leave the Vite development server running in the Visual Studio Code terminal.
  • Keep the Safari tab open at http://localhost:5173.
  • Keep the fitlog folder on your Mac.

Pause - I'll come back to this later

Shut down the local server to free the terminal. Your project files plus saved workout data stay on your Mac.

  • Return to the Visual Studio Code terminal from earlier.
  • Press Control+C to stop the Vite development server.

That is a clean stopping point. FitLog is ready for your next local run.

Delete - I don't want to use this again

Remove the saved workout records before deleting the local project. Clearing FitLog history cannot be undone through the app.

  • Return to the FitLog tab in Safari.
  • Click Clear all in the workout history card.
  • Confirm that the history shows No workouts yet. Add your first entry..
  • Return to the Visual Studio Code terminal from earlier.
  • Press Control+C to stop the Vite development server.
  • In Finder, locate the fitlog folder on your Mac.
  • Right-click the fitlog folder.
  • Select Move to Trash.

Your saved workout history is now removed from Safari. The complete project folder is now in Trash.

Nice Work!

Nice Work!

You did it! FitLog is now a responsive local fitness diary in Safari that restores saved workouts after every refresh.

You've learned how to:

  • Built a responsive workout form with visible activity history. Exposed the limits of in-memory state by refreshing the page.
  • Created persistent history by serializing workout records as JSON. Restored those records from localStorage when FitLog starts.
  • Turned saved records into a daily dashboard for today's totals. Added goal progress plus controls for deleting one workout or clearing the complete history.
  • Completed the Secret Mission by adding an activity filter. Derived a focused history view without overwriting workouts. Kept dashboard totals based on the complete saved list.

Ready to quiz yourself?