Skip to content

Repository files navigation

ProductPort — Meal & Recipe Planner

ProductPort is a local-first, offline-capable web application that turns a short onboarding questionnaire into a personalized weekly meal plan. It recommends recipes that respect the user's diet, allergies, budget, cooking ability, and health goals, calculates calorie/macronutrient splits, and lists missing ingredients for recipes matched against items in their fridge.


🚀 Run & Build Instructions

Follow these steps to run, test, and build the application locally:

1. Installation

Clone the repository and install dependencies:

npm install

2. Development Server

Start the local Vite development server:

npm run dev

3. Running Tests

Run the Vitest test suite once (includes unit, integration, and accessibility checks):

npm run test --run

Or run the tests in interactive watch mode:

npm run test

4. Linting

Run oxlint code linting:

npm run lint

5. Production Build

Verify type checks and compile optimized static assets:

npm run build

Once the build is complete, run the production preview server locally:

npm run preview

🏛️ Architectural Overview

ProductPort is designed using a clean, layered architecture with a clear separation between the UI components, business logic engines, and database persistence layers.

graph TD
  UI[React UI Components] --> Store[Zustand Stores - src/app/store.ts]
  Store --> Root[Composition Root - src/app/compositionRoot.ts]
  Root --> Engines[Core Logic Engines - src/core/]
  Engines --> Recommender[RuleBasedRecommender]
  Engines --> Nutrition[StaticNutritionProvider]
  Store --> Repos[Repository Helpers - src/data/repo.ts]
  Repos --> DB[(Dexie IndexedDB - src/data/db.ts)]
Loading

Core Architecture Components:

  1. React UI (src/features/ & src/components/): Mobile-first layouts representing screens (Recipes library, Planner, Onboarding, Profile settings, Fridge ingredient matcher).
  2. State Management (src/app/store.ts): Zustand stores containing application states (UI modal state, profile details, active weekly plans, and user pantry items).
  3. Local Database (src/data/db.ts & src/data/repo.ts): Local-first storage powered by Dexie and IndexedDB. Fully functional offline with zero server dependencies.
  4. Logic Engines (src/core/): Contains clean, pure TypeScript business logic including nutrition.ts (BMR/TDEE calculations), ruleBasedRecommender.ts (meal planning & constraints verification), and fridgeMatcher.ts (ingredients mapping).
  5. Offline Support: App assets and skeleton are cached offline using a Service Worker (public/sw.js). All dynamic data persists locally in IndexedDB, meaning ProductPort runs fully offline.

🔌 Plugging in an AI Recommendation Provider

To support clean drop-in replacements, ProductPort abstracts planning and scoring behind provider interfaces. You can plug in a future AIRecommendationProvider without modifying any UI components or Zustand stores.

Interface Contracts

All recommendation logic must conform to the RecommendationProvider interface defined in src/core/providers.ts:

export interface RecommendationProvider {
  scoreRecipes(candidates: Recipe[], ctx: RecommendationContext): ScoredRecipe[];
  generateWeeklyPlan(input: PlanInput): MealPlan;
  suggestSwap(
    dayOfWeek: number,
    slot: MealType,
    plan: MealPlan,
    recipes: Recipe[],
    profile: UserProfile
  ): Recipe[];
}

Steps to Integrate a Custom Provider:

  1. Create the Provider: Create a new file src/core/aiRecommender.ts implementing RecommendationProvider:

    import type { RecommendationProvider, RecommendationContext, PlanInput, ScoredRecipe } from './providers';
    import type { Recipe, UserProfile, MealPlan, MealType } from '../data/types';
    
    export class AIRecommendationProvider implements RecommendationProvider {
      scoreRecipes(candidates: Recipe[], ctx: RecommendationContext): ScoredRecipe[] {
        // Call your AI model endpoint or local LLM logic here to score candidates
        return []; 
      }
    
      generateWeeklyPlan(input: PlanInput): MealPlan {
        // Call LLM logic to generate a full 7-day meal plan
        return {} as MealPlan;
      }
    
      suggestSwap(dayOfWeek: number, slot: MealType, plan: MealPlan, recipes: Recipe[], profile: UserProfile): Recipe[] {
        // AI-driven meal alternatives suggestions
        return [];
      }
    }
  2. Register in the Composition Root: Open the composition root file src/app/compositionRoot.ts and swap out the provider:

    import { AIRecommendationProvider } from '../core/aiRecommender'; // 1. Import new provider
    import { StaticNutritionProvider } from '../core/staticNutritionProvider';
    import type { RecommendationProvider, NutritionProvider } from '../core/providers';
    
    export interface AppProviders {
      recommender: RecommendationProvider;
      nutritionProvider: NutritionProvider;
    }
    
    export const providers: AppProviders = {
      recommender: new AIRecommendationProvider(), // 2. Instantiate new provider here!
      nutritionProvider: new StaticNutritionProvider(),
    };
    
    export function setProviders(newProviders: Partial<AppProviders>) {
      Object.assign(providers, newProviders);
    }

No UI file or store changes are required, as they consume recommendations directly via providers.recommender references managed by the composition root.

About

Meal planning and recipe generation

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages