Skip to content
selslim-devPublic

About

Recipe discovery web app demonstrating MVC architecture in vanilla JavaScript — features live search with pagination, dynamic servings recalculation, bookmark persistence via localStorage, and recipe uploads via a REST API.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 

Repository files navigation

🍳 Forkify — Recipe Search & Discovery App

JavaScript Parcel HTML5 CSS3 Sass License

A recipe discovery web app built with vanilla JavaScript, demonstrating a full MVC architecture — no frameworks, just modern JS.

Live Demo · Report Bug · Request Feature


📖 Table of Contents


🔎 Overview

Forkify lets you search thousands of recipes, view detailed cooking instructions and ingredients, dynamically scale servings (with live ingredient recalculation), bookmark your favorites with persistent storage, and even upload your own custom recipes to share with others.

This project was built as a deep dive into vanilla JavaScript architecture — no React, no Vue, no state management library. Every pattern here (MVC, pub-sub, DOM diffing) is hand-rolled to demonstrate a genuine understanding of how those abstractions work under the hood.


✨ Features

  • 🔍 Live recipe search with pagination through large result sets
  • 📋 Detailed recipe view — ingredients, cooking time, servings, source link
  • ⚖️ Dynamic servings adjustment — ingredient quantities recalculate in real time (using fraction display, e.g. ¼ cup instead of 0.25 cup)
  • 🔖 Bookmarking system with localStorage persistence — your bookmarks survive a page refresh
  • 📤 Upload your own recipes via a custom form, parsed and sent to a live API
  • ⚡ Optimized re-rendering — a custom DOM-diffing algorithm updates only the elements that changed (e.g. servings numbers) instead of re-rendering the whole view, preserving scroll position and avoiding UI flicker
  • 🌐 Hash-based routing — shareable URLs for individual recipes (#recipeId)
  • ⏱️ Request timeout handling — API calls fail gracefully after a set timeout instead of hanging indefinitely
  • 💥 Robust error handling — user-friendly error states for failed searches, missing recipes, and network issues

🛠️ Tech Stack

Category Technology
Language JavaScript (ES6+) — classes, async/await, destructuring, optional chaining, private fields
Bundler Parcel 2 (zero-config bundling, hot module reloading, asset imports)
Markup / Styling Semantic HTML5, CSS3 (Flexbox, Grid, custom properties)
Polyfills core-js, regenerator-runtime (for older browser support of async/await and modern JS features)
Utilities fractional (converts decimal quantities into readable fractions, e.g. 1.5 → 1½)
API Forkify API — recipe search, retrieval, and user-submitted recipe uploads
Version Control Git & GitHub

🏗️ Architecture

This project follows the MVC (Model-View-Controller) pattern, entirely hand-built without a framework:

┌─────────────┐      ┌──────────────┐      ┌─────────────┐
│    Model    │◄────►│  Controller  │◄────►│    Views    │
│ (state.js)  │      │(controller.js)│      │(*.view.js) │
└─────────────┘      └──────────────┘      └─────────────┘
      │                                            │
      ▼                                            ▼
 API calls,                                  DOM rendering,
 data transforms,                            event listeners,
 localStorage                                pub-sub handlers

Key architectural decisions:

  • Publisher-Subscriber pattern — views never call the controller directly. Instead, each view exposes an addHandlerX(handler) method that subscribes a controller function to a DOM event (click, hashchange, submit). This keeps views decoupled from business logic entirely.
  • Abstract base View class — all views (RecipeView, ResultsView, BookmarksView, PaginationView, AddRecipeView) extend a shared View class that provides render(), renderSpinner(), renderError(), and a custom update() method for surgical DOM patching.
  • Single source of truth — a central state object in model.js holds all application data (current recipe, search results, bookmarks, pagination). Views are always a pure reflection of state — they never hold their own independent data.

📸 Screenshots

📷 [Homepage / Search Results]
📷 [Recipe Detail View]
📷 [Servings Adjustment in Action (GIF)]
📷 [Bookmarks Panel]
📷 [Upload Recipe Form]

Tip: use ScreenToGif (Windows) or Kap (Mac) to record short GIFs of interactions like servings adjustment or bookmarking — these are what recruiters/visitors notice first.


🚀 Getting Started

Prerequisites

  • Node.js (v16+ recommended)
  • npm or yarn

Installation

# Clone the repository
git clone https://github.com/yourusername/forkify.git

# Navigate into the project
cd forkify

# Install dependencies
npm install

# Start the development server
npm start

The app will open automatically at http://localhost:1234 (or your configured Parcel port).

Build for production

npm run build

📁 Project Structure

forkify/
├── src/
│   ├── js/
│   │   ├── model.js              # State + API calls + business logic
│   │   ├── controller.js         # Glue between model and views
│   │   ├── config.js             # API URLs, timeouts, constants
│   │   ├── helpers.js            # AJAX helper with timeout race
│   │   └── views/
│   │       ├── view.js           # Abstract base View class
│   │       ├── recipeView.js
│   │       ├── searchView.js
│   │       ├── resultsView.js
│   │       ├── paginationView.js
│   │       ├── bookmarksView.js
│   │       ├── previewView.js
│   │       └── addRecipeView.js
│   ├── img/
│   │   └── icons.svg
│   ├── sass/                     # (if applicable)
│   └── index.html
├── package.json
└── README.md

🐞 Key Engineering Challenges & Fixes

Building this project surfaced a number of real-world JavaScript bugs — the kind that don't show up in tutorials but absolutely show up in production code. Documenting them here as a demonstration of debugging process, not just feature-building:

Bug Root Cause Fix
Icons failing to load app-wide url: asset import already resolves to the full icon sprite URL, but .svg was appended a second time (${icons}.svg#icon-x) Removed the redundant extension across all <use href> references
Ingredients list crashing on render Private method #generateMarkupIngredient was invoked via a mismatched public reference (this._generateMarkupIngredient) Corrected the method name to match its declaration
Pagination buttons never appeared _generateMarkup() computed the correct markup but discarded it — every code path fell through to an unconditional return '' Added explicit return statements to each conditional branch
Wrong bookmark always got deleted Assignment operator (=) used instead of comparison (===) inside Array.prototype.findIndex, silently corrupting the object's id in the process Fixed to el.id === id; added regression awareness around =/=== bugs, since they fail silently rather than throwing
Un-bookmarking silently failed A leftover duplicate function call re-added the bookmark immediately after deleting it, in the same click handler Removed the redundant call
update() crashing on list-type views The custom DOM-diffing algorithm assumes matching structure length between old and new DOM — safe for value-only updates (servings), unsafe for variable-length lists (bookmarks, search results) Restricted update() to fixed-structure views only; list views use full render() instead
"Method is not a function" runtime errors Typos between a method's declaration and its call site (_addHandlerHideWidnow vs. _addHandlerHideWindow), compounded by Parcel's aggressive build caching masking whether a fix had actually taken effect Corrected method name mismatches; adopted a habit of clearing .parcel-cache and dist after structural changes
Async race conditions on rapid search Fast repeated searches could resolve out of order, rendering stale results over fresh ones Addressed via consistent state-driven rendering (render always reads from state, never from a captured local closure)

🗺️ Roadmap

Planned improvements to keep pushing this project further:

  • TypeScript migration — add static typing across model, views, and API response shapes to catch a whole class of bugs (like the ones documented above) at compile time instead of runtime
  • Unit & integration tests (Jest) for model logic — especially bookmark CRUD operations and ingredient parsing
  • Debounced search input to reduce redundant API calls while typing
  • Dark mode toggle with persisted preference
  • Recipe categories/filters (cuisine, dietary restrictions, cooking time)
  • Skeleton loading states instead of a single spinner, for a more polished perceived-performance feel
  • PWA support — offline access to bookmarked recipes via service workers
  • Accessibility audit — ARIA labels, keyboard navigation, focus trapping in the upload modal
  • CI pipeline (GitHub Actions) — lint + build check on every push

💡 What I Learned

This project was as much about debugging discipline as it was about writing new features. Key takeaways:

  • How silent failures (like = vs ===, or a missing return in a conditional branch) are far more dangerous than thrown errors — they don't announce themselves, they just quietly produce wrong behavior.
  • Why bundler caching (Parcel specifically) can make a correct fix look like it didn't work, and the discipline of clearing .parcel-cache/dist before concluding a fix failed.
  • The tradeoffs of hand-rolled DOM diffing vs. reaching for a framework — and why frameworks like React exist once you've felt the edge cases of maintaining your own update() method.
  • The real value of the Publisher-Subscriber pattern for keeping vanilla JS codebases maintainable as they grow past a handful of files.

🙏 Acknowledgements


📄 License

This project is licensed under the MIT License — see the LICENSE file for details.


⭐ If you found this project interesting, consider giving it a star!

About

Recipe discovery web app demonstrating MVC architecture in vanilla JavaScript — features live search with pagination, dynamic servings recalculation, bookmark persistence via localStorage, and recipe uploads via a REST API.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages