feat: added meals tables

This commit is contained in:
Yehoshua Sandler 2025-05-18 11:38:11 -05:00
parent 723dda191b
commit d0b5cf95ae
9 changed files with 850 additions and 1 deletions

View File

@ -0,0 +1,103 @@
import { CollectionConfig } from "payload";
export const Ingredients: CollectionConfig = {
slug: 'ingredients',
admin: {
useAsTitle: 'name',
},
fields: [
{
name: 'name',
type: 'text',
unique: true,
},
{
name: 'description',
type: 'textarea',
},
{
type: 'row',
fields: [
{
name: 'calories',
type: 'number',
admin: {
description: 'per 100g',
width: '25%',
}
},
{
name: 'protein',
type: 'number',
admin: {
description: 'per 100g',
width: '25%',
}
},
{
name: 'carbohydrates',
type: 'number',
admin: {
description: 'per 100g',
width: '25%',
}
},
{
name: 'fat',
type: 'number',
admin: {
description: 'per 100g',
width: '25%',
}
},
],
},
{
type: 'row',
fields: [
{
name: 'canBeKosher',
type: 'checkbox',
admin: {
description: 'Is capable of being vertified Kosher. Not a declaration that ingredient is Kosher.',
width: '20%',
},
},
{
name: 'canBeHalal',
type: 'checkbox',
admin: {
description: 'Is capable of being vertified Halal. Not a declaration that ingredient is Halal.',
width: '20%',
},
},
{
name: 'canBeVegan',
type: 'checkbox',
admin: {
description: 'Is capable of being vertified Vegan. Not a declaration that ingredient is Vegan.',
width: '20%',
},
},
{
name: 'canBeVegetarian',
type: 'checkbox',
admin: {
description: 'Is capable of being vertified Vegetarian. Not a declaration that ingredient is Vegetarian.',
width: '20%',
},
},
{
name: 'canBeGlutenFree',
type: 'checkbox',
admin: {
description: 'Is capable of being vertified Gluten Free. Not a declaration that ingredient is Gluten Free.',
width: '20%',
},
},
],
},
// TODO: Add Relationship to Common Allergens
],
}

View File

@ -0,0 +1,157 @@
import { CollectionConfig, PaginatedDocs } from "payload";
import { MealItem } from "@/payload-types";
import { setMealItemNutritionOnChange } from "./hooks/beforeChangeMealItems";
export const MealItems: CollectionConfig = {
slug: 'mealItems',
admin: {
description: 'Items that make up a meal, such as mashed potatoes, salad, grilled chicken, etc.',
useAsTitle: 'name',
},
fields: [
{
type: 'row',
fields: [
{
name: 'name',
type: 'text',
admin: {
width: '75%',
}
},
{
name: 'servingSize',
type: 'number',
admin: {
description: 'in Grams',
width: '25%',
}
},
],
},
{
name: 'description',
type: 'textarea',
},
{
type: 'row',
fields: [
{
name: 'calories',
type: 'number',
admin: {
description: 'per Serving, calculated from ingredients',
width: '25%',
},
},
{
name: 'protein',
type: 'number',
admin: {
description: 'per Serving, calculated from ingredients',
width: '25%',
},
},
{
name: 'carbohydrates',
type: 'number',
admin: {
description: 'per Serving, calculated from ingredients',
width: '25%',
},
},
{
name: 'fat',
type: 'number',
admin: {
description: 'per Serving, calculated from ingredients',
width: '25%',
},
},
]
},
{
type: 'row',
fields: [
{
name: 'canBeKosher',
type: 'checkbox',
admin: {
description: 'Calculated from ingredients. Is capable of being vertified Kosher. Not a declaration that ingredient is Kosher.',
width: '20%',
},
},
{
name: 'canBeHalal',
type: 'checkbox',
admin: {
description: 'Calculated from ingredients. Is capable of being vertified Halal. Not a declaration that ingredient is Halal.',
width: '20%',
},
},
{
name: 'canBeVegan',
type: 'checkbox',
admin: {
description: 'Calculated from ingredients. Is capable of being vertified Vegan. Not a declaration that ingredient is Vegan.',
width: '20%',
},
},
{
name: 'canBeVegetarian',
type: 'checkbox',
admin: {
description: 'Calculated from ingredients. Is capable of being vertified Vegetarian. Not a declaration that ingredient is Vegetarian.',
width: '20%',
},
},
{
name: 'canBeGlutenFree',
type: 'checkbox',
admin: {
description: 'Calculated from ingredients. Is capable of being vertified Gluten Free. Not a declaration that ingredient is Gluten Free.',
width: '20%',
},
},
],
},
{
name: 'instructions',
type: 'richText',
},
{
name: 'neededIngredients',
type: 'array',
fields: [
{
type: 'row',
fields: [
{
name: 'quantity',
type: 'number',
admin: {
description: 'as 100g intervals. (value of 3 = 300g)',
width: '25%',
}
},
{
name: 'ingredient',
type: 'relationship',
relationTo: 'ingredients',
hasMany: false,
admin: {
width: '75%',
}
},
]
}
]
}
],
hooks: {
beforeChange: [
setMealItemNutritionOnChange
]
}
}

