feat: add barcode scanner with OpenFoodFacts integration
All checks were successful
CI / update (push) Successful in 5m26s
All checks were successful
CI / update (push) Successful in 5m26s
- Camera-based barcode scanning in FoodSearch using barcode-detector (ZXing WASM) - Import script to load OFF MongoDB dump into lean openfoodfacts collection with kJ→kcal fallback and dedup handling - Barcode lookup API with live OFF API fallback that caches results locally, progressively enhancing the local database - Add 'off' source to food log, custom meal, and favorite ingredient models - OpenFoodFact mongoose model for the openfoodfacts collection
This commit is contained in:
@@ -2,7 +2,7 @@ import mongoose from 'mongoose';
|
||||
|
||||
interface ICustomMealIngredient {
|
||||
name: string;
|
||||
source: 'bls' | 'usda' | 'custom';
|
||||
source: 'bls' | 'usda' | 'custom' | 'off';
|
||||
sourceId?: string;
|
||||
amountGrams: number;
|
||||
portions?: { description: string; grams: number }[];
|
||||
@@ -54,7 +54,7 @@ const PortionSchema = new mongoose.Schema({
|
||||
|
||||
const IngredientSchema = new mongoose.Schema({
|
||||
name: { type: String, required: true, trim: true },
|
||||
source: { type: String, enum: ['bls', 'usda', 'custom'], required: true },
|
||||
source: { type: String, enum: ['bls', 'usda', 'custom', 'off'], required: true },
|
||||
sourceId: { type: String },
|
||||
amountGrams: { type: Number, required: true, min: 0 },
|
||||
portions: { type: [PortionSchema], default: undefined },
|
||||
|
||||
@@ -2,7 +2,7 @@ import mongoose from 'mongoose';
|
||||
|
||||
const FavoriteIngredientSchema = new mongoose.Schema(
|
||||
{
|
||||
source: { type: String, enum: ['bls', 'usda'], required: true },
|
||||
source: { type: String, enum: ['bls', 'usda', 'off'], required: true },
|
||||
sourceId: { type: String, required: true },
|
||||
name: { type: String, required: true },
|
||||
createdBy: { type: String, required: true },
|
||||
@@ -13,7 +13,7 @@ const FavoriteIngredientSchema = new mongoose.Schema(
|
||||
FavoriteIngredientSchema.index({ createdBy: 1, source: 1, sourceId: 1 }, { unique: true });
|
||||
|
||||
interface IFavoriteIngredient {
|
||||
source: 'bls' | 'usda';
|
||||
source: 'bls' | 'usda' | 'off';
|
||||
sourceId: string;
|
||||
name: string;
|
||||
createdBy: string;
|
||||
|
||||
@@ -5,7 +5,7 @@ interface IFoodLogEntry {
|
||||
date: Date;
|
||||
mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack';
|
||||
name: string;
|
||||
source: 'bls' | 'usda' | 'recipe' | 'custom';
|
||||
source: 'bls' | 'usda' | 'recipe' | 'custom' | 'off';
|
||||
sourceId?: string;
|
||||
amountGrams: number;
|
||||
per100g: {
|
||||
@@ -47,7 +47,7 @@ const FoodLogEntrySchema = new mongoose.Schema(
|
||||
date: { type: Date, required: true },
|
||||
mealType: { type: String, enum: ['breakfast', 'lunch', 'dinner', 'snack'], required: true },
|
||||
name: { type: String, required: true, trim: true },
|
||||
source: { type: String, enum: ['bls', 'usda', 'recipe', 'custom'], required: true },
|
||||
source: { type: String, enum: ['bls', 'usda', 'recipe', 'custom', 'off'], required: true },
|
||||
sourceId: { type: String },
|
||||
amountGrams: { type: Number, required: true, min: 0 },
|
||||
per100g: { type: NutritionSnapshotSchema, required: true },
|
||||
|
||||
53
src/models/OpenFoodFact.ts
Normal file
53
src/models/OpenFoodFact.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
interface IOpenFoodFact {
|
||||
barcode: string;
|
||||
name: string;
|
||||
nameDe?: string;
|
||||
brands?: string;
|
||||
category?: string;
|
||||
nutriscore?: string;
|
||||
productQuantityG?: number;
|
||||
serving?: { description: string; grams: number };
|
||||
per100g: {
|
||||
calories: number; protein: number; fat: number; saturatedFat?: number;
|
||||
carbs: number; fiber?: number; sugars?: number;
|
||||
calcium?: number; iron?: number; magnesium?: number; phosphorus?: number;
|
||||
potassium?: number; sodium?: number; zinc?: number;
|
||||
vitaminA?: number; vitaminC?: number; vitaminD?: number; vitaminE?: number;
|
||||
vitaminK?: number; thiamin?: number; riboflavin?: number; niacin?: number;
|
||||
vitaminB6?: number; vitaminB12?: number; folate?: number; cholesterol?: number;
|
||||
};
|
||||
}
|
||||
|
||||
const ServingSchema = new mongoose.Schema({
|
||||
description: String,
|
||||
grams: Number,
|
||||
}, { _id: false });
|
||||
|
||||
const Per100gSchema = new mongoose.Schema({
|
||||
calories: Number, protein: Number, fat: Number, saturatedFat: Number,
|
||||
carbs: Number, fiber: Number, sugars: Number,
|
||||
calcium: Number, iron: Number, magnesium: Number, phosphorus: Number,
|
||||
potassium: Number, sodium: Number, zinc: Number,
|
||||
vitaminA: Number, vitaminC: Number, vitaminD: Number, vitaminE: Number,
|
||||
vitaminK: Number, thiamin: Number, riboflavin: Number, niacin: Number,
|
||||
vitaminB6: Number, vitaminB12: Number, folate: Number, cholesterol: Number,
|
||||
}, { _id: false });
|
||||
|
||||
const OpenFoodFactSchema = new mongoose.Schema({
|
||||
barcode: { type: String, required: true, unique: true, index: true },
|
||||
name: { type: String, required: true },
|
||||
nameDe: String,
|
||||
brands: String,
|
||||
category: String,
|
||||
nutriscore: String,
|
||||
productQuantityG: Number,
|
||||
serving: ServingSchema,
|
||||
per100g: { type: Per100gSchema, required: true },
|
||||
}, { collection: 'openfoodfacts' });
|
||||
|
||||
let _model: mongoose.Model<IOpenFoodFact>;
|
||||
try { _model = mongoose.model<IOpenFoodFact>('OpenFoodFact'); } catch { _model = mongoose.model<IOpenFoodFact>('OpenFoodFact', OpenFoodFactSchema); }
|
||||
export const OpenFoodFact = _model;
|
||||
export type { IOpenFoodFact };
|
||||
Reference in New Issue
Block a user