Build a Rotation Diet Tracker

Build a browser app that enforces the 4-day rotation diet rule.

Introduction

30 Second Summary

If you have food sensitivities, keeping track of what you eat and when can feel like a full-time job. A rotation diet spreads foods out over a 4-day cycle so your body gets a break between repeated exposures.

In this project, you will build a browser-based rotation diet tracker that logs your meals, organizes foods by biological family, and warns you when you're about to repeat a food too soon.

What You'll Build

A responsive web app using HTML, CSS, and JavaScript that enforces the 4-day rotation diet rule with a visual planner, food log, and food family reference.

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

  • A tabbed navigation layout with Log, Planner, and Families views that switch content without reloading the page.
  • A 4-day rotation planner grid that color-codes food families as available or restricted based on your recent log.
  • A food log powered by JavaScript and localStorage that persists between browser sessions.
  • Secret Mission: Connect to a free recipe API to suggest meals based on your available ingredients.

Are there any prerequisites?

Basic familiarity with HTML and CSS (tags, selectors, properties) is helpful. No JavaScript experience is needed. This project introduces JavaScript from scratch.

Set Up VS Code and Build the App Shell

Every web app starts with an HTML file that defines its structure. Your rotation diet tracker needs a header, navigation tabs, and a content area where the app's three views will live.

In this step, you will write the HTML shell for your app and launch it in the browser using Live Server. This gives you a live preview that updates every time you save a file.

In this step, get ready to:
  • Build the HTML app shell with a header, tab navigation, and content area.
  • Launch Live Server to preview your app in the browser.
Build the HTML app shell

The app shell is the outer structure of your page. It holds the header, a navigation bar with three tab buttons, and an empty content area you will fill in later.

  • Click index.html in the Explorer sidebar to open it in the editor.
  • Paste the following code into the file:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Rotation Diet Tracker</title>
  <!-- Link the CSS stylesheet -->
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <!-- App container -->
  <div id="app">
    <!-- Header with app title and subtitle -->
    <header>
      <h1>Rotation Diet Tracker</h1>
      <p class="subtitle">4-Day Diversified Diet Planner</p>
    </header>
    <!-- Tab navigation bar with data-tab attributes -->
    <nav class="tabs">
      <button class="tab active" data-tab="log">Log</button>
      <button class="tab" data-tab="planner">Planner</button>
      <button class="tab" data-tab="families">Families</button>
    </nav>
    <!-- Main content area (sections added in later steps) -->
    <main>
    </main>
  </div>
  <!-- Link JavaScript at the end of body -->
  <script src="app.js"></script>
</body>
</html>

What does this code do?

The <head> section links your CSS stylesheet so styles you write later will apply to the page.

The <nav> bar has three buttons, each with a data-tab attribute. In a later step, JavaScript will read these attributes to know which content section to show when a tab is clicked.

The <script> tag at the bottom loads app.js after the page structure exists. Placing it at the end of the body ensures all HTML elements are available before your code runs.

  • Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).

You won't see anything in the browser yet. In the next substep, you will start Live Server to preview the page.

Seeing red underlines in the editor?

Make sure you pasted the code into index.html, not style.css or app.js. Check the filename shown on the editor tab at the top.

If VS Code highlights a syntax issue, confirm you copied the entire block starting from <!DOCTYPE html> through to the closing </html> tag.

Help me fix a syntax error in my index.html file.

✔️ Awesome, I've got everything!

Great. Double check you have saved index.html before moving on.

ⓧ I'd like to double check the full code

Here is the complete index.html at this point:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Rotation Diet Tracker</title>
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <div id="app">
    <header>
      <h1>Rotation Diet Tracker</h1>
      <p class="subtitle">4-Day Diversified Diet Planner</p>
    </header>
    <nav class="tabs">
      <button class="tab active" data-tab="log">Log</button>
      <button class="tab" data-tab="planner">Planner</button>
      <button class="tab" data-tab="families">Families</button>
    </nav>
    <main>
    </main>
  </div>
  <script src="app.js"></script>
</body>
</html>
Launch Live Server to preview your app

Live Server runs a local web server on your machine and opens the page in your browser. Every time you save a file, it automatically refreshes the page so you see changes instantly.

  • Right-click index.html in the Explorer sidebar.
  • Select Open with Live Server.

Your default browser opens and displays the page at a localhost address. You should see the title "Rotation Diet Tracker", the subtitle, and three unstyled tab buttons (Log, Planner, Families).

Why use Live Server instead of opening the file directly?

Opening an HTML file directly in the browser uses the file:// protocol, which can cause issues with JavaScript and external API calls later in the project. Live Server serves your files over http://localhost, which behaves like a real web server and avoids those problems.

Don't see Open with Live Server in the menu?

Make sure you are right-clicking the file name in the Explorer sidebar (the left panel), not inside the code editor area.

If the option still does not appear, confirm the Live Server extension is installed. Press Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows) to open the Extensions panel and search for "Live Server" by Ritwick Dey.

Help me get Live Server working in VS Code.

Your app shell is live in the browser. Next up, you will style the layout and build the food families display.

Style the Layout and Build the Food Families Display

Your app shell is live in the browser, but right now it is unstyled text on a blank page. To turn this into a usable diet tracker, you need a visual language that instantly communicates food status: safe, restricted, or warning.

In this step, you will build that visual system using CSS custom properties and style the food families display as clickable accordion cards.

In this step, get ready to:
  • Define a color system with CSS custom properties for safe, warning, and restricted states.
  • Style the tab navigation, cards, and accordion component.
  • Build the food families reference display with 8 biological food families.
Set up the color system and base layout

CSS custom properties let you define colors once and reuse them everywhere. Your tracker needs three status colors: green for safe foods, amber for warnings, and red for restricted foods.

  • In VS Code, open style.css and add the color system and base layout by pasting this code:
/* Color system: green = safe, amber = warning, red = restricted */
:root {
  --color-safe: #22c55e;
  --color-safe-bg: #f0fdf4;
  --color-warning: #f59e0b;
  --color-warning-bg: #fffbeb;
  --color-restricted: #ef4444;
  --color-restricted-bg: #fef2f2;
  --color-primary: #3b82f6;
  --color-text: #1f2937;
  --color-text-light: #6b7280;
  --color-border: #e5e7eb;
  --color-bg: #f9fafb;
  --color-white: #ffffff;
  --radius: 8px;
}

/* Reset default browser spacing */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/* Centered layout with max width for readability */
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: var(--color-bg);
  color: var(--color-text);
  line-height: 1.6;
  max-width: 800px;
  margin: 0 auto;
  padding: 1rem;
}

What does this code do?

The :root block defines CSS custom properties (variables) that you can reuse throughout your stylesheet with var(--name). Change a color in one place and it updates everywhere.

The * selector resets margins and padding on all elements so spacing is predictable. The body rule centers your app at a max width of 800px.

Now add the header and tab navigation styles below the body rule.

  • Still in style.css, paste this code below the closing } of the body rule:
/* Header centered with subtitle */
header {
  text-align: center;
  padding: 1.5rem 0;
}

header h1 {
  font-size: 1.5rem;
}

.subtitle {
  color: var(--color-text-light);
  font-size: 0.875rem;
}

/* Utility class to hide inactive tab sections */
.hidden {
  display: none;
}

/* Tab bar with bottom border highlight on active tab */
.tabs {
  display: flex;
  border-bottom: 2px solid var(--color-border);
  margin-bottom: 1.5rem;
}

.tab {
  flex: 1;
  padding: 0.75rem 1rem;
  border: none;
  background: none;
  cursor: pointer;
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--color-text-light);
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  transition: all 0.2s;
}

.tab.active {
  color: var(--color-primary);
  border-bottom-color: var(--color-primary);
}

.tab:hover {
  color: var(--color-text);
}

What does this code do?

The .tabs container uses flexbox to spread tabs evenly across the width. Each .tab button has a transparent bottom border that turns blue when the .active class is applied.

The margin-bottom: -2px trick overlaps the tab's border with the container's border so the active indicator looks seamless.

  • Save style.css.
  • Switch to your browser. You should see the header centered with the subtitle below it, and the three tab buttons displayed in a horizontal row with a blue underline on the active Log tab.

Tabs still look like plain text?

Make sure your <link rel="stylesheet" href="style.css" /> tag in index.html matches the filename exactly. CSS file names are case-sensitive.

Check that you saved both files. Live Server only reloads when a file is saved.

If you need help, ask here for debugging tips.

Style cards and the accordion component

The food families display needs two visual patterns: a card container for grouping content, and an accordion for expandable food lists.

  • In style.css, add the card and section title styles below your existing tab rules:
/* Card container for grouped content */
.card {
  background: var(--color-white);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 1rem;
  margin-bottom: 1rem;
}

/* Section headings inside cards */
.section-title {
  font-size: 1rem;
  font-weight: 600;
  margin-bottom: 0.75rem;
}

What does this code do?

The .card class creates a white box with a subtle border and rounded corners. You will use this to wrap forms and log entries later. The .section-title gives each card a consistent heading size.

  • Now add the accordion styles and search input below the section title rule:
/* Accordion: expandable food family panels */
.accordion-item {
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  margin-bottom: 0.5rem;
  overflow: hidden;
}

.accordion-header {
  padding: 0.75rem 1rem;
  background: var(--color-white);
  cursor: pointer;
  font-weight: 500;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.accordion-header:hover {
  background: var(--color-bg);
}

/* Hidden by default, shown when .open class is added */
.accordion-body {
  padding: 0 1rem 0.75rem;
  display: none;
}

.accordion-body.open {
  display: block;
}

/* Food items displayed as horizontal pills */
.accordion-body ul {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.accordion-body li {
  background: var(--color-bg);
  padding: 0.25rem 0.5rem;
  border-radius: 4px;
  font-size: 0.8rem;
}

/* Red highlight for restricted foods */
.accordion-body li.restricted-food {
  background: var(--color-restricted-bg);
  color: var(--color-restricted);
}

/* Search bar above the families list */
.search-input {
  width: 100%;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font-size: 0.9rem;
  margin-bottom: 1rem;
}

What does this code do?

Each .accordion-item is a bordered panel. The .accordion-body is hidden by default (display: none) and only appears when the .open class is toggled on.

Food items inside use flexbox with flex-wrap to flow as horizontal pills that wrap to the next line. The .restricted-food class turns individual food pills red when they violate the 4-day rule.

  • Save style.css.
  • You will not see the accordion styles in your browser yet because there is no HTML using these classes. The next substep adds that content.
Build the food families HTML

A rotation diet groups foods by biological family. Members of the same family share chemical compounds, so eating one tomato means all nightshades (potato, pepper, eggplant) go on the same 4-day rotation.

  • In index.html, find the empty <main> tag.
  • Replace the empty <main></main> with the following food families section:
    <main>
      <!-- Food Families tab content -->
      <section id="tab-families">
        <h2 class="section-title">Food Families Reference</h2>
        <input type="text" class="search-input" id="family-search" placeholder="Search foods or families..." />
        <div id="families-list">
          <!-- Nightshade family -->
          <div class="accordion-item"><div class="accordion-header">Nightshade (5 foods)</div><div class="accordion-body"><ul><li>tomato</li><li>potato</li><li>bell pepper</li><li>eggplant</li><li>chili pepper</li></ul></div></div>
          <!-- Grass/Grain family -->
          <div class="accordion-item"><div class="accordion-header">Grass/Grain (6 foods)</div><div class="accordion-body"><ul><li>wheat</li><li>rice</li><li>corn</li><li>oats</li><li>barley</li><li>rye</li></ul></div></div>
          <!-- Citrus family -->
          <div class="accordion-item"><div class="accordion-header">Citrus (5 foods)</div><div class="accordion-body"><ul><li>orange</li><li>lemon</li><li>grapefruit</li><li>lime</li><li>tangerine</li></ul></div></div>
          <!-- Legume family -->
          <div class="accordion-item"><div class="accordion-header">Legume (5 foods)</div><div class="accordion-body"><ul><li>peanut</li><li>soybean</li><li>lentil</li><li>chickpea</li><li>kidney bean</li></ul></div></div>
          <!-- Cruciferous family -->
          <div class="accordion-item"><div class="accordion-header">Cruciferous (5 foods)</div><div class="accordion-body"><ul><li>broccoli</li><li>cauliflower</li><li>cabbage</li><li>kale</li><li>brussels sprouts</li></ul></div></div>
          <!-- Dairy family -->
          <div class="accordion-item"><div class="accordion-header">Dairy (5 foods)</div><div class="accordion-body"><ul><li>milk</li><li>cheese</li><li>yogurt</li><li>butter</li><li>cream</li></ul></div></div>
          <!-- Allium family -->
          <div class="accordion-item"><div class="accordion-header">Allium (5 foods)</div><div class="accordion-body"><ul><li>onion</li><li>garlic</li><li>leek</li><li>chive</li><li>shallot</li></ul></div></div>
          <!-- Rose family -->
          <div class="accordion-item"><div class="accordion-header">Rose (6 foods)</div><div class="accordion-body"><ul><li>apple</li><li>pear</li><li>strawberry</li><li>almond</li><li>cherry</li><li>peach</li></ul></div></div>
        </div>
      </section>
    </main>

What does this code do?

Each food family is wrapped in an .accordion-item with a clickable header showing the family name and food count. The .accordion-body holds the list of individual foods.

The search input at the top will eventually filter families by name. The accordion bodies are hidden by default (your CSS sets display: none) so only the headers are visible until you add JavaScript to toggle them.

  • Save index.html.
  • Switch to your browser. You should see 8 accordion panels listed vertically, each showing a food family name like Nightshade (5 foods) with a white background and rounded border. A search input sits above them.

Accordion panels not showing up?

Check that your <main> tag contains the full <section id="tab-families"> block. If you see the old empty layout, you may have pasted the code outside the <main> tags.

Make sure each .accordion-item div has matching opening and closing tags. A mismatched tag can cause the entire section to disappear.

If you need help, ask here to debug your HTML structure.

Why can't I click the accordions open yet?

The accordion headers are styled to look clickable (with cursor: pointer), but clicking does nothing yet. Opening them requires JavaScript to toggle the .open class on the body. You will add that in the next step.

✔️ Awesome, I've got everything!

Great. Double check you have saved both style.css and index.html.

ⓧ I'd like to double check the full code

Here is the full style.css at this point:

:root {
  --color-safe: #22c55e;
  --color-safe-bg: #f0fdf4;
  --color-warning: #f59e0b;
  --color-warning-bg: #fffbeb;
  --color-restricted: #ef4444;
  --color-restricted-bg: #fef2f2;
  --color-primary: #3b82f6;
  --color-text: #1f2937;
  --color-text-light: #6b7280;
  --color-border: #e5e7eb;
  --color-bg: #f9fafb;
  --color-white: #ffffff;
  --radius: 8px;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: var(--color-bg);
  color: var(--color-text);
  line-height: 1.6;
  max-width: 800px;
  margin: 0 auto;
  padding: 1rem;
}

header {
  text-align: center;
  padding: 1.5rem 0;
}

header h1 {
  font-size: 1.5rem;
}

.subtitle {
  color: var(--color-text-light);
  font-size: 0.875rem;
}

.hidden {
  display: none;
}

.tabs {
  display: flex;
  border-bottom: 2px solid var(--color-border);
  margin-bottom: 1.5rem;
}

.tab {
  flex: 1;
  padding: 0.75rem 1rem;
  border: none;
  background: none;
  cursor: pointer;
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--color-text-light);
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  transition: all 0.2s;
}

.tab.active {
  color: var(--color-primary);
  border-bottom-color: var(--color-primary);
}

.tab:hover {
  color: var(--color-text);
}

.card {
  background: var(--color-white);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 1rem;
  margin-bottom: 1rem;
}

.section-title {
  font-size: 1rem;
  font-weight: 600;
  margin-bottom: 0.75rem;
}

.accordion-item {
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  margin-bottom: 0.5rem;
  overflow: hidden;
}

.accordion-header {
  padding: 0.75rem 1rem;
  background: var(--color-white);
  cursor: pointer;
  font-weight: 500;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.accordion-header:hover {
  background: var(--color-bg);
}

.accordion-body {
  padding: 0 1rem 0.75rem;
  display: none;
}

.accordion-body.open {
  display: block;
}

.accordion-body ul {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.accordion-body li {
  background: var(--color-bg);
  padding: 0.25rem 0.5rem;
  border-radius: 4px;
  font-size: 0.8rem;
}

.accordion-body li.restricted-food {
  background: var(--color-restricted-bg);
  color: var(--color-restricted);
}

.search-input {
  width: 100%;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font-size: 0.9rem;
  margin-bottom: 1rem;
}

And here is the full index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Rotation Diet Tracker</title>
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <div id="app">
    <header>
      <h1>Rotation Diet Tracker</h1>
      <p class="subtitle">4-Day Diversified Diet Planner</p>
    </header>
    <nav class="tabs">
      <button class="tab active" data-tab="log">Log</button>
      <button class="tab" data-tab="planner">Planner</button>
      <button class="tab" data-tab="families">Families</button>
    </nav>
    <main>
      <section id="tab-families">
        <h2 class="section-title">Food Families Reference</h2>
        <input type="text" class="search-input" id="family-search" placeholder="Search foods or families..." />
        <div id="families-list">
          <div class="accordion-item"><div class="accordion-header">Nightshade (5 foods)</div><div class="accordion-body"><ul><li>tomato</li><li>potato</li><li>bell pepper</li><li>eggplant</li><li>chili pepper</li></ul></div></div>
          <div class="accordion-item"><div class="accordion-header">Grass/Grain (6 foods)</div><div class="accordion-body"><ul><li>wheat</li><li>rice</li><li>corn</li><li>oats</li><li>barley</li><li>rye</li></ul></div></div>
          <div class="accordion-item"><div class="accordion-header">Citrus (5 foods)</div><div class="accordion-body"><ul><li>orange</li><li>lemon</li><li>grapefruit</li><li>lime</li><li>tangerine</li></ul></div></div>
          <div class="accordion-item"><div class="accordion-header">Legume (5 foods)</div><div class="accordion-body"><ul><li>peanut</li><li>soybean</li><li>lentil</li><li>chickpea</li><li>kidney bean</li></ul></div></div>
          <div class="accordion-item"><div class="accordion-header">Cruciferous (5 foods)</div><div class="accordion-body"><ul><li>broccoli</li><li>cauliflower</li><li>cabbage</li><li>kale</li><li>brussels sprouts</li></ul></div></div>
          <div class="accordion-item"><div class="accordion-header">Dairy (5 foods)</div><div class="accordion-body"><ul><li>milk</li><li>cheese</li><li>yogurt</li><li>butter</li><li>cream</li></ul></div></div>
          <div class="accordion-item"><div class="accordion-header">Allium (5 foods)</div><div class="accordion-body"><ul><li>onion</li><li>garlic</li><li>leek</li><li>chive</li><li>shallot</li></ul></div></div>
          <div class="accordion-item"><div class="accordion-header">Rose (6 foods)</div><div class="accordion-body"><ul><li>apple</li><li>pear</li><li>strawberry</li><li>almond</li><li>cherry</li><li>peach</li></ul></div></div>
        </div>
      </section>
    </main>
  </div>
  <script src="app.js"></script>
</body>
</html>

Your app now has a complete visual foundation and a structured food families reference. Next up, you will bring it to life with JavaScript: making the tabs switch between views and building a dynamic planner grid.

Add JavaScript for Tab Switching and the Planner Grid

Your app looks polished, but clicking the tab buttons does nothing. The Families content is always visible and there is no way to switch between views.

This is where JavaScript comes in. You will write your first JS code to make the tabs interactive, render the food families dynamically, and build a 4-day planner grid that shows which foods are available or restricted.

In this step, get ready to:
  • Restructure your HTML into three switchable tab sections.
  • Write JavaScript that switches tabs and renders food families dynamically.
  • Build a 4-day planner grid with available and restricted food families.
Restructure the HTML into three tab sections

Right now your <main> element contains only the Families content. For tabs to work, you need three separate sections that JavaScript can show or hide.

  • In VS Code, open index.html.
  • Find the <main> element and replace everything inside it (the entire <section id="tab-families"> block) with these three sections:
      <section id="tab-log">
      </section>

      <section id="tab-planner" class="hidden">
        <h2 class="section-title">4-Day Rotation Planner</h2>
        <div id="planner-grid" class="planner-grid"></div>
      </section>

      <section id="tab-families" class="hidden">
        <h2 class="section-title">Food Families Reference</h2>
        <input type="text" class="search-input" id="family-search" placeholder="Search foods or families..." />
        <div id="families-list"></div>
      </section>

What does this structure do?

Each section gets an ID that matches the data-tab attribute on the navigation buttons (log, planner, families). JavaScript will use this connection to toggle visibility.

The tab-log section is empty for now because you will build the food log form in the next step. The tab-planner has an empty grid div that JavaScript will fill. The tab-families keeps the search input but its content will now be rendered dynamically by JavaScript instead of being hardcoded.

  • Save index.html.
  • Refresh your browser. You should see a blank page below the tabs because the hardcoded food families are gone and the Log section is empty.

Still seeing the old food families?

Make sure you replaced the entire old <section id="tab-families"> block, including its closing </section> tag. You should have three <section> elements inside <main> now.

Help me fix my HTML structure for the three tab sections.

Write JavaScript for tab switching and food families

Time for your first JavaScript. You will write code that responds when a user clicks a tab button, hides all sections, and shows only the one that matches.

  • Open app.js (currently empty) and add the food families data array:
// --- Food Families Data ---
var foodFamilies = [
  { id: "nightshade", name: "Nightshade", members: ["tomato", "potato", "bell pepper", "eggplant", "chili pepper"] },
  { id: "grass", name: "Grass/Grain", members: ["wheat", "rice", "corn", "oats", "barley", "rye"] },
  { id: "citrus", name: "Citrus", members: ["orange", "lemon", "grapefruit", "lime", "tangerine"] },
  { id: "legume", name: "Legume", members: ["peanut", "soybean", "lentil", "chickpea", "kidney bean"] },
  { id: "cruciferous", name: "Cruciferous", members: ["broccoli", "cauliflower", "cabbage", "kale", "brussels sprouts"] },
  { id: "dairy", name: "Dairy", members: ["milk", "cheese", "yogurt", "butter", "cream"] },
  { id: "allium", name: "Allium", members: ["onion", "garlic", "leek", "chive", "shallot"] },
  { id: "rose", name: "Rose", members: ["apple", "pear", "strawberry", "almond", "cherry", "peach"] },
  { id: "umbellifer", name: "Umbellifer", members: ["carrot", "celery", "parsley", "dill", "fennel"] },
  { id: "poultry", name: "Poultry", members: ["chicken", "turkey", "duck", "egg"] }
];

What is this data structure?

This is a JavaScript array of objects. Each object represents one food family with three properties: an id for code references, a name for display, and a members array listing the foods that belong to that biological family.

Moving this data into JavaScript (instead of hardcoding it in HTML) means your code can loop through it to generate the accordion, populate dropdowns, and check rotation rules. One source of truth for all features.

  • Below the food families array, add the tab switching logic:
// --- Tab Switching ---
var tabs = document.querySelectorAll(".tab");
var sections = document.querySelectorAll("main > section");

tabs.forEach(function(tab) {
  tab.addEventListener("click", function() {
    // Remove active class from all tabs
    tabs.forEach(function(t) { t.classList.remove("active"); });
    // Add active class to the clicked tab
    tab.classList.add("active");

    // Hide all sections
    var target = tab.getAttribute("data-tab");
    sections.forEach(function(section) { section.classList.add("hidden"); });
    // Show the matching section
    document.getElementById("tab-" + target).classList.remove("hidden");

    // Re-render content when switching to specific tabs
    if (target === "planner") {
      renderPlanner();
    }
    if (target === "families") {
      renderFamilies("");
    }
  });
});

What does this code do?

  • document.querySelectorAll(".tab") grabs all three tab buttons from the navigation bar.
  • forEach loops through each button and attaches a click listener so something happens when the user clicks.
  • When clicked, the code removes the active class from every tab, adds it to the clicked one, hides all sections, then reveals only the matching section by building the ID from "tab-" + target.

The tabs won't fully work yet because renderPlanner() and renderFamilies() don't exist yet. You need the families rendering function so the Families tab has content.

  • Below the tab switching code, add the renderFamilies function, the search listener, and the initialization call:
// --- Families UI ---
function renderFamilies(filter) {
  var listDiv = document.getElementById("families-list");
  var html = "";

  // Loop through each food family
  for (var i = 0; i < foodFamilies.length; i++) {
    var family = foodFamilies[i];
    // Check if the family name matches the search filter
    var nameMatch = family.name.toLowerCase().indexOf(filter) !== -1;
    // Check which individual members match
    var memberMatches = family.members.filter(function(m) {
      return m.toLowerCase().indexOf(filter) !== -1;
    });

    // Skip this family if filter is active and nothing matches
    if (filter && !nameMatch && memberMatches.length === 0) continue;

    // Show only matching members if the family name itself didn't match
    var displayMembers = (filter && !nameMatch) ? memberMatches : family.members;

    // Build the accordion HTML for this family
    html += '<div class="accordion-item"><div class="accordion-header" data-family="' + family.id + '">' +
      '<span>' + family.name + ' (' + family.members.length + ' foods)</span>' +
      '</div>' +
      '<div class="accordion-body" id="acc-' + family.id + '"><ul>';

    for (var j = 0; j < displayMembers.length; j++) {
      html += '<li>' + displayMembers[j] + '</li>';
    }

    html += '</ul></div></div>';
  }

  // Show a message if no families match the search
  if (!html) {
    html = '<p style="color:var(--color-text-light);text-align:center;padding:2rem">No matching foods or families found.</p>';
  }

  listDiv.innerHTML = html;

  // Attach click handlers to accordion headers
  var headers = listDiv.querySelectorAll(".accordion-header");
  headers.forEach(function(header) {
    header.addEventListener("click", function() {
      var familyId = header.getAttribute("data-family");
      var body = document.getElementById("acc-" + familyId);
      body.classList.toggle("open");
    });
  });
}

// Filter families when the user types in the search box
document.getElementById("family-search").addEventListener("input", function(e) {
  renderFamilies(e.target.value.toLowerCase());
});

// --- Initialize ---
renderFamilies("");

What does renderFamilies do?

  • It loops through the foodFamilies array and generates accordion HTML for each family dynamically.
  • The filter parameter lets users search by food or family name. If a filter is active, only matching families and members are shown.
  • After inserting the HTML, it attaches fresh click listeners to each accordion header so they expand and collapse using the open class.
  • The renderFamilies("") call at the bottom runs immediately when the page loads, populating the Families tab with all 10 food families.
  • Save app.js.
  • Refresh your browser and click the Families tab. You should see all 10 food families rendered as accordion cards (including Umbellifer and Poultry, which are new).
  • Click the Log tab. The Families section disappears and the Log section appears (blank for now).
  • Try typing a food name in the search box on the Families tab. The list filters as you type.

Tabs not switching?

  • Open your browser's developer console (right-click the page, select **Inspect**, then click the **Console** tab). Look for red error messages. A common issue is a typo in a function name or a missing closing bracket.
  • Check that your section IDs in HTML match exactly: tab-log, tab-planner, tab-families. These must match the data-tab values on the buttons.
  • Make sure your <script src="app.js"></script> tag is at the bottom of the body, after the HTML elements it references.

Help me debug why my tab switching isn't working.

Build the 4-day planner grid

The planner is the core visual feature of this app. It shows 4 day columns so you can see at a glance which food families are available and which are restricted.

  • In style.css, find the .section-title rule. Below it, add the planner grid styles:
.planner-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 1rem;
}

.planner-day {
  background: var(--color-white);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 0.75rem;
}

.planner-day h3 {
  font-size: 0.85rem;
  margin-bottom: 0.75rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--color-border);
}

.family-status {
  font-size: 0.8rem;
  padding: 0.25rem 0;
}

.family-status.safe {
  color: #16a34a;
}

.family-status.restricted {
  color: var(--color-restricted);
}

.family-detail {
  font-size: 0.7rem;
  color: var(--color-text-light);
  margin-left: 0.25rem;
}

What do these styles do?

  • .planner-grid creates a responsive CSS grid that fits as many 170px-wide columns as the screen allows.
  • .planner-day styles each day column as a white card with a border.
  • .family-status.safe and .family-status.restricted color-code families green or red so the planner is scannable at a glance.
  • Save style.css.

Now you need the JavaScript function that generates the planner content. For this step, you will hardcode some families as restricted and others as available. In Step 5, you will make this dynamic based on actual logged foods.

  • In app.js, find the line that says // --- Families UI ---. Above that line, add the static planner function:
// --- Planner UI (static placeholder) ---
function renderPlanner() {
  var grid = document.getElementById("planner-grid");
  var html = "";

  // Generate 4 day columns (today + next 3 days)
  for (var i = 0; i < 4; i++) {
    var date = new Date();
    date.setDate(date.getDate() + i);
    var dateStr = date.toISOString().split("T")[0];
    var label = i === 0 ? "Today" : date.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });

    // Build the day column with hardcoded available/restricted families
    html += '<div class="planner-day"><h3>' + label + '<br><small style="font-weight:normal;color:var(--color-text-light)">' + dateStr + '</small></h3>';
    html += '<p style="font-size:0.7rem;font-weight:600;color:var(--color-safe);margin-bottom:0.25rem">AVAILABLE</p>';
    html += '<div class="family-status safe">Nightshade</div>';
    html += '<div class="family-status safe">Citrus</div>';
    html += '<p style="font-size:0.7rem;font-weight:600;color:var(--color-restricted);margin-top:0.5rem;margin-bottom:0.25rem">RESTRICTED</p>';
    html += '<div class="family-status restricted">Dairy <span class="family-detail">milk 1d ago</span></div>';
    html += '</div>';
  }

  grid.innerHTML = html;
}

