web app solo dev

GreenPlate

image coming key art

ABOUT THIS PROJECT

Cook what you already have, and see what it costs the planet.

Most recipe sites assume you are willing to go shopping. GreenPlate starts from the ingredients already in your kitchen and works outward, and it attaches an eco-score to every result so the environmental impact of a meal is visible before you commit to it. A React front end against a Node backend, with accounts and saved favorites.

A typed API on both sides

Express server in TypeScript with recipe endpoints over GET and POST and full CRUD on favorites through GET, POST, PUT and DELETE. Client and server share the same type definitions, so a change to the recipe shape fails at compile time on both ends rather than at runtime in the browser.

image coming search

Auth, and removing the hardcoded user

Google OAuth through Firebase, an AuthContext holding auth state on the client, and the Admin SDK verifying tokens on the server. The first version had a hardcoded user id standing in for a real account; replacing that with the authenticated user's uid is what turned favorites from a global list into per-user data, and it touched the navbar, the profile page and every favorites route.

image coming eco-scores

Persistence in Firestore

Moved storage into Firestore so accounts and saved recipes survive a server restart, which the in-memory version did not. Favorites are keyed by uid, so the profile page reads only that user's rows rather than filtering a shared collection on the client.

image coming favorites

The eco-score itself

Every recipe starts at a neutral 5 and is adjusted from its ingredient list and its nutrition data: ingredient categories move it in steps of 4, 2, 1 and -3 depending on how they are produced, then high fiber pulls it down as a plant-based signal and high saturated fat pushes it up as an animal-product one. The result is clamped to 1 through 10 so no single ingredient can blow out the scale. Recipes come from the Spoonacular API, with the detail requests fanned out through Promise.all rather than awaited in sequence, since the search returns ten at a time.