diff --git a/.gitignore b/.gitignore index 0f4444b6..da82f89e 100644 --- a/.gitignore +++ b/.gitignore @@ -145,4 +145,26 @@ node_modules/ scratch.py dev/data/backups/dev_sample_data*.zip !dev/data/backups/test*.zip -dev/data/recipes/* \ No newline at end of file +dev/data/recipes/* +dev/scripts/output/app_routes.py +dev/scripts/output/javascriptAPI/apiRoutes.js +dev/scripts/output/javascriptAPI/appEvents.js +dev/scripts/output/javascriptAPI/authentication.js +dev/scripts/output/javascriptAPI/backups.js +dev/scripts/output/javascriptAPI/debug.js +dev/scripts/output/javascriptAPI/groups.js +dev/scripts/output/javascriptAPI/index.js +dev/scripts/output/javascriptAPI/mealPlan.js +dev/scripts/output/javascriptAPI/migration.js +dev/scripts/output/javascriptAPI/queryAllRecipes.js +dev/scripts/output/javascriptAPI/recipeCategories.js +dev/scripts/output/javascriptAPI/recipeCRUD.js +dev/scripts/output/javascriptAPI/recipeTags.js +dev/scripts/output/javascriptAPI/settings.js +dev/scripts/output/javascriptAPI/shoppingLists.js +dev/scripts/output/javascriptAPI/siteMedia.js +dev/scripts/output/javascriptAPI/themes.js +dev/scripts/output/javascriptAPI/userAPITokens.js +dev/scripts/output/javascriptAPI/users.js +dev/scripts/output/javascriptAPI/userSignup.js +dev/scripts/output/javascriptAPI/utils.js diff --git a/dev/scripts/app_routes_gen copy.py b/dev/scripts/app_routes_gen copy.py new file mode 100644 index 00000000..3e42f057 --- /dev/null +++ b/dev/scripts/app_routes_gen copy.py @@ -0,0 +1,144 @@ +import json +import re +from enum import Enum +from itertools import groupby +from pathlib import Path + +import slugify +from fastapi import FastAPI +from humps import camelize +from jinja2 import Template +from mealie.app import app +from pydantic import BaseModel + +CWD = Path(__file__).parent +OUT_DIR = CWD / "output" +OUT_FILE = OUT_DIR / "app_routes.py" + +JS_DIR = OUT_DIR / "javascriptAPI" +JS_OUT_FILE = JS_DIR / "apiRoutes.js" +TEMPLATES_DIR = CWD / "templates" + +PYTEST_TEMPLATE = TEMPLATES_DIR / "pytest_routes.j2" +JS_REQUESTS = TEMPLATES_DIR / "js_requests.j2" +JS_ROUTES = TEMPLATES_DIR / "js_routes.j2" +JS_INDEX = TEMPLATES_DIR / "js_index.j2" + +JS_DIR.mkdir(exist_ok=True, parents=True) + + +class RouteObject: + def __init__(self, route_string) -> None: + self.prefix = "/" + route_string.split("/")[1] + self.route = route_string.replace(self.prefix, "") + self.js_route = self.route.replace("{", "${") + self.parts = route_string.split("/")[1:] + self.var = re.findall(r"\{(.*?)\}", route_string) + self.is_function = "{" in self.route + self.router_slug = slugify.slugify("_".join(self.parts[1:]), separator="_") + self.router_camel = camelize(self.router_slug) + + def __repr__(self) -> str: + return f"""Route: {self.route} +Parts: {self.parts} +Function: {self.is_function} +Var: {self.var} +Slug: {self.router_slug} +""" + + +class RequestType(str, Enum): + get = "get" + put = "put" + post = "post" + patch = "patch" + delete = "delete" + + +class HTTPRequest(BaseModel): + request_type: RequestType + description: str = "" + summary: str + tags: list[str] + + @property + def summary_camel(self): + return camelize(self.summary) + + @property + def js_docs(self): + return self.description.replace("\n", " \n * ") + + +class PathObject(BaseModel): + route_object: RouteObject + http_verbs: list[HTTPRequest] + + class Config: + arbitrary_types_allowed = True + + +def get_path_objects(app: FastAPI): + paths = [] + + with open("scratch.json", "w") as f: + f.write(json.dumps(app.openapi())) + for key, value in app.openapi().items(): + if key == "paths": + for key, value in value.items(): + + paths.append( + PathObject( + route_object=RouteObject(key), + http_verbs=[HTTPRequest(request_type=k, **v) for k, v in value.items()], + ) + ) + + return paths + + +def read_template(file: Path): + with open(file, "r") as f: + return f.read() + + +def generate_template(app): + paths = get_path_objects(app) + + static_paths = [x.route_object for x in paths if not x.route_object.is_function] + function_paths = [x.route_object for x in paths if x.route_object.is_function] + + static_paths.sort(key=lambda x: x.router_slug) + function_paths.sort(key=lambda x: x.router_slug) + + template = Template(read_template(PYTEST_TEMPLATE)) + content = template.render(paths={"prefix": "/api", "static_paths": static_paths, "function_paths": function_paths}) + with open(OUT_FILE, "w") as f: + f.write(content) + + template = Template(read_template(JS_ROUTES)) + content = template.render( + paths={"prefix": "/api", "static_paths": static_paths, "function_paths": function_paths, "all_paths": paths} + ) + with open(JS_OUT_FILE, "w") as f: + f.write(content) + + all_tags = [] + for k, g in groupby(paths, lambda x: x.http_verbs[0].tags[0]): + template = Template(read_template(JS_REQUESTS)) + content = template.render(paths={"all_paths": list(g), "export_name": camelize(k)}) + + all_tags.append(camelize(k)) + + with open(JS_DIR.joinpath(camelize(k) + ".js"), "w") as f: + f.write(content) + + template = Template(read_template(JS_INDEX)) + content = template.render(files={"files": all_tags}) + + with open(JS_DIR.joinpath("index.js"), "w") as f: + f.write(content) + + +if __name__ == "__main__": + generate_template(app) diff --git a/dev/scripts/app_routes_gen.py b/dev/scripts/app_routes_gen.py deleted file mode 100644 index aa52c1da..00000000 --- a/dev/scripts/app_routes_gen.py +++ /dev/null @@ -1,81 +0,0 @@ -import json -import re -from pathlib import Path - -import slugify -from jinja2 import Template -from mealie.app import app - -CWD = Path(__file__).parent -OUT_FILE = CWD.joinpath("output", "app_routes.py") - -code_template = """ -class AppRoutes: - def __init__(self) -> None: - self.prefix = '{{paths.prefix}}' -{% for path in paths.static_paths %} - self.{{ path.router_slug }} = "{{path.prefix}}{{ path.route }}"{% endfor %} -{% for path in paths.function_paths %} - def {{path.router_slug}}(self, {{path.var|join(", ")}}): - return f"{self.prefix}{{ path.route }}" -{% endfor %} -""" - - -def get_variables(path): - path = path.replace("/", " ") - print(path) - var = re.findall(r" \{.*\}", path) - print(var) - if var: - return [v.replace("{", "").replace("}", "") for v in var] - else: - return None - - -class RouteObject: - def __init__(self, route_string) -> None: - self.prefix = "/" + route_string.split("/")[1] - self.route = route_string.replace(self.prefix, "") - self.parts = route_string.split("/")[1:] - self.var = re.findall(r"\{(.*?)\}", route_string) - self.is_function = "{" in self.route - self.router_slug = slugify.slugify("_".join(self.parts[1:]), separator="_") - - def __repr__(self) -> str: - return f"""Route: {self.route} -Parts: {self.parts} -Function: {self.is_function} -Var: {self.var} -Slug: {self.router_slug} -""" - - -def get_paths(app): - paths = [] - print(json.dumps(app.openapi())) - for key, value in app.openapi().items(): - if key == "paths": - for key, value in value.items(): - paths.append(key) - - return paths - - -def generate_template(app): - paths = get_paths(app) - new_paths = [RouteObject(path) for path in paths] - - static_paths = [p for p in new_paths if not p.is_function] - function_paths = [p for p in new_paths if p.is_function] - - template = Template(code_template) - - content = template.render(paths={"prefix": "/api", "static_paths": static_paths, "function_paths": function_paths}) - - with open(OUT_FILE, "w") as f: - f.write(content) - - -if __name__ == "__main__": - generate_template(app) diff --git a/dev/scripts/output/app_routes.py b/dev/scripts/output/app_routes.py index 40e4b2c1..f46ec710 100644 --- a/dev/scripts/output/app_routes.py +++ b/dev/scripts/output/app_routes.py @@ -1,105 +1,63 @@ +# This Content is Auto Generated for Pytest + + class AppRoutes: def __init__(self) -> None: - self.prefix = "/api" + self.prefix = '/api' + self.about_events = "/api/about/events" + self.about_events_notifications = "/api/about/events/notifications" + self.about_events_notifications_test = "/api/about/events/notifications/test" + self.auth_refresh = "/api/auth/refresh" self.auth_token = "/api/auth/token" self.auth_token_long = "/api/auth/token/long" - self.auth_refresh = "/api/auth/refresh" - self.users_sign_ups = "/api/users/sign-ups" - self.users = "/api/users" - self.users_self = "/api/users/self" - self.users_api_tokens = "/api/users-tokens" - self.groups = "/api/groups" - self.groups_self = "/api/groups/self" - self.recipes_summary = "/api/recipes/summary" - self.recipes_summary_untagged = "/api/recipes/summary/untagged" - self.recipes_summary_uncategorized = "/api/recipes/summary/uncategorized" - self.recipes_category = "/api/recipes/category" - self.recipes_tag = "/api/recipes/tag" - self.recipes_create = "/api/recipes/create" - self.recipes_create_url = "/api/recipes/create-url" + self.backups_available = "/api/backups/available" + self.backups_export_database = "/api/backups/export/database" + self.backups_upload = "/api/backups/upload" self.categories = "/api/categories" self.categories_empty = "/api/categories/empty" - self.tags = "/api/tags" - self.tags_empty = "/api/tags/empty" - self.about_events = "/api/about/events" + self.debug = "/api/debug" + self.debug_last_recipe_json = "/api/debug/last-recipe-json" + self.debug_log = "/api/debug/log" + self.debug_statistics = "/api/debug/statistics" + self.debug_version = "/api/debug/version" + self.groups = "/api/groups" + self.groups_self = "/api/groups/self" self.meal_plans_all = "/api/meal-plans/all" self.meal_plans_create = "/api/meal-plans/create" self.meal_plans_this_week = "/api/meal-plans/this-week" self.meal_plans_today = "/api/meal-plans/today" self.meal_plans_today_image = "/api/meal-plans/today/image" - self.site_settings_custom_pages = "/api/site-settings/custom-pages" + self.migrations = "/api/migrations" + self.recipes_category = "/api/recipes/category" + self.recipes_create = "/api/recipes/create" + self.recipes_create_url = "/api/recipes/create-url" + self.recipes_summary = "/api/recipes/summary" + self.recipes_summary_uncategorized = "/api/recipes/summary/uncategorized" + self.recipes_summary_untagged = "/api/recipes/summary/untagged" + self.recipes_tag = "/api/recipes/tag" + self.shopping_lists = "/api/shopping-lists" self.site_settings = "/api/site-settings" + self.site_settings_custom_pages = "/api/site-settings/custom-pages" self.site_settings_webhooks_test = "/api/site-settings/webhooks/test" + self.tags = "/api/tags" + self.tags_empty = "/api/tags/empty" self.themes = "/api/themes" self.themes_create = "/api/themes/create" - self.backups_available = "/api/backups/available" - self.backups_export_database = "/api/backups/export/database" - self.backups_upload = "/api/backups/upload" - self.migrations = "/api/migrations" - self.debug = "/api/debug" - self.debug_statistics = "/api/debug/statistics" - self.debug_version = "/api/debug/version" - self.debug_last_recipe_json = "/api/debug/last-recipe-json" - self.debug_log = "/api/debug/log" + self.users = "/api/users" + self.users_api_tokens = "/api/users-tokens" + self.users_self = "/api/users/self" + self.users_sign_ups = "/api/users/sign-ups" self.utils_download = "/api/utils/download" - def users_sign_ups_token(self, token): - return f"{self.prefix}/users/sign-ups/{token}" - - def users_id(self, id): - return f"{self.prefix}/users/{id}" - - def users_id_reset_password(self, id): - return f"{self.prefix}/users/{id}/reset-password" - - def users_id_image(self, id): - return f"{self.prefix}/users/{id}/image" - - def users_id_password(self, id): - return f"{self.prefix}/users/{id}/password" - - def users_api_tokens_token_id(self, token_id): - return f"{self.prefix}/users-tokens/{token_id}" - - def groups_id(self, id): - return f"{self.prefix}/groups/{id}" - - def recipes_recipe_slug(self, recipe_slug): - return f"{self.prefix}/recipes/{recipe_slug}" - - def recipes_recipe_slug_image(self, recipe_slug): - return f"{self.prefix}/recipes/{recipe_slug}/image" - - def recipes_recipe_slug_assets(self, recipe_slug): - return f"{self.prefix}/recipes/{recipe_slug}/assets" - - def categories_category(self, category): - return f"{self.prefix}/categories/{category}" - - def tags_tag(self, tag): - return f"{self.prefix}/tags/{tag}" - - def media_recipes_recipe_slug_images_file_name(self, recipe_slug, file_name): - return f"{self.prefix}/media/recipes/{recipe_slug}/images/{file_name}" - - def media_recipes_recipe_slug_assets_file_name(self, recipe_slug, file_name): - return f"{self.prefix}/media/recipes/{recipe_slug}/assets/{file_name}" - def about_events_id(self, id): return f"{self.prefix}/about/events/{id}" - def meal_plans_plan_id(self, plan_id): - return f"{self.prefix}/meal-plans/{plan_id}" + def about_events_notifications_id(self, id): + return f"{self.prefix}/about/events/notifications/{id}" - def meal_plans_id_shopping_list(self, id): - return f"{self.prefix}/meal-plans/{id}/shopping-list" - - def site_settings_custom_pages_id(self, id): - return f"{self.prefix}/site-settings/custom-pages/{id}" - - def themes_id(self, id): - return f"{self.prefix}/themes/{id}" + def backups_file_name_delete(self, file_name): + return f"{self.prefix}/backups/{file_name}/delete" def backups_file_name_download(self, file_name): return f"{self.prefix}/backups/{file_name}/download" @@ -107,17 +65,71 @@ class AppRoutes: def backups_file_name_import(self, file_name): return f"{self.prefix}/backups/{file_name}/import" - def backups_file_name_delete(self, file_name): - return f"{self.prefix}/backups/{file_name}/delete" + def categories_category(self, category): + return f"{self.prefix}/categories/{category}" - def migrations_import_type_file_name_import(self, import_type, file_name): - return f"{self.prefix}/migrations/{import_type}/{file_name}/import" + def debug_log_num(self, num): + return f"{self.prefix}/debug/log/{num}" + + def groups_id(self, id): + return f"{self.prefix}/groups/{id}" + + def meal_plans_id_shopping_list(self, id): + return f"{self.prefix}/meal-plans/{id}/shopping-list" + + def meal_plans_plan_id(self, plan_id): + return f"{self.prefix}/meal-plans/{plan_id}" + + def media_recipes_recipe_slug_assets_file_name(self, recipe_slug, file_name): + return f"{self.prefix}/media/recipes/{recipe_slug}/assets/{file_name}" + + def media_recipes_recipe_slug_images_file_name(self, recipe_slug, file_name): + return f"{self.prefix}/media/recipes/{recipe_slug}/images/{file_name}" def migrations_import_type_file_name_delete(self, import_type, file_name): return f"{self.prefix}/migrations/{import_type}/{file_name}/delete" + def migrations_import_type_file_name_import(self, import_type, file_name): + return f"{self.prefix}/migrations/{import_type}/{file_name}/import" + def migrations_import_type_upload(self, import_type): return f"{self.prefix}/migrations/{import_type}/upload" - def debug_log_num(self, num): - return f"{self.prefix}/debug/log/{num}" + def recipes_recipe_slug(self, recipe_slug): + return f"{self.prefix}/recipes/{recipe_slug}" + + def recipes_recipe_slug_assets(self, recipe_slug): + return f"{self.prefix}/recipes/{recipe_slug}/assets" + + def recipes_recipe_slug_image(self, recipe_slug): + return f"{self.prefix}/recipes/{recipe_slug}/image" + + def shopping_lists_id(self, id): + return f"{self.prefix}/shopping-lists/{id}" + + def site_settings_custom_pages_id(self, id): + return f"{self.prefix}/site-settings/custom-pages/{id}" + + def tags_tag(self, tag): + return f"{self.prefix}/tags/{tag}" + + def themes_id(self, id): + return f"{self.prefix}/themes/{id}" + + def users_api_tokens_token_id(self, token_id): + return f"{self.prefix}/users-tokens/{token_id}" + + def users_id(self, id): + return f"{self.prefix}/users/{id}" + + def users_id_image(self, id): + return f"{self.prefix}/users/{id}/image" + + def users_id_password(self, id): + return f"{self.prefix}/users/{id}/password" + + def users_id_reset_password(self, id): + return f"{self.prefix}/users/{id}/reset-password" + + def users_sign_ups_token(self, token): + return f"{self.prefix}/users/sign-ups/{token}" diff --git a/frontend/src/api/apiRoutes.js b/frontend/src/api/apiRoutes.js new file mode 100644 index 00000000..a978ccd3 --- /dev/null +++ b/frontend/src/api/apiRoutes.js @@ -0,0 +1,77 @@ +// This Content is Auto Generated +const prefix = '/api' +export const API_ROUTES = { + aboutEvents: "/api/about/events", + aboutEventsNotifications: "/api/about/events/notifications", + aboutEventsNotificationsTest: "/api/about/events/notifications/test", + authRefresh: "/api/auth/refresh", + authToken: "/api/auth/token", + authTokenLong: "/api/auth/token/long", + backupsAvailable: "/api/backups/available", + backupsExportDatabase: "/api/backups/export/database", + backupsUpload: "/api/backups/upload", + categories: "/api/categories", + categoriesEmpty: "/api/categories/empty", + debug: "/api/debug", + debugLastRecipeJson: "/api/debug/last-recipe-json", + debugLog: "/api/debug/log", + debugStatistics: "/api/debug/statistics", + debugVersion: "/api/debug/version", + groups: "/api/groups", + groupsSelf: "/api/groups/self", + mealPlansAll: "/api/meal-plans/all", + mealPlansCreate: "/api/meal-plans/create", + mealPlansThisWeek: "/api/meal-plans/this-week", + mealPlansToday: "/api/meal-plans/today", + mealPlansTodayImage: "/api/meal-plans/today/image", + migrations: "/api/migrations", + recipesCategory: "/api/recipes/category", + recipesCreate: "/api/recipes/create", + recipesCreateUrl: "/api/recipes/create-url", + recipesSummary: "/api/recipes/summary", + recipesSummaryUncategorized: "/api/recipes/summary/uncategorized", + recipesSummaryUntagged: "/api/recipes/summary/untagged", + recipesTag: "/api/recipes/tag", + shoppingLists: "/api/shopping-lists", + siteSettings: "/api/site-settings", + siteSettingsCustomPages: "/api/site-settings/custom-pages", + siteSettingsWebhooksTest: "/api/site-settings/webhooks/test", + tags: "/api/tags", + tagsEmpty: "/api/tags/empty", + themes: "/api/themes", + themesCreate: "/api/themes/create", + users: "/api/users", + usersApiTokens: "/api/users-tokens", + usersSelf: "/api/users/self", + usersSignUps: "/api/users/sign-ups", + utilsDownload: "/api/utils/download", + + aboutEventsId: (id) => `${prefix}/about/events/${id}`, + aboutEventsNotificationsId: (id) => `${prefix}/about/events/notifications/${id}`, + backupsFileNameDelete: (file_name) => `${prefix}/backups/${file_name}/delete`, + backupsFileNameDownload: (file_name) => `${prefix}/backups/${file_name}/download`, + backupsFileNameImport: (file_name) => `${prefix}/backups/${file_name}/import`, + categoriesCategory: (category) => `${prefix}/categories/${category}`, + debugLogNum: (num) => `${prefix}/debug/log/${num}`, + groupsId: (id) => `${prefix}/groups/${id}`, + mealPlansIdShoppingList: (id) => `${prefix}/meal-plans/${id}/shopping-list`, + mealPlansPlanId: (plan_id) => `${prefix}/meal-plans/${plan_id}`, + mediaRecipesRecipeSlugAssetsFileName: (recipe_slug, file_name) => `${prefix}/media/recipes/${recipe_slug}/assets/${file_name}`, + mediaRecipesRecipeSlugImagesFileName: (recipe_slug, file_name) => `${prefix}/media/recipes/${recipe_slug}/images/${file_name}`, + migrationsImportTypeFileNameDelete: (import_type, file_name) => `${prefix}/migrations/${import_type}/${file_name}/delete`, + migrationsImportTypeFileNameImport: (import_type, file_name) => `${prefix}/migrations/${import_type}/${file_name}/import`, + migrationsImportTypeUpload: (import_type) => `${prefix}/migrations/${import_type}/upload`, + recipesRecipeSlug: (recipe_slug) => `${prefix}/recipes/${recipe_slug}`, + recipesRecipeSlugAssets: (recipe_slug) => `${prefix}/recipes/${recipe_slug}/assets`, + recipesRecipeSlugImage: (recipe_slug) => `${prefix}/recipes/${recipe_slug}/image`, + shoppingListsId: (id) => `${prefix}/shopping-lists/${id}`, + siteSettingsCustomPagesId: (id) => `${prefix}/site-settings/custom-pages/${id}`, + tagsTag: (tag) => `${prefix}/tags/${tag}`, + themesId: (id) => `${prefix}/themes/${id}`, + usersApiTokensTokenId: (token_id) => `${prefix}/users-tokens/${token_id}`, + usersId: (id) => `${prefix}/users/${id}`, + usersIdImage: (id) => `${prefix}/users/${id}/image`, + usersIdPassword: (id) => `${prefix}/users/${id}/password`, + usersIdResetPassword: (id) => `${prefix}/users/${id}/reset-password`, + usersSignUpsToken: (token) => `${prefix}/users/sign-ups/${token}`, +} \ No newline at end of file diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 53d22e8a..dbc63321 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -12,6 +12,7 @@ import { signupAPI } from "./signUps"; import { groupAPI } from "./groups"; import { siteSettingsAPI } from "./siteSettings"; import { aboutAPI } from "./about"; +import { shoppingListsAPI } from "./shoppingLists"; /** * The main object namespace for interacting with the backend database @@ -32,4 +33,5 @@ export const api = { signUps: signupAPI, groups: groupAPI, about: aboutAPI, + shoppingLists: shoppingListsAPI, }; diff --git a/frontend/src/api/shoppingLists.js b/frontend/src/api/shoppingLists.js new file mode 100644 index 00000000..74a7c9e2 --- /dev/null +++ b/frontend/src/api/shoppingLists.js @@ -0,0 +1,33 @@ +// This Content is Auto Generated +import { API_ROUTES } from "./apiRoutes"; +import { apiReq } from "./api-utils"; + +export const shoppingListsAPI = { + /** Create Shopping List in the Database + */ + async createShoppingList(data) { + const response = await apiReq.post(API_ROUTES.shoppingLists, data); + return response.data; + }, + /** Get Shopping List from the Database + * @param id + */ + async getShoppingList(id) { + const response = await apiReq.get(API_ROUTES.shoppingListsId(id)); + return response.data; + }, + /** Update Shopping List in the Database + * @param id + */ + async updateShoppingList(id, data) { + const response = await apiReq.put(API_ROUTES.shoppingListsId(id), data); + return response.data; + }, + /** Delete Shopping List from the Database + * @param id + */ + async deleteShoppingList(id) { + const response = await apiReq.delete(API_ROUTES.shoppingListsId(id)); + return response.data; + }, +}; diff --git a/frontend/src/components/Fallbacks/NoRecipe.vue b/frontend/src/components/Fallbacks/NoRecipe.vue new file mode 100644 index 00000000..d93b9240 --- /dev/null +++ b/frontend/src/components/Fallbacks/NoRecipe.vue @@ -0,0 +1,17 @@ + + + + + + diff --git a/frontend/src/components/Fallbacks/The404.vue b/frontend/src/components/Fallbacks/The404.vue new file mode 100644 index 00000000..01e57e37 --- /dev/null +++ b/frontend/src/components/Fallbacks/The404.vue @@ -0,0 +1,51 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/MealPlan/MealPlanCard.vue b/frontend/src/components/MealPlan/MealPlanCard.vue index 69330e89..da95e286 100644 --- a/frontend/src/components/MealPlan/MealPlanCard.vue +++ b/frontend/src/components/MealPlan/MealPlanCard.vue @@ -1,14 +1,76 @@