What does renderPlanner do?

  • It creates a Date object for each of the next 4 days and formats it for display.
  • Each day gets a column with an "AVAILABLE" section (green) and a "RESTRICTED" section (red). For now these are hardcoded examples.
  • The family-detail span shows which food triggered the restriction and how many days ago. In Step 5, this will be calculated from actual log data.
  • Save app.js.
  • In your browser, click the Planner tab. You should see a 4-column grid with today's date and the next 3 days, each showing "Nightshade" and "Citrus" in green and "Dairy" in red.

Planner tab shows nothing?

  • Make sure the renderPlanner function is placed above the // --- Families UI --- comment. JavaScript reads top to bottom, but the function is called by the tab click listener which fires later. What matters is that no syntax errors prevent the file from loading.
  • Check the developer console for errors. A missing quote or bracket in the HTML string will break the entire script.
  • Verify that your HTML has <div id="planner-grid" class="planner-grid"></div> inside the tab-planner section.

Help me figure out why my planner grid isn't rendering.

✔️ Awesome, I've got everything!

Great. Make sure all three files are saved before moving on.

ⓧ I'd like to double check the full code

Here is the complete code for all three files at this point. Compare yours against these:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Rotation Diet Tracker</title>
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <div id="app">
    <header>
      <h1>Rotation Diet Tracker</h1>
      <p class="subtitle">4-Day Diversified Diet Planner</p>
    </header>
    <nav class="tabs">
      <button class="tab active" data-tab="log">Log</button>
      <button class="tab" data-tab="planner">Planner</button>
      <button class="tab" data-tab="families">Families</button>
    </nav>
    <main>
      <section id="tab-log">
      </section>

      <section id="tab-planner" class="hidden">
        <h2 class="section-title">4-Day Rotation Planner</h2>
        <div id="planner-grid" class="planner-grid"></div>
      </section>

      <section id="tab-families" class="hidden">
        <h2 class="section-title">Food Families Reference</h2>
        <input type="text" class="search-input" id="family-search" placeholder="Search foods or families..." />
        <div id="families-list"></div>
      </section>
    </main>
  </div>
  <script src="app.js"></script>
</body>
</html>
:root {
  --color-safe: #22c55e;
  --color-safe-bg: #f0fdf4;
  --color-warning: #f59e0b;
  --color-warning-bg: #fffbeb;
  --color-restricted: #ef4444;
  --color-restricted-bg: #fef2f2;
  --color-primary: #3b82f6;
  --color-text: #1f2937;
  --color-text-light: #6b7280;
  --color-border: #e5e7eb;
  --color-bg: #f9fafb;
  --color-white: #ffffff;
  --radius: 8px;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: var(--color-bg);
  color: var(--color-text);
  line-height: 1.6;
  max-width: 800px;
  margin: 0 auto;
  padding: 1rem;
}

header {
  text-align: center;
  padding: 1.5rem 0;
}

header h1 {
  font-size: 1.5rem;
}

.subtitle {
  color: var(--color-text-light);
  font-size: 0.875rem;
}

.hidden {
  display: none;
}

.tabs {
  display: flex;
  border-bottom: 2px solid var(--color-border);
  margin-bottom: 1.5rem;
}

.tab {
  flex: 1;
  padding: 0.75rem 1rem;
  border: none;
  background: none;
  cursor: pointer;
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--color-text-light);
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  transition: all 0.2s;
}

.tab.active {
  color: var(--color-primary);
  border-bottom-color: var(--color-primary);
}

.tab:hover {
  color: var(--color-text);
}

.card {
  background: var(--color-white);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 1rem;
  margin-bottom: 1rem;
}

.section-title {
  font-size: 1rem;
  font-weight: 600;
  margin-bottom: 0.75rem;
}

.planner-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 1rem;
}

.planner-day {
  background: var(--color-white);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 0.75rem;
}

.planner-day h3 {
  font-size: 0.85rem;
  margin-bottom: 0.75rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--color-border);
}

.family-status {
  font-size: 0.8rem;
  padding: 0.25rem 0;
}

.family-status.safe {
  color: #16a34a;
}

.family-status.restricted {
  color: var(--color-restricted);
}

.family-detail {
  font-size: 0.7rem;
  color: var(--color-text-light);
  margin-left: 0.25rem;
}

.accordion-item {
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  margin-bottom: 0.5rem;
  overflow: hidden;
}

.accordion-header {
  padding: 0.75rem 1rem;
  background: var(--color-white);
  cursor: pointer;
  font-weight: 500;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.accordion-header:hover {
  background: var(--color-bg);
}

.accordion-body {
  padding: 0 1rem 0.75rem;
  display: none;
}

.accordion-body.open {
  display: block;
}

.accordion-body ul {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.accordion-body li {
  background: var(--color-bg);
  padding: 0.25rem 0.5rem;
  border-radius: 4px;
  font-size: 0.8rem;
}

.accordion-body li.restricted-food {
  background: var(--color-restricted-bg);
  color: var(--color-restricted);
}

.search-input {
  width: 100%;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font-size: 0.9rem;
  margin-bottom: 1rem;
}
// --- Food Families Data ---
var foodFamilies = [
  { id: "nightshade", name: "Nightshade", members: ["tomato", "potato", "bell pepper", "eggplant", "chili pepper"] },
  { id: "grass", name: "Grass/Grain", members: ["wheat", "rice", "corn", "oats", "barley", "rye"] },
  { id: "citrus", name: "Citrus", members: ["orange", "lemon", "grapefruit", "lime", "tangerine"] },
  { id: "legume", name: "Legume", members: ["peanut", "soybean", "lentil", "chickpea", "kidney bean"] },
  { id: "cruciferous", name: "Cruciferous", members: ["broccoli", "cauliflower", "cabbage", "kale", "brussels sprouts"] },
  { id: "dairy", name: "Dairy", members: ["milk", "cheese", "yogurt", "butter", "cream"] },
  { id: "allium", name: "Allium", members: ["onion", "garlic", "leek", "chive", "shallot"] },
  { id: "rose", name: "Rose", members: ["apple", "pear", "strawberry", "almond", "cherry", "peach"] },
  { id: "umbellifer", name: "Umbellifer", members: ["carrot", "celery", "parsley", "dill", "fennel"] },
  { id: "poultry", name: "Poultry", members: ["chicken", "turkey", "duck", "egg"] }
];

// --- Tab Switching ---
var tabs = document.querySelectorAll(".tab");
var sections = document.querySelectorAll("main > section");

tabs.forEach(function(tab) {
  tab.addEventListener("click", function() {
    tabs.forEach(function(t) { t.classList.remove("active"); });
    tab.classList.add("active");

    var target = tab.getAttribute("data-tab");
    sections.forEach(function(section) { section.classList.add("hidden"); });
    document.getElementById("tab-" + target).classList.remove("hidden");

    if (target === "planner") {
      renderPlanner();
    }
    if (target === "families") {
      renderFamilies("");
    }
  });
});

// --- Planner UI (static placeholder) ---
function renderPlanner() {
  var grid = document.getElementById("planner-grid");
  var html = "";

  for (var i = 0; i < 4; i++) {
    var date = new Date();
    date.setDate(date.getDate() + i);
    var dateStr = date.toISOString().split("T")[0];
    var label = i === 0 ? "Today" : date.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });

    html += '<div class="planner-day"><h3>' + label + '<br><small style="font-weight:normal;color:var(--color-text-light)">' + dateStr + '</small></h3>';
    html += '<p style="font-size:0.7rem;font-weight:600;color:var(--color-safe);margin-bottom:0.25rem">AVAILABLE</p>';
    html += '<div class="family-status safe">Nightshade</div>';
    html += '<div class="family-status safe">Citrus</div>';
    html += '<p style="font-size:0.7rem;font-weight:600;color:var(--color-restricted);margin-top:0.5rem;margin-bottom:0.25rem">RESTRICTED</p>';
    html += '<div class="family-status restricted">Dairy <span class="family-detail">milk 1d ago</span></div>';
    html += '</div>';
  }

  grid.innerHTML = html;
}

// --- Families UI ---
function renderFamilies(filter) {
  var listDiv = document.getElementById("families-list");
  var html = "";

  for (var i = 0; i < foodFamilies.length; i++) {
    var family = foodFamilies[i];
    var nameMatch = family.name.toLowerCase().indexOf(filter) !== -1;
    var memberMatches = family.members.filter(function(m) {
      return m.toLowerCase().indexOf(filter) !== -1;
    });

    if (filter && !nameMatch && memberMatches.length === 0) continue;

    var displayMembers = (filter && !nameMatch) ? memberMatches : family.members;

    html += '<div class="accordion-item"><div class="accordion-header" data-family="' + family.id + '">' +
      '<span>' + family.name + ' (' + family.members.length + ' foods)</span>' +
      '</div>' +
      '<div class="accordion-body" id="acc-' + family.id + '"><ul>';

    for (var j = 0; j < displayMembers.length; j++) {
      html += '<li>' + displayMembers[j] + '</li>';
    }

    html += '</ul></div></div>';
  }

  if (!html) {
    html = '<p style="color:var(--color-text-light);text-align:center;padding:2rem">No matching foods or families found.</p>';
  }

  listDiv.innerHTML = html;

  var headers = listDiv.querySelectorAll(".accordion-header");
  headers.forEach(function(header) {
    header.addEventListener("click", function() {
      var familyId = header.getAttribute("data-family");
      var body = document.getElementById("acc-" + familyId);
      body.classList.toggle("open");
    });
  });
}

document.getElementById("family-search").addEventListener("input", function(e) {
  renderFamilies(e.target.value.toLowerCase());
});

// --- Initialize ---
renderFamilies("");

Your app now has working navigation and a visual planner. Next up, you will build the food log form so users can record what they eat and persist it between browser sessions using localStorage.

Build the Food Log with localStorage

Your app looks great and switches between tabs smoothly. But right now, the Log tab is completely empty.

In this step, you will build the food logging form and teach your app to remember what you eat. The secret ingredient is localStorage, a browser feature that saves data even after you close the tab.

In this step, get ready to:
  • Build a food logging form with date, food, and meal selectors.
  • Save and load food entries using localStorage.
  • Add delete functionality so entries can be removed.
Add the food log form to your HTML

The Log tab needs a form where you pick a date, choose a food, select a meal type, and submit. It also needs a display area to show logged entries.

  • In index.html, find the empty <section id="tab-log"> section and replace it with this code:
<section id="tab-log">
  <div class="card">
    <h2 class="section-title">Log Food</h2>
    <!-- Warning messages will appear here -->
    <div id="violation-warning"></div>
    <form id="food-form">
      <div class="form-group">
        <label for="log-date">Date</label>
        <input type="date" id="log-date" />
      </div>
      <div class="form-group">
        <label for="food-select">Food</label>
        <select id="food-select">
          <option value="">Select a food...</option>
        </select>
      </div>
      <div class="form-group">
        <label for="meal-select">Meal</label>
        <select id="meal-select">
          <option value="breakfast">Breakfast</option>
          <option value="lunch">Lunch</option>
          <option value="dinner">Dinner</option>
          <option value="snack">Snack</option>
        </select>
      </div>
      <button type="submit" class="btn btn-primary">Add to Log</button>
    </form>
  </div>
  <div class="card">
    <h2 class="section-title">Today's Log</h2>
    <!-- Logged food entries will render here -->
    <div id="log-entries"></div>
  </div>
</section>

What does this HTML do?

This creates two cards inside the Log tab. The first card holds the logging form with three inputs: a date picker, a food dropdown (which JavaScript will populate later), and a meal type selector.

The second card holds a log-entries div where your logged foods will appear. The violation-warning div is a placeholder for alerts you will add in the next step.

  • Save index.html.
  • Refresh your browser. You should see the Log tab now has a "Log Food" card with date, food, and meal fields, and a "Today's Log" card below it.

Don't see the form?

Make sure the new HTML goes inside the <section id="tab-log"> tags, replacing the empty section. Check that you did not accidentally delete the closing </section> tag.

help me find why my Log tab form is not showing up

  • Now style the form elements by adding this CSS at the bottom of style.css, after the existing .section-title rule:
.form-group {
  margin-bottom: 0.75rem;
}

.form-group label {
  display: block;
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--color-text-light);
  margin-bottom: 0.25rem;
}

.form-group select,
.form-group input {
  width: 100%;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font-size: 0.9rem;
  background: var(--color-white);
}

.btn {
  padding: 0.5rem 1rem;
  border: none;
  border-radius: var(--radius);
  font-size: 0.875rem;
  font-weight: 500;
  cursor: pointer;
  transition: opacity 0.2s;
}

.btn:hover {
  opacity: 0.85;
}

.btn-primary {
  background: var(--color-primary);
  color: white;
}

.btn-danger {
  background: var(--color-restricted);
  color: white;
  font-size: 0.75rem;
  padding: 0.25rem 0.5rem;
}

.warning-banner {
  background: var(--color-warning-bg);
  border: 1px solid var(--color-warning);
  border-radius: var(--radius);
  padding: 0.75rem;
  margin-bottom: 1rem;
  font-size: 0.85rem;
  color: #92400e;
}

.entry-list {
  list-style: none;
}

.entry-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--color-border);
}

.entry-item:last-child {
  border-bottom: none;
}

.entry-food {
  font-weight: 500;
}

.entry-family {
  color: var(--color-text-light);
  font-size: 0.8rem;
}

.entry-violation {
  color: var(--color-restricted);
}

.meal-group-title {
  font-size: 0.8rem;
  color: var(--color-text-light);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin: 1rem 0 0.5rem;
}

What do these styles do?

The .form-group styles space out the form fields and make labels small and subtle. The .btn styles create reusable button classes with a hover fade effect.

The entry styles (.entry-list, .entry-item, .meal-group-title) prepare the layout for displaying logged foods grouped by meal.

  • Save style.css and refresh your browser.
  • The form fields now have proper spacing, rounded borders, and the blue "Add to Log" button has a polished look.

Form looks unstyled?

Make sure your CSS class names match the HTML exactly. The form inputs are wrapped in div elements with class="form-group". If the button is unstyled, confirm it has both class="btn btn-primary" in your HTML.

help me debug why my form styles are not applying

Write localStorage functions and populate the food dropdown

Your browser has a built-in storage system called localStorage. It saves text data that persists even after you close the browser. You will use it to store your food log entries as a JSON string.

  • In app.js, find the // --- Tab Switching --- comment. Add this code directly above it (below the food families array):
// --- localStorage Functions ---
function getFoodLog() {
  // Read the saved food log from the browser
  var data = localStorage.getItem("rotation-diet-food-log");
  if (data) {
    return JSON.parse(data);
  }
  return [];
}

function saveFoodLog(log) {
  // Save the food log array as a JSON string
  localStorage.setItem("rotation-diet-food-log", JSON.stringify(log));
}

function addFoodEntry(food, familyId, date, meal, isViolation) {
  // Create a new entry object and add it to the log
  var log = getFoodLog();
  var entry = {
    id: Date.now().toString(),
    food: food,
    familyId: familyId,
    date: date,
    meal: meal,
    isViolation: isViolation
  };
  log.push(entry);
  saveFoodLog(log);
  return entry;
}

What does this code do?

These three functions handle all the data storage. getFoodLog() reads saved entries from the browser using JSON.parse() to convert the stored text back into a JavaScript array.

saveFoodLog() does the reverse, converting your array into text with JSON.stringify() before saving. addFoodEntry() creates a new entry with a unique ID (the current timestamp) and appends it to the log.

Why does localStorage only store text?

Browsers designed localStorage as a simple key-value store for strings. JavaScript objects and arrays are complex data structures that cannot be saved directly as text. That is why you need JSON.stringify() (object to text) and JSON.parse() (text back to object).

  • Now add the food log UI code. Find the // --- Planner UI comment and add this code directly above it (below the tab switching section):
// --- Food Log UI ---
var foodSelect = document.getElementById("food-select");
var logDateInput = document.getElementById("log-date");
var foodForm = document.getElementById("food-form");
var warningDiv = document.getElementById("violation-warning");

// Set today's date as the default value
logDateInput.value = new Date().toISOString().split("T")[0];

// Build a flat list of all foods from every family
var allFoods = [];
for (var i = 0; i < foodFamilies.length; i++) {
  for (var j = 0; j < foodFamilies[i].members.length; j++) {
    allFoods.push({
      food: foodFamilies[i].members[j],
      familyId: foodFamilies[i].id,
      familyName: foodFamilies[i].name
    });
  }
}
allFoods.sort(function(a, b) { return a.food.localeCompare(b.food); });

// Populate the food dropdown with sorted options
for (var i = 0; i < allFoods.length; i++) {
  var option = document.createElement("option");
  option.value = allFoods[i].food;
  option.setAttribute("data-family", allFoods[i].familyId);
  option.textContent = allFoods[i].food + " (" + allFoods[i].familyName + ")";
  foodSelect.appendChild(option);
}

What does this code do?

First, it grabs references to the form elements using their IDs. Then it sets the date input to today's date so the learner does not have to pick it manually each time.

The nested loop flattens all food family members into a single allFoods array, sorts them alphabetically, and creates a dropdown option for each one. Each option shows the food name and its family (like "apple (Rose)").

  • Save app.js and refresh your browser.
  • Click the food dropdown. You should see a long alphabetical list of foods, each showing its family name in parentheses. The date field should already show today's date.

Dropdown is empty or shows only "Select a food..."?