View File

@ -0,0 +1,142 @@
import { CollectionConfig } from "payload";
import { setMealNutritionOnChange } from "./hooks/beforeChangeMeal";
export const Meals: CollectionConfig = {
slug: 'meals',
admin: {
useAsTitle: 'name',
},
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'description',
type: 'textarea'
},
{
name: 'excludeItemInstructions',
type: 'checkbox',
},
{
name: 'instructions',
type: 'richText',
},
{
type: 'row',
fields: [
{
name: 'calories',
type: 'number',
admin: {
description: 'per Serving, calculated from ingredients',
width: '25%',
},
},
{
name: 'protein',
type: 'number',
admin: {
description: 'per Serving, calculated from ingredients',
width: '25%',
},
},
{
name: 'carbohydrates',
type: 'number',
admin: {
description: 'per Serving, calculated from ingredients',
width: '25%',
},
},
{
name: 'fat',
type: 'number',
admin: {
description: 'per Serving, calculated from ingredients',
width: '25%',
},
},
]
},
{
type: 'row',
fields: [
{
name: 'canBeKosher',
type: 'checkbox',
admin: {
description: 'Calculated from ingredients. Is capable of being vertified Kosher. Not a declaration that ingredient is Kosher.',
width: '20%',
},
},
{
name: 'canBeHalal',
type: 'checkbox',
admin: {
description: 'Calculated from ingredients. Is capable of being vertified Halal. Not a declaration that ingredient is Halal.',
width: '20%',
},
},
{
name: 'canBeVegan',
type: 'checkbox',
admin: {
description: 'Calculated from ingredients. Is capable of being vertified Vegan. Not a declaration that ingredient is Vegan.',
width: '20%',
},
},
{
name: 'canBeVegetarian',
type: 'checkbox',
admin: {
description: 'Calculated from ingredients. Is capable of being vertified Vegetarian. Not a declaration that ingredient is Vegetarian.',
width: '20%',
},
},
{
name: 'canBeGlutenFree',
type: 'checkbox',
admin: {
description: 'Calculated from ingredients. Is capable of being vertified Gluten Free. Not a declaration that ingredient is Gluten Free.',
width: '20%',
},
},
],
},
{
name: 'items',
type: 'array',
fields: [
{
type: 'row',
fields: [
{
name: 'item',
type: 'relationship',
relationTo: 'mealItems',
hasMany: false,
admin: {
width: '75%',
},
},
{
name: 'quantity',
type: 'number',
admin: {
description: 'as Servings',
width: '25%',
}
}
]
},
],
},
],
hooks: {
beforeChange: [
setMealNutritionOnChange
]
}
}

