diff --git a/frontend/components/Domain/Recipe/RecipeActionMenu.vue b/frontend/components/Domain/Recipe/RecipeActionMenu.vue
index 394509f82..6781ec082 100644
--- a/frontend/components/Domain/Recipe/RecipeActionMenu.vue
+++ b/frontend/components/Domain/Recipe/RecipeActionMenu.vue
@@ -20,7 +20,7 @@
-
+
@@ -63,7 +63,7 @@
/>
-
+
+
+
+
+
diff --git a/frontend/components/Domain/Recipe/RecipeNotes.vue b/frontend/components/Domain/Recipe/RecipeNotes.vue
index 0f22f153d..db29f38dd 100644
--- a/frontend/components/Domain/Recipe/RecipeNotes.vue
+++ b/frontend/components/Domain/Recipe/RecipeNotes.vue
@@ -37,7 +37,8 @@ export default defineComponent({
props: {
value: {
type: Array as () => RecipeNote[],
- required: true,
+ required: false,
+ default: () => [],
},
edit: {
diff --git a/frontend/components/Domain/Recipe/RecipePage/RecipePage.vue b/frontend/components/Domain/Recipe/RecipePage/RecipePage.vue
new file mode 100644
index 000000000..300272ecb
--- /dev/null
+++ b/frontend/components/Domain/Recipe/RecipePage/RecipePage.vue
@@ -0,0 +1,319 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("general.new") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageComments.vue b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageComments.vue
new file mode 100644
index 000000000..077f2c27a
--- /dev/null
+++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageComments.vue
@@ -0,0 +1,117 @@
+
+
+
+
+ {{ $globals.icons.commentTextMultipleOutline }}
+
+ {{ $t("recipe.comments") }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $globals.icons.check }}
+ {{ $t("general.submit") }}
+
+
+
+
+
+
+
+ {{ comment.user.username }} • {{ $d(Date.parse(comment.createdAt), "medium") }}
+ {{ comment.text }}
+
+
+
+ {{ $t("general.delete") }}
+
+
+
+
+
+
+
+
diff --git a/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageEditorToolbar.vue b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageEditorToolbar.vue
new file mode 100644
index 000000000..23fd471d8
--- /dev/null
+++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageEditorToolbar.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageFooter.vue b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageFooter.vue
new file mode 100644
index 000000000..8a557daed
--- /dev/null
+++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageFooter.vue
@@ -0,0 +1,113 @@
+
+
+
+
+
+ {{ $t("recipe.original-url") }}
+
+
+
+
+ API Extras
+
+
+ Recipes extras are a key feature of the Mealie API. They allow you to create custom json key/value pairs
+ within a recipe to reference from 3rd part applications. You can use these keys to contain information to
+ trigger automation or custom messages to relay to your desired device.
+
+
+
+
+
+ {{ $globals.icons.delete }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageHeader.vue b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageHeader.vue
new file mode 100644
index 000000000..37a0f68f3
--- /dev/null
+++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageHeader.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+ {{ recipe.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageIngredientEditor.vue b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageIngredientEditor.vue
new file mode 100644
index 000000000..176822a75
--- /dev/null
+++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageIngredientEditor.vue
@@ -0,0 +1,148 @@
+
+
+
{{ $t("recipe.ingredients") }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $globals.icons.foods }}
+
+ Parse
+
+
+
+ {{ parserToolTip }}
+
+
+ {{ $t("general.new") }}
+
+
+
+
+
diff --git a/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageIngredientToolsView.vue b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageIngredientToolsView.vue
new file mode 100644
index 000000000..2245bfde5
--- /dev/null
+++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageIngredientToolsView.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
Required Tools
+
+
+
+
+ {{ tool.name }}
+
+
+
+
+
+
+
diff --git a/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageInstructions.vue b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageInstructions.vue
new file mode 100644
index 000000000..36ba0a0e6
--- /dev/null
+++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageInstructions.vue
@@ -0,0 +1,700 @@
+
+
+
+
+
+
+
+ {{ $globals.icons.link }}
+
+ {{ $t("recipe.ingredient-linker") }}
+
+
+
+
+
+ {{ activeText }}
+
+
+
+
+
+
+
+
+
+ {{ $t("recipe.linked-to-other-step") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $globals.icons.robot }}
+ {{ $t("recipe.auto") }}
+
+
+
+
+
+
+
+
{{ $t("recipe.instructions") }}
+
+
+ {{ $globals.icons.primary }}
+
+ {{ $t("recipe.cook-mode") }}
+
+
+
+
+
+
+
+ {{ step.title }}
+
+
+
+
+
+
+
+
+ {{ $globals.icons.arrowUpDown }}
+ {{ $t("recipe.step-index", { step: index + 1 }) }}
+
+
+
+
+
+
+
+
+ {{ $globals.icons.checkboxMarkedCircle }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageOrganizers.vue b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageOrganizers.vue
new file mode 100644
index 000000000..0d32c426f
--- /dev/null
+++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageOrganizers.vue
@@ -0,0 +1,93 @@
+
+
+
+
+
+ {{ $t("recipe.categories") }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("tag.tags") }}
+
+
+
+
+
+
+
+
+
+
+ Required Tools
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageScale.vue b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageScale.vue
new file mode 100644
index 000000000..7f6f9c96b
--- /dev/null
+++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageScale.vue
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+ {{ $t("recipe.edit-scale") }}
+
+
+
+
+
+
+
+
diff --git a/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageTitleContent.vue b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageTitleContent.vue
new file mode 100644
index 000000000..182d960b6
--- /dev/null
+++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageTitleContent.vue
@@ -0,0 +1,79 @@
+
+
+
+
+ {{ recipe.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/components/Domain/Recipe/RecipePage/index.ts b/frontend/components/Domain/Recipe/RecipePage/index.ts
new file mode 100644
index 000000000..836372a2a
--- /dev/null
+++ b/frontend/components/Domain/Recipe/RecipePage/index.ts
@@ -0,0 +1,3 @@
+import RecipePage from "./RecipePage.vue";
+
+export default RecipePage;
diff --git a/frontend/components/global/MarkdownEditor.vue b/frontend/components/global/MarkdownEditor.vue
index bbc648325..0816393d0 100644
--- a/frontend/components/global/MarkdownEditor.vue
+++ b/frontend/components/global/MarkdownEditor.vue
@@ -5,7 +5,7 @@
:buttons="[
{
icon: previewState ? $globals.icons.edit : $globals.icons.eye,
- text: previewState ? $t('general.edit') : $t('markdown-editor.preview-markdown-button-label'),
+ text: previewState ? $tc('general.edit') : $tc('markdown-editor.preview-markdown-button-label'),
event: 'toggle',
},
]"
@@ -49,7 +49,7 @@ export default defineComponent({
default: true,
},
textarea: {
- type: Object,
+ type: Object as () => unknown,
default: () => ({}),
},
},
diff --git a/frontend/composables/api/static-routes.ts b/frontend/composables/api/static-routes.ts
index fd2275eca..65429fd33 100644
--- a/frontend/composables/api/static-routes.ts
+++ b/frontend/composables/api/static-routes.ts
@@ -1,6 +1,10 @@
import { useContext } from "@nuxtjs/composition-api";
import { detectServerBaseUrl } from "../use-utils";
+function UnknownToString(ukn: string | unknown) {
+ return typeof ukn === "string" ? ukn : "";
+}
+
export const useStaticRoutes = () => {
const { $config, req } = useContext();
const serverBase = detectServerBaseUrl(req);
@@ -10,16 +14,20 @@ export const useStaticRoutes = () => {
const fullBase = serverBase + prefix;
// Methods to Generate reference urls for assets/images *
- function recipeImage(recipeId: string, version = "", key = 1) {
- return `${fullBase}/media/recipes/${recipeId}/images/original.webp?rnd=${key}&version=${version}`;
+ function recipeImage(recipeId: string, version: string | unknown = "", key: string | number = 1) {
+ return `${fullBase}/media/recipes/${recipeId}/images/original.webp?rnd=${key}&version=${UnknownToString(version)}`;
}
- function recipeSmallImage(recipeId: string, version = "", key = 1) {
- return `${fullBase}/media/recipes/${recipeId}/images/min-original.webp?rnd=${key}&version=${version}`;
+ function recipeSmallImage(recipeId: string, version: string | unknown = "", key: string | number = 1) {
+ return `${fullBase}/media/recipes/${recipeId}/images/min-original.webp?rnd=${key}&version=${UnknownToString(
+ version
+ )}`;
}
- function recipeTinyImage(recipeId: string, version = "", key = 1) {
- return `${fullBase}/media/recipes/${recipeId}/images/tiny-original.webp?rnd=${key}&version=${version}`;
+ function recipeTinyImage(recipeId: string, version: string | unknown = "", key: string | number = 1) {
+ return `${fullBase}/media/recipes/${recipeId}/images/tiny-original.webp?rnd=${key}&version=${UnknownToString(
+ version
+ )}`;
}
function recipeAssetPath(recipeId: string, assetName: string) {
diff --git a/frontend/composables/recipe-page/shared-state.ts b/frontend/composables/recipe-page/shared-state.ts
new file mode 100644
index 000000000..14b3f9cd9
--- /dev/null
+++ b/frontend/composables/recipe-page/shared-state.ts
@@ -0,0 +1,155 @@
+import { computed, ComputedRef, ref, Ref, useContext } from "@nuxtjs/composition-api";
+import { UserOut } from "~/types/api-types/user";
+
+export enum PageMode {
+ EDIT = "EDIT",
+ VIEW = "VIEW",
+ COOK = "COOK",
+}
+
+export enum EditorMode {
+ JSON = "JSON",
+ FORM = "FORM",
+}
+
+/**
+ * PageState encapsulates the state of the recipe page the can be shared across components.
+ * It allows and facilitates the complex state management of the recipe page where many components
+ * need to share and communicate with each other and guarantee consistency.
+ *
+ * **Page Modes**
+ *
+ * are ComputedRefs so we can use a readonly reactive copy of the state of the page.
+ */
+interface PageState {
+ slug: Ref;
+ imageKey: Ref;
+
+ pageMode: ComputedRef;
+ editMode: ComputedRef;
+
+ /**
+ * true is the page is in edit mode and the edit mode is in form mode.
+ */
+ isEditForm: ComputedRef;
+ /**
+ * true is the page is in edit mode and the edit mode is in json mode.
+ */
+ isEditJSON: ComputedRef;
+ /**
+ * true is the page is in view mode.
+ */
+ isEditMode: ComputedRef;
+ /**
+ * true is the page is in cook mode.
+ */
+ isCookMode: ComputedRef;
+
+ setMode: (v: PageMode) => void;
+ setEditMode: (v: EditorMode) => void;
+ toggleEditMode: () => void;
+ toggleCookMode: () => void;
+}
+
+const memo: Record = {};
+
+function pageStateConstructor(slug: string): PageState {
+ const slugRef = ref(slug);
+ const pageModeRef = ref(PageMode.VIEW);
+ const editModeRef = ref(EditorMode.FORM);
+
+ const toggleEditMode = () => {
+ if (editModeRef.value === EditorMode.FORM) {
+ editModeRef.value = EditorMode.JSON;
+ return;
+ }
+ editModeRef.value = EditorMode.FORM;
+ };
+
+ const toggleCookMode = () => {
+ if (pageModeRef.value === PageMode.COOK) {
+ pageModeRef.value = PageMode.VIEW;
+ return;
+ }
+ pageModeRef.value = PageMode.COOK;
+ };
+
+ const setEditMode = (v: EditorMode) => {
+ editModeRef.value = v;
+ };
+
+ const setMode = (toMode: PageMode) => {
+ const fromMode = pageModeRef.value;
+
+ if (fromMode === PageMode.EDIT && toMode === PageMode.VIEW) {
+ setEditMode(EditorMode.FORM);
+ }
+
+ pageModeRef.value = toMode;
+ };
+
+ return {
+ slug: slugRef,
+ pageMode: computed(() => pageModeRef.value),
+ editMode: computed(() => editModeRef.value),
+ imageKey: ref(1),
+
+ toggleEditMode,
+ setMode,
+ setEditMode,
+ toggleCookMode,
+
+ isEditForm: computed(() => {
+ return pageModeRef.value === PageMode.EDIT && editModeRef.value === EditorMode.FORM;
+ }),
+ isEditJSON: computed(() => {
+ return pageModeRef.value === PageMode.EDIT && editModeRef.value === EditorMode.JSON;
+ }),
+ isEditMode: computed(() => {
+ return pageModeRef.value === PageMode.EDIT;
+ }),
+ isCookMode: computed(() => {
+ return pageModeRef.value === PageMode.COOK;
+ }),
+ };
+}
+
+/**
+ * usePageState provides a common way to interact with shared state across the
+ * RecipePage component.
+ */
+export function usePageState(slug: string): PageState {
+ if (!memo[slug]) {
+ memo[slug] = pageStateConstructor(slug);
+ }
+
+ return memo[slug];
+}
+
+export function clearPageState(slug: string) {
+ delete memo[slug];
+}
+
+/**
+ * usePageUser provides a wrapper around $auth that provides a type-safe way to
+ * access the UserOut type from the context. If no user is logged in then an empty
+ * object with all properties set to their zero value is returned.
+ */
+export function usePageUser(): { user: UserOut } {
+ const { $auth } = useContext();
+
+ if (!$auth.user) {
+ return {
+ user: {
+ id: "",
+ group: "",
+ groupId: "",
+ cacheKey: "",
+ email: "",
+ },
+ };
+ }
+
+ // @ts-expect-error - We know that the API always returns a UserOut, but I'm unsure how to type the $auth to know what type user is
+ return { user: $auth.user as UserOut };
+}
diff --git a/frontend/pages/recipe/_slug/index.vue b/frontend/pages/recipe/_slug/index.vue
index ba3e7ba70..793115ec8 100644
--- a/frontend/pages/recipe/_slug/index.vue
+++ b/frontend/pages/recipe/_slug/index.vue
@@ -1,905 +1,32 @@
-
-
-
-
-
-
-
-
-
-
- {{ recipe.name }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ recipe.name }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ $t("recipe.ingredients") }}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $globals.icons.foods }}
-
- Parse
-
-
-
- {{ paserToolTip }}
-
-
- {{ $t("general.new") }}
-
-
-
-
-
-
-
- {{ $t("recipe.edit-scale") }}
-
-
-
-
-
-
-
-
-
-
-
-
-
Required Tools
-
-
-
-
- {{ tool.name }}
-
-
-
-
-
-
-
-
- {{ $t("recipe.categories") }}
-
-
-
-
-
-
-
-
-
-
-
- {{ $t("tag.tags") }}
-
-
-
-
-
-
-
-
-
-
- Required Tools
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t("general.new") }}
-
-
-
-
-
-
- Required Tools
-
-
-
-
-
-
-
-
-
- {{ $t("recipe.categories") }}
-
-
-
-
-
-
-
-
-
-
-
- {{ $t("tag.tags") }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t("recipe.original-url") }}
-
-
-
-
-
- API Extras
-
-
- Recipes extras are a key feature of the Mealie API. They allow you to create custom json key/value pairs
- within a recipe to reference from 3rd part applications. You can use these keys to contain information to
- trigger automation or custom messages to relay to your desired device.
-
-
-
-
-
- {{ $globals.icons.delete }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
+
diff --git a/frontend/pages/recipe/_slug/old.vue b/frontend/pages/recipe/_slug/old.vue
new file mode 100644
index 000000000..ba3e7ba70
--- /dev/null
+++ b/frontend/pages/recipe/_slug/old.vue
@@ -0,0 +1,905 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ recipe.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ recipe.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t("recipe.ingredients") }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $globals.icons.foods }}
+
+ Parse
+
+
+
+ {{ paserToolTip }}
+
+
+ {{ $t("general.new") }}
+
+
+
+
+
+
+
+ {{ $t("recipe.edit-scale") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
Required Tools
+
+
+
+
+ {{ tool.name }}
+
+
+
+
+
+
+
+
+ {{ $t("recipe.categories") }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("tag.tags") }}
+
+
+
+
+
+
+
+
+
+
+ Required Tools
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("general.new") }}
+
+
+
+
+
+
+ Required Tools
+
+
+
+
+
+
+
+
+
+ {{ $t("recipe.categories") }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("tag.tags") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("recipe.original-url") }}
+
+
+
+
+
+ API Extras
+
+
+ Recipes extras are a key feature of the Mealie API. They allow you to create custom json key/value pairs
+ within a recipe to reference from 3rd part applications. You can use these keys to contain information to
+ trigger automation or custom messages to relay to your desired device.
+
+
+
+
+
+ {{ $globals.icons.delete }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/types/api.ts b/frontend/types/api.ts
index 6d13a2572..2881927f7 100644
--- a/frontend/types/api.ts
+++ b/frontend/types/api.ts
@@ -1,5 +1,7 @@
import { AxiosResponse } from "axios";
+export type NoUndefinedField = { [P in keyof T]-?: NoUndefinedField> };
+
export interface RequestResponse {
response: AxiosResponse | null;
data: T | null;
diff --git a/frontend/types/ts-shim.d.ts b/frontend/types/ts-shim.d.ts
index b39982764..0660bd67a 100644
--- a/frontend/types/ts-shim.d.ts
+++ b/frontend/types/ts-shim.d.ts
@@ -1,4 +1,4 @@
declare module "*.vue" {
- import Vue from "vue"
- export default Vue
+ import Vue from "vue";
+ export default Vue;
}
diff --git a/template.vue b/template.vue
new file mode 100644
index 000000000..aae72df7e
--- /dev/null
+++ b/template.vue
@@ -0,0 +1,27 @@
+
+
+
+
+