Open your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows). Look for red error messages. A common cause is a typo in food-select (the ID in your HTML must match exactly).

help me figure out why my food dropdown is not populating

  • Still in app.js, add the form submit handler and the renderLog function directly below the dropdown population loop (still above // --- Planner UI):
// Handle form submission
foodForm.addEventListener("submit", function(e) {
  // Prevent the browser from reloading the page
  e.preventDefault();
  var selected = foodSelect.options[foodSelect.selectedIndex];
  if (!selected.value) return;

  var familyId = selected.getAttribute("data-family");
  var date = logDateInput.value;
  var meal = document.getElementById("meal-select").value;

  // Save the entry (false = no violation check yet)
  addFoodEntry(selected.value, familyId, date, meal, false);
  foodSelect.value = "";
  warningDiv.innerHTML = "";
  renderLog();
});

function renderLog() {
  // Display logged entries for the selected date
  var entriesDiv = document.getElementById("log-entries");
  var date = logDateInput.value;
  var log = getFoodLog();
  var todayEntries = log.filter(function(entry) { return entry.date === date; });

  if (todayEntries.length === 0) {
    entriesDiv.innerHTML = '<p style="color: var(--color-text-light); font-size: 0.85rem;">No entries for this date.</p>';
    return;
  }

  var meals = ["breakfast", "lunch", "dinner", "snack"];
  var html = "";

  for (var m = 0; m < meals.length; m++) {
    var mealEntries = todayEntries.filter(function(e) { return e.meal === meals[m]; });
    if (mealEntries.length === 0) continue;

    html += '<p class="meal-group-title">' + meals[m] + '</p><ul class="entry-list">';
    for (var i = 0; i < mealEntries.length; i++) {
      var entry = mealEntries[i];
      html += '<li class="entry-item"><span><span class="entry-food">' +
        entry.food + '</span> <span class="entry-family">(' + entry.familyId +
        ')</span></span><button class="btn btn-danger" onclick="deleteEntry(\'' +
        entry.id + '\')">Remove</button></li>';
    }
    html += '</ul>';
  }

  entriesDiv.innerHTML = html;
}

What does this code do?

The submit handler prevents the page from reloading (the default behavior of forms), reads the selected food and meal, then saves a new entry to localStorage. After saving, it resets the dropdown and re-renders the log.

renderLog() reads all entries from localStorage, filters them to the selected date, and groups them by meal type (breakfast, lunch, dinner, snack). Each entry gets a "Remove" button that you will wire up next.

  • Save app.js.
  • You won't see a change yet because the Remove buttons call a deleteEntry function that does not exist yet. You will add it in the next chunk.
Add delete functionality and test persistence

Each logged entry needs a way to be removed. You also need to make sure entries load when the page first opens and when you change the date.

  • In app.js, add the deleteFoodEntry function. Find the addFoodEntry function you just wrote and add this directly below it (still inside the localStorage Functions section):
function deleteFoodEntry(entryId) {
  // Remove an entry by filtering it out of the array
  var log = getFoodLog();
  var updated = log.filter(function(entry) {
    return entry.id !== entryId;
  });
  saveFoodLog(updated);
}

What does this code do?

This reads the full log, creates a new array with every entry except the one matching the given ID, then saves the filtered array back to localStorage. The original entry is effectively deleted.

  • Now add the deleteEntry helper and the date change listener. Add this code directly below the renderLog() function (still above // --- Planner UI):
function deleteEntry(id) {
  deleteFoodEntry(id);
  renderLog();
}

// Re-render the log when the date changes
logDateInput.addEventListener("change", function() {
  renderLog();
});

What does this code do?

deleteEntry() is the function called by the Remove buttons in the HTML. It deletes the entry from localStorage and immediately refreshes the displayed list.

The date change listener ensures that when you pick a different date, the log updates to show entries for that day instead.

  • Finally, update the initialization at the bottom of app.js. Find the // --- Initialize --- comment and replace that section so it looks like this:
// --- Initialize ---
renderLog();
renderFamilies("");

Why add renderLog() to initialization?

Without this call, the log entries div would be empty when the page first loads. Calling renderLog() at startup immediately displays any previously saved entries from localStorage.

  • One more update. Find the renderPlanner() function and replace its body so it shows all food families as available. Replace the entire function with this:
// --- Planner UI ---
function renderPlanner() {
  var grid = document.getElementById("planner-grid");
  var html = "";

  for (var i = 0; i < 4; i++) {
    var date = new Date();
    date.setDate(date.getDate() + i);
    var dateStr = date.toISOString().split("T")[0];
    var label = i === 0 ? "Today" : date.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });

    html += '<div class="planner-day"><h3>' + label + '<br><small style="font-weight:normal;color:var(--color-text-light)">' + dateStr + '</small></h3>';
    html += '<p style="font-size:0.7rem;font-weight:600;color:var(--color-safe);margin-bottom:0.25rem">AVAILABLE</p>';
    // Show all families as available (violation checking comes next step)
    for (var f = 0; f < foodFamilies.length; f++) {
      html += '<div class="family-status safe">' + foodFamilies[f].name + '</div>';
    }
    html += '</div>';
  }

  grid.innerHTML = html;
}

Why update the planner?

The previous planner had hardcoded placeholder families. Now it loops through all 10 food families and marks them all as available. In the next step, you will make this dynamic based on your actual food log.

  • Save app.js and refresh your browser.
  • Select a food from the dropdown (like "apple (Rose)"), pick "Lunch" as the meal, and click Add to Log.
  • You should see the entry appear under "Today's Log" with a red Remove button.
  • Refresh the page. The entry should still be there because it is saved in localStorage.
  • Click Remove on the entry. It disappears immediately.

Entries disappear after refresh?

Check that your saveFoodLog() function uses JSON.stringify(log) and your getFoodLog() uses JSON.parse(data). Without these, localStorage cannot store arrays properly.

Also confirm the localStorage key is spelled exactly the same in both functions: rotation-diet-food-log.

help me debug why my food log entries are not persisting after refresh

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files before moving on.

ⓧ I'd like to double check the full code

Here is the full code for each file at this point.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Rotation Diet Tracker</title>
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <div id="app">
    <header>
      <h1>Rotation Diet Tracker</h1>
      <p class="subtitle">4-Day Diversified Diet Planner</p>
    </header>
    <nav class="tabs">
      <button class="tab active" data-tab="log">Log</button>
      <button class="tab" data-tab="planner">Planner</button>
      <button class="tab" data-tab="families">Families</button>
    </nav>
    <main>
      <section id="tab-log">
        <div class="card">
          <h2 class="section-title">Log Food</h2>
          <div id="violation-warning"></div>
          <form id="food-form">
            <div class="form-group">
              <label for="log-date">Date</label>
              <input type="date" id="log-date" />
            </div>
            <div class="form-group">
              <label for="food-select">Food</label>
              <select id="food-select">
                <option value="">Select a food...</option>
              </select>
            </div>
            <div class="form-group">
              <label for="meal-select">Meal</label>
              <select id="meal-select">
                <option value="breakfast">Breakfast</option>
                <option value="lunch">Lunch</option>
                <option value="dinner">Dinner</option>
                <option value="snack">Snack</option>
              </select>
            </div>
            <button type="submit" class="btn btn-primary">Add to Log</button>
          </form>
        </div>
        <div class="card">
          <h2 class="section-title">Today's Log</h2>
          <div id="log-entries"></div>
        </div>
      </section>

      <section id="tab-planner" class="hidden">
        <h2 class="section-title">4-Day Rotation Planner</h2>
        <div id="planner-grid" class="planner-grid"></div>
      </section>

      <section id="tab-families" class="hidden">
        <h2 class="section-title">Food Families Reference</h2>
        <input type="text" class="search-input" id="family-search" placeholder="Search foods or families..." />
        <div id="families-list"></div>
      </section>
    </main>
  </div>
  <script src="app.js"></script>
</body>
</html>
:root {
  --color-safe: #22c55e;
  --color-safe-bg: #f0fdf4;
  --color-warning: #f59e0b;
  --color-warning-bg: #fffbeb;
  --color-restricted: #ef4444;
  --color-restricted-bg: #fef2f2;
  --color-primary: #3b82f6;
  --color-text: #1f2937;
  --color-text-light: #6b7280;
  --color-border: #e5e7eb;
  --color-bg: #f9fafb;
  --color-white: #ffffff;
  --radius: 8px;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: var(--color-bg);
  color: var(--color-text);
  line-height: 1.6;
  max-width: 800px;
  margin: 0 auto;
  padding: 1rem;
}

header {
  text-align: center;
  padding: 1.5rem 0;
}

header h1 {
  font-size: 1.5rem;
}

.subtitle {
  color: var(--color-text-light);
  font-size: 0.875rem;
}

.hidden {
  display: none;
}

.tabs {
  display: flex;
  border-bottom: 2px solid var(--color-border);
  margin-bottom: 1.5rem;
}

.tab {
  flex: 1;
  padding: 0.75rem 1rem;
  border: none;
  background: none;
  cursor: pointer;
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--color-text-light);
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  transition: all 0.2s;
}

.tab.active {
  color: var(--color-primary);
  border-bottom-color: var(--color-primary);
}

.tab:hover {
  color: var(--color-text);
}

.card {
  background: var(--color-white);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 1rem;
  margin-bottom: 1rem;
}

.section-title {
  font-size: 1rem;
  font-weight: 600;
  margin-bottom: 0.75rem;
}

.form-group {
  margin-bottom: 0.75rem;
}

.form-group label {
  display: block;
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--color-text-light);
  margin-bottom: 0.25rem;
}

.form-group select,
.form-group input {
  width: 100%;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font-size: 0.9rem;
  background: var(--color-white);
}

.btn {
  padding: 0.5rem 1rem;
  border: none;
  border-radius: var(--radius);
  font-size: 0.875rem;
  font-weight: 500;
  cursor: pointer;
  transition: opacity 0.2s;
}

.btn:hover {
  opacity: 0.85;
}

.btn-primary {
  background: var(--color-primary);
  color: white;
}

.btn-danger {
  background: var(--color-restricted);
  color: white;
  font-size: 0.75rem;
  padding: 0.25rem 0.5rem;
}

.warning-banner {
  background: var(--color-warning-bg);
  border: 1px solid var(--color-warning);
  border-radius: var(--radius);
  padding: 0.75rem;
  margin-bottom: 1rem;
  font-size: 0.85rem;
  color: #92400e;
}

.entry-list {
  list-style: none;
}

.entry-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--color-border);
}

.entry-item:last-child {
  border-bottom: none;
}

.entry-food {
  font-weight: 500;
}

.entry-family {
  color: var(--color-text-light);
  font-size: 0.8rem;
}

.entry-violation {
  color: var(--color-restricted);
}

.meal-group-title {
  font-size: 0.8rem;
  color: var(--color-text-light);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin: 1rem 0 0.5rem;
}

.planner-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 1rem;
}

.planner-day {
  background: var(--color-white);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 0.75rem;
}

.planner-day h3 {
  font-size: 0.85rem;
  margin-bottom: 0.75rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--color-border);
}

.family-status {
  font-size: 0.8rem;
  padding: 0.25rem 0;
}

.family-status.safe {
  color: #16a34a;
}

.family-status.restricted {
  color: var(--color-restricted);
}

.family-detail {
  font-size: 0.7rem;
  color: var(--color-text-light);
  margin-left: 0.25rem;
}

.accordion-item {
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  margin-bottom: 0.5rem;
  overflow: hidden;
}