View File

@ -0,0 +1,69 @@
import { Meal } from "@/payload-types"
type ItemNeeded = Extract<Meal['items'], Array<unknown>>[0]
import { BeforeChangeHook } from "node_modules/payload/dist/collections/config/types"
export const setMealNutritionOnChange: BeforeChangeHook = async (args) => {
const data = args.data as Partial<Meal>
const payload = args.req.payload
if (!data.items?.length) {
const zeroedOutNutrition: Partial<Meal> = {
calories: 0,
carbohydrates: 0,
fat: 0,
protein: 0,
canBeKosher: false,
canBeHalal: false,
canBeVegan: false,
canBeVegetarian: false,
canBeGlutenFree: false,
}
return { ...data, ...zeroedOutNutrition }
}
const itemsPromises = data.items?.map(async (i: ItemNeeded) => {
return await payload.findByID({
collection: 'mealItems',
id: i.item as number || 0,
select: {
calories: true,
protein: true,
carbohydrates: true,
fat: true,
canBeGlutenFree: true,
canBeHalal: true,
canBeKosher: true,
canBeVegan: true,
canBeVegetarian: true,
},
})
}) || []
const items = await Promise.all(itemsPromises)
const updatedNutrition: Partial<Meal> = {
calories: items.reduce((previousValue, item) => {
const quantity = data.items?.find(i => i.item === item.id)?.quantity || 1
return previousValue + ((item?.calories || 0) * quantity)
}, 0),
carbohydrates: items.reduce((previousValue, item) => {
const quantity = data.items?.find(i => i.item === item.id)?.quantity || 1
return previousValue + ((item?.carbohydrates || 0) * quantity)
}, 0),
fat: items.reduce((previousValue, item) => {
const quantity = data.items?.find(i => i.item === item.id)?.quantity || 1
return previousValue + ((item?.fat || 0) * quantity)
}, 0),
protein: items.reduce((previousValue, item) => {
const quantity = data.items?.find(i => i.item === item.id)?.quantity || 1
return previousValue + ((item?.protein || 0) * quantity)
}, 0),
canBeKosher: !items.find(i => !i.canBeKosher),
canBeHalal: !items.find(i => !i.canBeHalal),
canBeVegan: !items.find(i => !i.canBeVegan),
canBeVegetarian: !items.find(i => !i.canBeVegetarian),
canBeGlutenFree: !items.find(i => !i.canBeGlutenFree),
}
return { ...data, ...updatedNutrition }
}

View File