.accordion-header {
  padding: 0.75rem 1rem;
  background: var(--color-white);
  cursor: pointer;
  font-weight: 500;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.accordion-header:hover {
  background: var(--color-bg);
}

.accordion-body {
  padding: 0 1rem 0.75rem;
  display: none;
}

.accordion-body.open {
  display: block;
}

.accordion-body ul {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.accordion-body li {
  background: var(--color-bg);
  padding: 0.25rem 0.5rem;
  border-radius: 4px;
  font-size: 0.8rem;
}

.accordion-body li.restricted-food {
  background: var(--color-restricted-bg);
  color: var(--color-restricted);
}

.search-input {
  width: 100%;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font-size: 0.9rem;
  margin-bottom: 1rem;
}
// --- Food Families Data ---
var foodFamilies = [
  { id: "nightshade", name: "Nightshade", members: ["tomato", "potato", "bell pepper", "eggplant", "chili pepper"] },
  { id: "grass", name: "Grass/Grain", members: ["wheat", "rice", "corn", "oats", "barley", "rye"] },
  { id: "citrus", name: "Citrus", members: ["orange", "lemon", "grapefruit", "lime", "tangerine"] },
  { id: "legume", name: "Legume", members: ["peanut", "soybean", "lentil", "chickpea", "kidney bean"] },
  { id: "cruciferous", name: "Cruciferous", members: ["broccoli", "cauliflower", "cabbage", "kale", "brussels sprouts"] },
  { id: "dairy", name: "Dairy", members: ["milk", "cheese", "yogurt", "butter", "cream"] },
  { id: "allium", name: "Allium", members: ["onion", "garlic", "leek", "chive", "shallot"] },
  { id: "rose", name: "Rose", members: ["apple", "pear", "strawberry", "almond", "cherry", "peach"] },
  { id: "umbellifer", name: "Umbellifer", members: ["carrot", "celery", "parsley", "dill", "fennel"] },
  { id: "poultry", name: "Poultry", members: ["chicken", "turkey", "duck", "egg"] }
];

// --- localStorage Functions ---
function getFoodLog() {
  var data = localStorage.getItem("rotation-diet-food-log");
  if (data) {
    return JSON.parse(data);
  }
  return [];
}

function saveFoodLog(log) {
  localStorage.setItem("rotation-diet-food-log", JSON.stringify(log));
}

function addFoodEntry(food, familyId, date, meal, isViolation) {
  var log = getFoodLog();
  var entry = {
    id: Date.now().toString(),
    food: food,
    familyId: familyId,
    date: date,
    meal: meal,
    isViolation: isViolation
  };
  log.push(entry);
  saveFoodLog(log);
  return entry;
}

function deleteFoodEntry(entryId) {
  var log = getFoodLog();
  var updated = log.filter(function(entry) {
    return entry.id !== entryId;
  });
  saveFoodLog(updated);
}

// --- Tab Switching ---
var tabs = document.querySelectorAll(".tab");
var sections = document.querySelectorAll("main > section");

tabs.forEach(function(tab) {
  tab.addEventListener("click", function() {
    tabs.forEach(function(t) { t.classList.remove("active"); });
    tab.classList.add("active");

    var target = tab.getAttribute("data-tab");
    sections.forEach(function(section) { section.classList.add("hidden"); });
    document.getElementById("tab-" + target).classList.remove("hidden");

    if (target === "planner") {
      renderPlanner();
    }
    if (target === "families") {
      renderFamilies("");
    }
  });
});

// --- Food Log UI ---
var foodSelect = document.getElementById("food-select");
var logDateInput = document.getElementById("log-date");
var foodForm = document.getElementById("food-form");
var warningDiv = document.getElementById("violation-warning");

logDateInput.value = new Date().toISOString().split("T")[0];

var allFoods = [];
for (var i = 0; i < foodFamilies.length; i++) {
  for (var j = 0; j < foodFamilies[i].members.length; j++) {
    allFoods.push({
      food: foodFamilies[i].members[j],
      familyId: foodFamilies[i].id,
      familyName: foodFamilies[i].name
    });
  }
}
allFoods.sort(function(a, b) { return a.food.localeCompare(b.food); });

for (var i = 0; i < allFoods.length; i++) {
  var option = document.createElement("option");
  option.value = allFoods[i].food;
  option.setAttribute("data-family", allFoods[i].familyId);
  option.textContent = allFoods[i].food + " (" + allFoods[i].familyName + ")";
  foodSelect.appendChild(option);
}

foodForm.addEventListener("submit", function(e) {
  e.preventDefault();
  var selected = foodSelect.options[foodSelect.selectedIndex];
  if (!selected.value) return;

  var familyId = selected.getAttribute("data-family");
  var date = logDateInput.value;
  var meal = document.getElementById("meal-select").value;

  addFoodEntry(selected.value, familyId, date, meal, false);
  foodSelect.value = "";
  warningDiv.innerHTML = "";
  renderLog();
});

function renderLog() {
  var entriesDiv = document.getElementById("log-entries");
  var date = logDateInput.value;
  var log = getFoodLog();
  var todayEntries = log.filter(function(entry) { return entry.date === date; });

  if (todayEntries.length === 0) {
    entriesDiv.innerHTML = '<p style="color: var(--color-text-light); font-size: 0.85rem;">No entries for this date.</p>';
    return;
  }

  var meals = ["breakfast", "lunch", "dinner", "snack"];
  var html = "";

  for (var m = 0; m < meals.length; m++) {
    var mealEntries = todayEntries.filter(function(e) { return e.meal === meals[m]; });
    if (mealEntries.length === 0) continue;

    html += '<p class="meal-group-title">' + meals[m] + '</p><ul class="entry-list">';
    for (var i = 0; i < mealEntries.length; i++) {
      var entry = mealEntries[i];
      html += '<li class="entry-item"><span><span class="entry-food">' +
        entry.food + '</span> <span class="entry-family">(' + entry.familyId +
        ')</span></span><button class="btn btn-danger" onclick="deleteEntry(\'' +
        entry.id + '\')">Remove</button></li>';
    }
    html += '</ul>';
  }

  entriesDiv.innerHTML = html;
}

function deleteEntry(id) {
  deleteFoodEntry(id);
  renderLog();
}

logDateInput.addEventListener("change", function() {
  renderLog();
});

// --- Planner UI ---
function renderPlanner() {
  var grid = document.getElementById("planner-grid");
  var html = "";

  for (var i = 0; i < 4; i++) {
    var date = new Date();
    date.setDate(date.getDate() + i);
    var dateStr = date.toISOString().split("T")[0];
    var label = i === 0 ? "Today" : date.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });

    html += '<div class="planner-day"><h3>' + label + '<br><small style="font-weight:normal;color:var(--color-text-light)">' + dateStr + '</small></h3>';
    html += '<p style="font-size:0.7rem;font-weight:600;color:var(--color-safe);margin-bottom:0.25rem">AVAILABLE</p>';
    for (var f = 0; f < foodFamilies.length; f++) {
      html += '<div class="family-status safe">' + foodFamilies[f].name + '</div>';
    }
    html += '</div>';
  }

  grid.innerHTML = html;
}

// --- Families UI ---
function renderFamilies(filter) {
  var listDiv = document.getElementById("families-list");
  var html = "";

  for (var i = 0; i < foodFamilies.length; i++) {
    var family = foodFamilies[i];
    var nameMatch = family.name.toLowerCase().indexOf(filter) !== -1;
    var memberMatches = family.members.filter(function(m) {
      return m.toLowerCase().indexOf(filter) !== -1;
    });

    if (filter && !nameMatch && memberMatches.length === 0) continue;

    var displayMembers = (filter && !nameMatch) ? memberMatches : family.members;

    html += '<div class="accordion-item"><div class="accordion-header" data-family="' + family.id + '">' +
      '<span>' + family.name + ' (' + family.members.length + ' foods)</span>' +
      '</div>' +
      '<div class="accordion-body" id="acc-' + family.id + '"><ul>';

    for (var j = 0; j < displayMembers.length; j++) {
      html += '<li>' + displayMembers[j] + '</li>';
    }

    html += '</ul></div></div>';
  }

  if (!html) {
    html = '<p style="color:var(--color-text-light);text-align:center;padding:2rem">No matching foods or families found.</p>';
  }

  listDiv.innerHTML = html;

  var headers = listDiv.querySelectorAll(".accordion-header");
  headers.forEach(function(header) {
    header.addEventListener("click", function() {
      var familyId = header.getAttribute("data-family");
      var body = document.getElementById("acc-" + familyId);
      body.classList.toggle("open");
    });
  });
}

document.getElementById("family-search").addEventListener("input", function(e) {
  renderFamilies(e.target.value.toLowerCase());
});

// --- Initialize ---
renderLog();
renderFamilies("");

Your food log is fully functional. You can add entries, see them grouped by meal, remove them, and they persist across page refreshes. Next up, you will add the rotation rule engine that checks whether a food violates the 4-day window.

Add Rotation Rule Checking and Dynamic Planner

Your food log works perfectly. You can add entries, remove them, and everything persists between browser sessions.

But the app is missing its most important feature: enforcing the 4-day rotation rule. Right now, nothing stops someone from logging tomato every single day. In this step, you will write the logic that detects when a food (or any member of its family) was eaten too recently and warns the user before they log it.

In this step, get ready to:
  • Write a violation-checking function that enforces the 4-day rotation rule.
  • Display real-time warnings when a selected food violates the rotation window.
  • Make the planner and families views update dynamically based on your food log.
Write the checkViolation function

The 4-day rotation rule means that once you eat a food, you cannot eat anything from the same food family again until 4 full days have passed. Your function needs to look back through the log and find any same-family entry within that window.

  • In app.js, find the deleteFoodEntry function. Add the following code directly below it:
// --- Rotation Rule Engine ---
function checkViolation(familyId, date) {
  var log = getFoodLog();
  var targetDate = new Date(date);
  var windowStart = new Date(date);
  windowStart.setDate(windowStart.getDate() - 3);

  for (var i = 0; i < log.length; i++) {
    var entry = log[i];
    var entryDate = new Date(entry.date);
    if (entry.familyId === familyId && entryDate >= windowStart && entryDate <= targetDate) {
      var daysSince = Math.floor((targetDate - entryDate) / (1000 * 60 * 60 * 24));
      if (daysSince > 0 && daysSince < 4) {
        return {
          isViolation: true,
          lastFood: entry.food,
          lastDate: entry.date,
          daysSince: daysSince,
          daysRemaining: 4 - daysSince
        };
      }
    }
  }
  return { isViolation: false };
}

What does this code do?

This function takes a food family ID and a date, then checks whether any food from that family was eaten within the last 3 days (day 1 is when you ate it, so you need to wait 3 more days before eating it again on day 5).

  • windowStart.setDate(windowStart.getDate() - 3) creates a 3-day lookback window from the target date.
  • The loop checks every log entry to find one from the same family within that window.
  • If found, it returns details including which food triggered it, how many days ago, and how many days remain.
  • If no match is found, it returns { isViolation: false } meaning the food is safe to eat.

You won't see a change yet. The next chunk adds helper functions that use checkViolation to check entire days at once.

  • Add these two helper functions directly below checkViolation:
function getRestrictedFamilies(date) {
  var restricted = [];
  for (var i = 0; i < foodFamilies.length; i++) {
    var result = checkViolation(foodFamilies[i].id, date);
    if (result.isViolation) {
      result.familyId = foodFamilies[i].id;
      result.familyName = foodFamilies[i].name;
      restricted.push(result);
    }
  }
  return restricted;
}

function getAvailableFamilies(date) {
  var restrictedIds = getRestrictedFamilies(date).map(function(r) {
    return r.familyId;
  });
  return foodFamilies.filter(function(f) {
    return restrictedIds.indexOf(f.id) === -1;
  });
}

What do these helpers do?

  • getRestrictedFamilies(date) loops through all 10 food families and runs checkViolation on each one. It collects families that have a violation into an array.
  • getAvailableFamilies(date) returns the opposite: all families that are NOT restricted on the given date.
  • Save app.js.

You won't see a visible change yet. The next substep wires these functions into your form so you can see warnings in real time.

Wire the violation check into the food log form

Now you will connect the rule engine to your Log tab. The moment a user selects a food from the dropdown, the app will check whether that food family was eaten recently and display a warning if so.

  • In app.js, find the line that populates the food dropdown (the end of the for loop that appends options to foodSelect). Add this event listener directly below that loop, before the foodForm.addEventListener block:
// Check violation on food selection
foodSelect.addEventListener("change", function() {
  var selected = foodSelect.options[foodSelect.selectedIndex];
  if (!selected.value) {
    warningDiv.innerHTML = "";
    return;
  }
  var familyId = selected.getAttribute("data-family");
  var date = logDateInput.value;
  var result = checkViolation(familyId, date);

  if (result.isViolation) {
    warningDiv.innerHTML = '<div class="warning-banner">Warning: "' + result.lastFood +
      '" (' + familyId + ' family) was eaten ' + result.daysSince +
      ' day(s) ago. The 4-day rotation requires waiting ' + result.daysRemaining +
      ' more day(s). You can still log it, but it will be flagged.</div>';
  } else {
    warningDiv.innerHTML = "";
  }
});

What does this code do?

Every time the user picks a food from the dropdown, this listener fires. It grabs the selected food's family, runs checkViolation, and if a violation is found, injects an amber warning banner into the #violation-warning div.

The warning tells the user exactly what triggered it and how many days they need to wait. It still allows logging because tracking is more valuable than blocking.

Next, you need to update the form submit handler so it records whether an entry is a violation. You also need to update the log display to flag violations in red.

  • In app.js, find the foodForm.addEventListener("submit", ...) block. Locate this line:
  var meal = document.getElementById("meal-select").value;

  addFoodEntry(selected.value, familyId, date, meal, false);
  • Replace it with this updated version that checks for violations before saving:
  var meal = document.getElementById("meal-select").value;
  var result = checkViolation(familyId, date);

  addFoodEntry(selected.value, familyId, date, meal, result.isViolation);
  • Now find the renderLog function. Inside the inner for loop, find this line:
      var entry = mealEntries[i];
      html += '<li class="entry-item"><span><span class="entry-food">' +
  • Replace it with this version that applies the entry-violation class when the entry was flagged:
      var entry = mealEntries[i];
      var violationClass = entry.isViolation ? " entry-violation" : "";
      html += '<li class="entry-item"><span><span class="entry-food' + violationClass + '">' +
  • Finally, find the logDateInput.addEventListener("change", ...) block at the bottom of the Food Log UI section. It currently looks like this:
logDateInput.addEventListener("change", function() {
  renderLog();
});
  • Replace it with this version that also re-checks the selected food's violation status when the date changes:
logDateInput.addEventListener("change", function() {
  renderLog();
  foodSelect.dispatchEvent(new Event("change"));
});

Why dispatch a change event on the food select?

When the user changes the date, the violation status of the currently selected food might change too. Dispatching a change event on foodSelect triggers the listener you just wrote, which recalculates and updates the warning banner.

  • Save app.js and switch to your browser.
  • On the Log tab, select a food (e.g. tomato) and click Add to Log.
  • Now select another food from the same family (e.g. potato). You should see an amber warning banner appear above the form.

No warning banner appearing?

  • Make sure your foodSelect.addEventListener("change", ...) block is placed BEFORE the foodForm.addEventListener("submit", ...) block.
  • Open the browser console (right-click, Inspect, then Console tab) and check for red error messages. A typo in your function names or variable references will show here.
  • Confirm you logged the first food successfully before selecting the second one. The warning only appears when there is already a logged entry from the same family.

Why isn't my rotation violation warning showing up when I select a food from the same family?

Make the planner and families views dynamic

Right now your Planner tab shows every family as "Available" regardless of what you have logged. The Families tab does not indicate which families are restricted either. You will now replace both functions so they use your rule engine to display real-time status.

  • In app.js, find your existing renderPlanner function (under the // --- Planner UI --- comment). Replace the entire function with this updated version:
// --- Planner UI ---
function renderPlanner() {
  var grid = document.getElementById("planner-grid");
  var html = "";

  for (var i = 0; i < 4; i++) {
    var date = new Date();
    date.setDate(date.getDate() + i);
    var dateStr = date.toISOString().split("T")[0];
    var label = i === 0 ? "Today" : date.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });

    // Check which families are restricted vs available for this day
    var restricted = getRestrictedFamilies(dateStr);
    var available = getAvailableFamilies(dateStr);

    html += '<div class="planner-day"><h3>' + label + '<br><small style="font-weight:normal;color:var(--color-text-light)">' + dateStr + '</small></h3>';

    if (available.length > 0) {
      html += '<p style="font-size:0.7rem;font-weight:600;color:var(--color-safe);margin-bottom:0.25rem">AVAILABLE</p>';
      for (var a = 0; a < available.length; a++) {
        html += '<div class="family-status safe">' + available[a].name + '</div>';
      }
    }

    if (restricted.length > 0) {
      html += '<p style="font-size:0.7rem;font-weight:600;color:var(--color-restricted);margin-top:0.5rem;margin-bottom:0.25rem">RESTRICTED</p>';
      for (var r = 0; r < restricted.length; r++) {
        html += '<div class="family-status restricted">' + restricted[r].familyName +
          ' <span class="family-detail">' + restricted[r].lastFood + ' ' + restricted[r].daysSince + 'd ago</span></div>';
      }
    }

    html += '</div>';
  }

  grid.innerHTML = html;
}

What changed in renderPlanner?

Instead of blindly listing all families as available, the function now calls getRestrictedFamilies and getAvailableFamilies for each of the 4 days.

Available families appear in green. Restricted families appear in red with a detail note showing which food triggered it and how many days ago. This gives a clear picture of what you can eat on each upcoming day.

  • Save app.js and click the Planner tab in your browser.
  • You should see the food family you logged earlier listed under "RESTRICTED" in red for today's column, with the remaining families in green under "AVAILABLE".

Planner still shows everything as available?

  • Make sure you replaced the ENTIRE old renderPlanner function, not just added code below it. Having two renderPlanner definitions means the second one overwrites the first.
  • Confirm you have food entries in your log for today. Switch back to the Log tab and check that entries are visible under Today's Log.