@ -0,0 +1,69 @@
import { MealItem } from "@/payload-types"
type ItemNeededIngredient = Extract<MealItem['neededIngredients'], Array<unknown>>[0]
import { BeforeChangeHook } from "node_modules/payload/dist/collections/config/types"
export const setMealItemNutritionOnChange: BeforeChangeHook = async (args) => {
const data = args.data as Partial<MealItem>
const payload = args.req.payload
if (!data.neededIngredients?.length) {
const zeroedOutNutrition: Partial<MealItem> = {
calories: 0,
carbohydrates: 0,
fat: 0,
protein: 0,
canBeKosher: false,
canBeHalal: false,
canBeVegan: false,
canBeVegetarian: false,
canBeGlutenFree: false,
}
return { ...data, ...zeroedOutNutrition }
}
const neededIngredientsPromises = data.neededIngredients?.map(async (i: ItemNeededIngredient) => {
return await payload.findByID({
collection: 'ingredients',
id: i.ingredient as number || 0,
select: {
calories: true,
protein: true,
carbohydrates: true,
fat: true,
canBeGlutenFree: true,
canBeHalal: true,
canBeKosher: true,
canBeVegan: true,
canBeVegetarian: true,
},
})
}) || []
const ingredients = await Promise.all(neededIngredientsPromises)
const updatedNutrition: Partial<MealItem> = {
calories: ingredients.reduce((previousValue, ingredient) => {
const quantity = data.neededIngredients?.find(i => i.ingredient === ingredient.id)?.quantity || 1
return previousValue + ((ingredient?.calories || 0) * quantity)
}, 0),
carbohydrates: ingredients.reduce((previousValue, ingredient) => {
const quantity = data.neededIngredients?.find(i => i.ingredient === ingredient.id)?.quantity || 1
return previousValue + ((ingredient?.carbohydrates || 0) * quantity)
}, 0),
fat: ingredients.reduce((previousValue, ingredient) => {
const quantity = data.neededIngredients?.find(i => i.ingredient === ingredient.id)?.quantity || 1
return previousValue + ((ingredient?.fat || 0) * quantity)
}, 0),
protein: ingredients.reduce((previousValue, ingredient) => {
const quantity = data.neededIngredients?.find(i => i.ingredient === ingredient.id)?.quantity || 1
return previousValue + ((ingredient?.protein || 0) * quantity)
}, 0),
canBeKosher: !ingredients.find(i => !i.canBeKosher),
canBeHalal: !ingredients.find(i => !i.canBeHalal),
canBeVegan: !ingredients.find(i => !i.canBeVegan),
canBeVegetarian: !ingredients.find(i => !i.canBeVegetarian),
canBeGlutenFree: !ingredients.find(i => !i.canBeGlutenFree),
}
return { ...data, ...updatedNutrition }
}

View File

@ -0,0 +1,8 @@
import { Ingredients } from "./Ingredients";
import { MealItems } from "./MealItems";
import { Meals } from "./Meals";
export { Ingredients, MealItems, Meals }

View File

@ -79,6 +79,9 @@ export interface Config {
equipments: Equipment; equipments: Equipment;
workouts: Workout; workouts: Workout;
workoutTypes: WorkoutType; workoutTypes: WorkoutType;
ingredients: Ingredient;
mealItems: MealItem;
meals: Meal;
redirects: Redirect; redirects: Redirect;
forms: Form; forms: Form;
'form-submissions': FormSubmission; 'form-submissions': FormSubmission;
@ -102,6 +105,9 @@ export interface Config {
equipments: EquipmentsSelect<false> | EquipmentsSelect<true>; equipments: EquipmentsSelect<false> | EquipmentsSelect<true>;
workouts: WorkoutsSelect<false> | WorkoutsSelect<true>; workouts: WorkoutsSelect<false> | WorkoutsSelect<true>;
workoutTypes: WorkoutTypesSelect<false> | WorkoutTypesSelect<true>; workoutTypes: WorkoutTypesSelect<false> | WorkoutTypesSelect<true>;
ingredients: IngredientsSelect<false> | IngredientsSelect<true>;
mealItems: MealItemsSelect<false> | MealItemsSelect<true>;
meals: MealsSelect<false> | MealsSelect<true>;
redirects: RedirectsSelect<false> | RedirectsSelect<true>; redirects: RedirectsSelect<false> | RedirectsSelect<true>;
forms: FormsSelect<false> | FormsSelect<true>; forms: FormsSelect<false> | FormsSelect<true>;
'form-submissions': FormSubmissionsSelect<false> | FormSubmissionsSelect<true>; 'form-submissions': FormSubmissionsSelect<false> | FormSubmissionsSelect<true>;
@ -886,6 +892,207 @@ export interface WorkoutType {
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "ingredients".
*/
export interface Ingredient {
id: number;
tenant?: (number | null) | Tenant;
name?: string | null;
description?: string | null;
/**
* per 100g
*/
calories?: number | null;
/**
* per 100g
*/
protein?: number | null;
/**
* per 100g
*/
carbohydrates?: number | null;
/**
* per 100g
*/
fat?: number | null;
/**
* Is capable of being vertified Kosher. Not a declaration that ingredient is Kosher.
*/
canBeKosher?: boolean | null;
/**
* Is capable of being vertified Halal. Not a declaration that ingredient is Halal.
*/
canBeHalal?: boolean | null;
/**
* Is capable of being vertified Vegan. Not a declaration that ingredient is Vegan.
*/
canBeVegan?: boolean | null;
/**
* Is capable of being vertified Vegetarian. Not a declaration that ingredient is Vegetarian.
*/
canBeVegetarian?: boolean | null;
/**
* Is capable of being vertified Gluten Free. Not a declaration that ingredient is Gluten Free.
*/
canBeGlutenFree?: boolean | null;
updatedAt: string;
createdAt: string;
}
/**
* Items that make up a meal, such as mashed potatoes, salad, grilled chicken, etc.
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "mealItems".
*/
export interface MealItem {
id: number;
tenant?: (number | null) | Tenant;
name?: string | null;
/**
* in Grams
*/
servingSize?: number | null;
description?: string | null;
/**
* per Serving, calculated from ingredients
*/
calories?: number | null;
/**
* per Serving, calculated from ingredients
*/
protein?: number | null;
/**
* per Serving, calculated from ingredients
*/
carbohydrates?: number | null;
/**
* per Serving, calculated from ingredients
*/
fat?: number | null;
/**
* Calculated from ingredients. Is capable of being vertified Kosher. Not a declaration that ingredient is Kosher.
*/
canBeKosher?: boolean | null;
/**
* Calculated from ingredients. Is capable of being vertified Halal. Not a declaration that ingredient is Halal.
*/
canBeHalal?: boolean | null;
/**
* Calculated from ingredients. Is capable of being vertified Vegan. Not a declaration that ingredient is Vegan.
*/
canBeVegan?: boolean | null;
/**
* Calculated from ingredients. Is capable of being vertified Vegetarian. Not a declaration that ingredient is Vegetarian.
*/
canBeVegetarian?: boolean | null;
/**
* Calculated from ingredients. Is capable of being vertified Gluten Free. Not a declaration that ingredient is Gluten Free.
*/
canBeGlutenFree?: boolean | null;
instructions?: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
neededIngredients?:
| {
/**
* as 100g intervals. (value of 3 = 300g)
*/
quantity?: number | null;
ingredient?: (number | null) | Ingredient;
id?: string | null;
}[]
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "meals".
*/
export interface Meal {
id: number;
tenant?: (number | null) | Tenant;
name?: string | null;
description?: string | null;
excludeItemInstructions?: boolean | null;
instructions?: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
/**
* per Serving, calculated from ingredients
*/
calories?: number | null;
/**
* per Serving, calculated from ingredients
*/
protein?: number | null;
/**
* per Serving, calculated from ingredients
*/
carbohydrates?: number | null;
/**
* per Serving, calculated from ingredients
*/
fat?: number | null;
/**
* Calculated from ingredients. Is capable of being vertified Kosher. Not a declaration that ingredient is Kosher.
*/
canBeKosher?: boolean | null;
/**
* Calculated from ingredients. Is capable of being vertified Halal. Not a declaration that ingredient is Halal.
*/
canBeHalal?: boolean | null;
/**
* Calculated from ingredients. Is capable of being vertified Vegan. Not a declaration that ingredient is Vegan.
*/
canBeVegan?: boolean | null;
/**
* Calculated from ingredients. Is capable of being vertified Vegetarian. Not a declaration that ingredient is Vegetarian.
*/
canBeVegetarian?: boolean | null;
/**
* Calculated from ingredients. Is capable of being vertified Gluten Free. Not a declaration that ingredient is Gluten Free.
*/
canBeGlutenFree?: boolean | null;
items?:
| {
item?: (number | null) | MealItem;
/**
* as Servings
*/
quantity?: number | null;
id?: string | null;
}[]
| null;
updatedAt: string;
createdAt: string;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "redirects". * via the `definition` "redirects".
@ -1106,6 +1313,18 @@ export interface PayloadLockedDocument {
relationTo: 'workoutTypes'; relationTo: 'workoutTypes';
value: number | WorkoutType; value: number | WorkoutType;
} | null) } | null)
| ({
relationTo: 'ingredients';
value: number | Ingredient;
} | null)
| ({
relationTo: 'mealItems';
value: number | MealItem;
} | null)
| ({
relationTo: 'meals';
value: number | Meal;
} | null)
| ({ | ({
relationTo: 'redirects'; relationTo: 'redirects';
value: number | Redirect; value: number | Redirect;
@ -1569,6 +1788,84 @@ export interface WorkoutTypesSelect<T extends boolean = true> {
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "ingredients_select".
*/
export interface IngredientsSelect<T extends boolean = true> {
tenant?: T;
name?: T;
description?: T;
calories?: T;
protein?: T;
carbohydrates?: T;
fat?: T;
canBeKosher?: T;
canBeHalal?: T;
canBeVegan?: T;
canBeVegetarian?: T;
canBeGlutenFree?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "mealItems_select".
*/
export interface MealItemsSelect<T extends boolean = true> {
tenant?: T;
name?: T;
servingSize?: T;
description?: T;
calories?: T;
protein?: T;
carbohydrates?: T;
fat?: T;
canBeKosher?: T;
canBeHalal?: T;
canBeVegan?: T;
canBeVegetarian?: T;
canBeGlutenFree?: T;
instructions?: T;
neededIngredients?:
| T
| {
quantity?: T;
ingredient?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "meals_select".
*/
export interface MealsSelect<T extends boolean = true> {
tenant?: T;
name?: T;
description?: T;
excludeItemInstructions?: T;
instructions?: T;
calories?: T;
protein?: T;
carbohydrates?: T;
fat?: T;
canBeKosher?: T;
canBeHalal?: T;
canBeVegan?: T;
canBeVegetarian?: T;
canBeGlutenFree?: T;
items?:
| T
| {
item?: T;
quantity?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "redirects_select". * via the `definition` "redirects_select".

View File

@ -19,6 +19,7 @@ import { getServerSideURL } from './utilities/getURL'
import { Tenants } from './collections/Tenants' import { Tenants } from './collections/Tenants'
import { Equipments, Exercises, ExerciseTypes, MuscleGroups } from './collections/Exercises' import { Equipments, Exercises, ExerciseTypes, MuscleGroups } from './collections/Exercises'
import { Workouts, WorkoutTypes } from './collections/Workouts' import { Workouts, WorkoutTypes } from './collections/Workouts'
import { Ingredients, MealItems, Meals } from './collections/Meals'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
@ -67,7 +68,7 @@ export default buildConfig({
connectionString: process.env.DATABASE_URI || '', connectionString: process.env.DATABASE_URI || '',
}, },
}), }),
collections: [Pages, Posts, Media, Categories, Users, Tenants, Exercises, ExerciseTypes, MuscleGroups, Equipments, Workouts, WorkoutTypes], collections: [Pages, Posts, Media, Categories, Users, Tenants, Exercises, ExerciseTypes, MuscleGroups, Equipments, Workouts, WorkoutTypes, Ingredients, MealItems, Meals],
cors: [getServerSideURL()].filter(Boolean), cors: [getServerSideURL()].filter(Boolean),
globals: [Header, Footer], globals: [Header, Footer],
plugins: [ plugins: [

View File

@ -105,6 +105,9 @@ export const plugins: Plugin[] = [
equipments: {}, equipments: {},
workouts: {}, workouts: {},
workoutTypes: {}, workoutTypes: {},
ingredients: {},
mealItems: {},
meals: {},
}, },
tenantsArrayField: { tenantsArrayField: {
includeDefaultField: false, includeDefaultField: false,