Help me figure out why my planner is not showing restricted families after I logged food.

  • Now find your existing renderFamilies function (under the // --- Families UI --- comment). Replace the entire function with this updated version:
// --- Families UI ---
function renderFamilies(filter) {
  var listDiv = document.getElementById("families-list");
  var today = new Date().toISOString().split("T")[0];
  var restricted = getRestrictedFamilies(today);
  var restrictedFamilyIds = restricted.map(function(r) { return r.familyId; });

  var html = "";

  for (var i = 0; i < foodFamilies.length; i++) {
    var family = foodFamilies[i];
    var nameMatch = family.name.toLowerCase().indexOf(filter) !== -1;
    var memberMatches = family.members.filter(function(m) {
      return m.toLowerCase().indexOf(filter) !== -1;
    });

    if (filter && !nameMatch && memberMatches.length === 0) continue;

    var displayMembers = (filter && !nameMatch) ? memberMatches : family.members;
    var isRestricted = restrictedFamilyIds.indexOf(family.id) !== -1;
    var statusColor = isRestricted ? "var(--color-restricted)" : "var(--color-safe)";
    var statusText = isRestricted ? "Restricted" : "Available";

    html += '<div class="accordion-item"><div class="accordion-header" data-family="' + family.id + '">' +
      '<span>' + family.name + ' (' + family.members.length + ' foods)</span>' +
      '<span style="font-size:0.75rem;color:' + statusColor + '">' + statusText + '</span></div>' +
      '<div class="accordion-body" id="acc-' + family.id + '"><ul>';

    for (var j = 0; j < displayMembers.length; j++) {
      var isRestrictedFood = isRestricted ? ' class="restricted-food"' : '';
      html += '<li' + isRestrictedFood + '>' + displayMembers[j] + '</li>';
    }

    html += '</ul></div></div>';
  }

  if (!html) {
    html = '<p style="color:var(--color-text-light);text-align:center;padding:2rem">No matching foods or families found.</p>';
  }

  listDiv.innerHTML = html;

  // Accordion click handlers
  var headers = listDiv.querySelectorAll(".accordion-header");
  headers.forEach(function(header) {
    header.addEventListener("click", function() {
      var familyId = header.getAttribute("data-family");
      var body = document.getElementById("acc-" + familyId);
      body.classList.toggle("open");
    });
  });
}

What changed in renderFamilies?

The function now checks today's restricted families at the top. Each accordion header displays either "Available" in green or "Restricted" in red on the right side. Individual food items inside a restricted family get the restricted-food class, which highlights them with a red background.

  • Save app.js and click the Families tab.
  • You should see the food family you logged earlier showing "Restricted" in red text on the right side of its accordion header. All other families should show "Available" in green.
  • Click the restricted family's header to expand it. The individual foods inside should have a red-tinted background.

Families tab not showing status colors?

  • Check that you did not accidentally leave the old renderFamilies function below the new one. JavaScript will use whichever definition comes last.
  • Ensure the CSS class .accordion-body li.restricted-food exists in your style.css. It was added in Step 2.

Why is my Families tab not showing the Available or Restricted status labels?

✔️ Awesome, I've got everything!

Great. Double check you have saved app.js and that all three tabs (Log, Planner, Families) are reflecting your food log data dynamically.

ⓧ I'd like to double check the full code

Here is the complete app.js file as it should look after this step. Compare your code against it:

// --- Food Families Data ---
var foodFamilies = [
  { id: "nightshade", name: "Nightshade", members: ["tomato", "potato", "bell pepper", "eggplant", "chili pepper"] },
  { id: "grass", name: "Grass/Grain", members: ["wheat", "rice", "corn", "oats", "barley", "rye"] },
  { id: "citrus", name: "Citrus", members: ["orange", "lemon", "grapefruit", "lime", "tangerine"] },
  { id: "legume", name: "Legume", members: ["peanut", "soybean", "lentil", "chickpea", "kidney bean"] },
  { id: "cruciferous", name: "Cruciferous", members: ["broccoli", "cauliflower", "cabbage", "kale", "brussels sprouts"] },
  { id: "dairy", name: "Dairy", members: ["milk", "cheese", "yogurt", "butter", "cream"] },
  { id: "allium", name: "Allium", members: ["onion", "garlic", "leek", "chive", "shallot"] },
  { id: "rose", name: "Rose", members: ["apple", "pear", "strawberry", "almond", "cherry", "peach"] },
  { id: "umbellifer", name: "Umbellifer", members: ["carrot", "celery", "parsley", "dill", "fennel"] },
  { id: "poultry", name: "Poultry", members: ["chicken", "turkey", "duck", "egg"] }
];

// --- localStorage Functions ---
function getFoodLog() {
  var data = localStorage.getItem("rotation-diet-food-log");
  if (data) {
    return JSON.parse(data);
  }
  return [];
}

function saveFoodLog(log) {
  localStorage.setItem("rotation-diet-food-log", JSON.stringify(log));
}

function addFoodEntry(food, familyId, date, meal, isViolation) {
  var log = getFoodLog();
  var entry = {
    id: Date.now().toString(),
    food: food,
    familyId: familyId,
    date: date,
    meal: meal,
    isViolation: isViolation
  };
  log.push(entry);
  saveFoodLog(log);
  return entry;
}

function deleteFoodEntry(entryId) {
  var log = getFoodLog();
  var updated = log.filter(function(entry) {
    return entry.id !== entryId;
  });
  saveFoodLog(updated);
}

// --- Rotation Rule Engine ---
function checkViolation(familyId, date) {
  var log = getFoodLog();
  var targetDate = new Date(date);
  var windowStart = new Date(date);
  windowStart.setDate(windowStart.getDate() - 3);

  for (var i = 0; i < log.length; i++) {
    var entry = log[i];
    var entryDate = new Date(entry.date);
    if (entry.familyId === familyId && entryDate >= windowStart && entryDate <= targetDate) {
      var daysSince = Math.floor((targetDate - entryDate) / (1000 * 60 * 60 * 24));
      if (daysSince > 0 && daysSince < 4) {
        return {
          isViolation: true,
          lastFood: entry.food,
          lastDate: entry.date,
          daysSince: daysSince,
          daysRemaining: 4 - daysSince
        };
      }
    }
  }
  return { isViolation: false };
}

function getRestrictedFamilies(date) {
  var restricted = [];
  for (var i = 0; i < foodFamilies.length; i++) {
    var result = checkViolation(foodFamilies[i].id, date);
    if (result.isViolation) {
      result.familyId = foodFamilies[i].id;
      result.familyName = foodFamilies[i].name;
      restricted.push(result);
    }
  }
  return restricted;
}

function getAvailableFamilies(date) {
  var restrictedIds = getRestrictedFamilies(date).map(function(r) {
    return r.familyId;
  });
  return foodFamilies.filter(function(f) {
    return restrictedIds.indexOf(f.id) === -1;
  });
}

// --- Tab Switching ---
var tabs = document.querySelectorAll(".tab");
var sections = document.querySelectorAll("main > section");

tabs.forEach(function(tab) {
  tab.addEventListener("click", function() {
    tabs.forEach(function(t) { t.classList.remove("active"); });
    tab.classList.add("active");

    var target = tab.getAttribute("data-tab");
    sections.forEach(function(section) { section.classList.add("hidden"); });
    document.getElementById("tab-" + target).classList.remove("hidden");

    if (target === "planner") {
      renderPlanner();
    }
    if (target === "families") {
      renderFamilies("");
    }
  });
});

// --- Food Log UI ---
var foodSelect = document.getElementById("food-select");
var logDateInput = document.getElementById("log-date");
var foodForm = document.getElementById("food-form");
var warningDiv = document.getElementById("violation-warning");

// Set today's date as default
logDateInput.value = new Date().toISOString().split("T")[0];

// Populate food dropdown
var allFoods = [];
for (var i = 0; i < foodFamilies.length; i++) {
  for (var j = 0; j < foodFamilies[i].members.length; j++) {
    allFoods.push({
      food: foodFamilies[i].members[j],
      familyId: foodFamilies[i].id,
      familyName: foodFamilies[i].name
    });
  }
}
allFoods.sort(function(a, b) { return a.food.localeCompare(b.food); });

for (var i = 0; i < allFoods.length; i++) {
  var option = document.createElement("option");
  option.value = allFoods[i].food;
  option.setAttribute("data-family", allFoods[i].familyId);
  option.textContent = allFoods[i].food + " (" + allFoods[i].familyName + ")";
  foodSelect.appendChild(option);
}

// Check violation on food selection
foodSelect.addEventListener("change", function() {
  var selected = foodSelect.options[foodSelect.selectedIndex];
  if (!selected.value) {
    warningDiv.innerHTML = "";
    return;
  }
  var familyId = selected.getAttribute("data-family");
  var date = logDateInput.value;
  var result = checkViolation(familyId, date);

  if (result.isViolation) {
    warningDiv.innerHTML = '<div class="warning-banner">Warning: "' + result.lastFood +
      '" (' + familyId + ' family) was eaten ' + result.daysSince +
      ' day(s) ago. The 4-day rotation requires waiting ' + result.daysRemaining +
      ' more day(s). You can still log it, but it will be flagged.</div>';
  } else {
    warningDiv.innerHTML = "";
  }
});

// Submit food log
foodForm.addEventListener("submit", function(e) {
  e.preventDefault();
  var selected = foodSelect.options[foodSelect.selectedIndex];
  if (!selected.value) return;

  var familyId = selected.getAttribute("data-family");
  var date = logDateInput.value;
  var meal = document.getElementById("meal-select").value;
  var result = checkViolation(familyId, date);

  addFoodEntry(selected.value, familyId, date, meal, result.isViolation);
  foodSelect.value = "";
  warningDiv.innerHTML = "";
  renderLog();
});

function renderLog() {
  var entriesDiv = document.getElementById("log-entries");
  var date = logDateInput.value;
  var log = getFoodLog();
  var todayEntries = log.filter(function(entry) { return entry.date === date; });

  if (todayEntries.length === 0) {
    entriesDiv.innerHTML = '<p style="color: var(--color-text-light); font-size: 0.85rem;">No entries for this date.</p>';
    return;
  }

  var meals = ["breakfast", "lunch", "dinner", "snack"];
  var html = "";

  for (var m = 0; m < meals.length; m++) {
    var mealEntries = todayEntries.filter(function(e) { return e.meal === meals[m]; });
    if (mealEntries.length === 0) continue;

    html += '<p class="meal-group-title">' + meals[m] + '</p><ul class="entry-list">';
    for (var i = 0; i < mealEntries.length; i++) {
      var entry = mealEntries[i];
      var violationClass = entry.isViolation ? " entry-violation" : "";
      html += '<li class="entry-item"><span><span class="entry-food' + violationClass + '">' +
        entry.food + '</span> <span class="entry-family">(' + entry.familyId +
        ')</span></span><button class="btn btn-danger" onclick="deleteEntry(\'' +
        entry.id + '\')">Remove</button></li>';
    }
    html += '</ul>';
  }

  entriesDiv.innerHTML = html;
}

function deleteEntry(id) {
  deleteFoodEntry(id);
  renderLog();
}

logDateInput.addEventListener("change", function() {
  renderLog();
  foodSelect.dispatchEvent(new Event("change"));
});

// --- Planner UI ---
function renderPlanner() {
  var grid = document.getElementById("planner-grid");
  var html = "";

  for (var i = 0; i < 4; i++) {
    var date = new Date();
    date.setDate(date.getDate() + i);
    var dateStr = date.toISOString().split("T")[0];
    var label = i === 0 ? "Today" : date.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });

    var restricted = getRestrictedFamilies(dateStr);
    var available = getAvailableFamilies(dateStr);

    html += '<div class="planner-day"><h3>' + label + '<br><small style="font-weight:normal;color:var(--color-text-light)">' + dateStr + '</small></h3>';

    if (available.length > 0) {
      html += '<p style="font-size:0.7rem;font-weight:600;color:var(--color-safe);margin-bottom:0.25rem">AVAILABLE</p>';
      for (var a = 0; a < available.length; a++) {
        html += '<div class="family-status safe">' + available[a].name + '</div>';
      }
    }

    if (restricted.length > 0) {
      html += '<p style="font-size:0.7rem;font-weight:600;color:var(--color-restricted);margin-top:0.5rem;margin-bottom:0.25rem">RESTRICTED</p>';
      for (var r = 0; r < restricted.length; r++) {
        html += '<div class="family-status restricted">' + restricted[r].familyName +
          ' <span class="family-detail">' + restricted[r].lastFood + ' ' + restricted[r].daysSince + 'd ago</span></div>';
      }
    }

    html += '</div>';
  }

  grid.innerHTML = html;
}

// --- Families UI ---
function renderFamilies(filter) {
  var listDiv = document.getElementById("families-list");
  var today = new Date().toISOString().split("T")[0];
  var restricted = getRestrictedFamilies(today);
  var restrictedFamilyIds = restricted.map(function(r) { return r.familyId; });

  var html = "";

  for (var i = 0; i < foodFamilies.length; i++) {
    var family = foodFamilies[i];
    var nameMatch = family.name.toLowerCase().indexOf(filter) !== -1;
    var memberMatches = family.members.filter(function(m) {
      return m.toLowerCase().indexOf(filter) !== -1;
    });

    if (filter && !nameMatch && memberMatches.length === 0) continue;

    var displayMembers = (filter && !nameMatch) ? memberMatches : family.members;
    var isRestricted = restrictedFamilyIds.indexOf(family.id) !== -1;
    var statusColor = isRestricted ? "var(--color-restricted)" : "var(--color-safe)";
    var statusText = isRestricted ? "Restricted" : "Available";

    html += '<div class="accordion-item"><div class="accordion-header" data-family="' + family.id + '">' +
      '<span>' + family.name + ' (' + family.members.length + ' foods)</span>' +
      '<span style="font-size:0.75rem;color:' + statusColor + '">' + statusText + '</span></div>' +
      '<div class="accordion-body" id="acc-' + family.id + '"><ul>';

    for (var j = 0; j < displayMembers.length; j++) {
      var isRestrictedFood = isRestricted ? ' class="restricted-food"' : '';
      html += '<li' + isRestrictedFood + '>' + displayMembers[j] + '</li>';
    }

    html += '</ul></div></div>';
  }

  if (!html) {
    html = '<p style="color:var(--color-text-light);text-align:center;padding:2rem">No matching foods or families found.</p>';
  }

  listDiv.innerHTML = html;

  // Accordion click handlers
  var headers = listDiv.querySelectorAll(".accordion-header");
  headers.forEach(function(header) {
    header.addEventListener("click", function() {
      var familyId = header.getAttribute("data-family");
      var body = document.getElementById("acc-" + familyId);
      body.classList.toggle("open");
    });
  });
}

// Search filter for families
document.getElementById("family-search").addEventListener("input", function(e) {
  renderFamilies(e.target.value.toLowerCase());
});

// --- Initialize ---
renderLog();
renderFamilies("");

Your rotation diet tracker is now fully functional. It enforces the 4-day rule in real time, shows warnings before you log a violation, and dynamically updates the planner and families views based on your actual food log. Next up, you can take on the Secret Mission and connect your app to a real recipe API.

Secret mission

Fetch Recipe Suggestions from an API

Your planner shows which food families are available today. But what can you actually cook with those ingredients? In this mission, connect your tracker to a free recipe API so it suggests real meals based on what you're allowed to eat.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. All data is stored in your browser's localStorage and in files on your computer.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The rotation-diet-tracker folder on your Desktop (contains index.html, style.css, and app.js).
  • Browser localStorage data (your food log entries).
  • Live Server extension in VS Code.

Keep everything running

No action needed. Choose this if you're still actively using the rotation diet tracker or plan to keep building on it.

  • Your rotation-diet-tracker folder stays on your Desktop.
  • Your food log data persists in localStorage between browser sessions.
  • To reopen the app, open the folder in VS Code, right-click index.html, and select Open with Live Server.
  • The Live Server extension remains installed in VS Code for future projects.

Pause - I'll come back to this later

Stop the running server to free up the port, but keep all your files and data intact.

  • In VS Code, click Go Live in the bottom status bar to stop Live Server (or press Cmd+L, Cmd+C on macOS).
  • Close VS Code.
  • Your files and localStorage data remain untouched for when you return.

Delete - I don't want to use this again

Remove all project files and stored data. There are no cloud resources or accounts to clean up.

  • Stop Live Server if it is still running (click Go Live in the VS Code status bar).
  • Close VS Code.
  • Delete the project folder by running this command in your terminal:
rm -rf ~/Desktop/rotation-diet-tracker

On Windows, run this instead:

rmdir /s /q "$HOME\Desktop\rotation-diet-tracker"
  • Clear your food log data from localStorage. Open your browser, press Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows) to open DevTools.
  • Go to the Application tab, expand Local Storage in the sidebar, select http://127.0.0.1:5500, and click Clear All.
  • Optionally, uninstall the Live Server extension. In VS Code, open the Extensions sidebar (Cmd+Shift+X on macOS or Ctrl+Shift+X on Windows), find Live Server, and click Uninstall.

Nice Work!

Nice Work!

You just built a fully functional rotation diet tracker from scratch using HTML, CSS, and JavaScript. This app solves a real problem for people with food sensitivities, and you built every piece of it yourself.

Here's what you learned:

  • Built a responsive web app with tabbed navigation, CSS custom properties for a color system, and grid layout for the 4-day planner.
  • Wrote your first JavaScript from scratch: event listeners, DOM manipulation, localStorage for persistent data, and dynamic HTML rendering.
  • Implemented rotation diet logic with date arithmetic to enforce the 4-day rule, and built a dynamic planner that updates based on your food log.
  • Secret Mission: Connected to a real external API (TheMealDB) using fetch() and displayed recipe suggestions based on available ingredients.

Ready to quiz yourself?