From caa9e0305092987a5b4cb161b2c9495173767749 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sat, 27 Aug 2022 10:44:58 -0800 Subject: [PATCH] refactor: recipe-page (#1587) Refactor recipe page to use break up the component and make it more usable across different pages. I've left the old route in as well in case there is some functional breaks, I plan to remove it before the official release once we've tested the new editor some more in production. For now there will just have to be some duplicate components and pages around. --- .../Domain/Recipe/RecipeActionMenu.vue | 12 +- .../Domain/Recipe/RecipeIngredientHtml.vue | 15 + .../components/Domain/Recipe/RecipeNotes.vue | 3 +- .../Domain/Recipe/RecipePage/RecipePage.vue | 319 ++++++ .../RecipePageParts/RecipePageComments.vue | 117 +++ .../RecipePageEditorToolbar.vue | 59 ++ .../RecipePageParts/RecipePageFooter.vue | 113 +++ .../RecipePageParts/RecipePageHeader.vue | 112 +++ .../RecipePageIngredientEditor.vue | 148 +++ .../RecipePageIngredientToolsView.vue | 60 ++ .../RecipePageInstructions.vue | 700 ++++++++++++++ .../RecipePageParts/RecipePageOrganizers.vue | 93 ++ .../RecipePageParts/RecipePageScale.vue | 101 ++ .../RecipePageTitleContent.vue | 79 ++ .../Domain/Recipe/RecipePage/index.ts | 3 + frontend/components/global/MarkdownEditor.vue | 4 +- frontend/composables/api/static-routes.ts | 20 +- .../composables/recipe-page/shared-state.ts | 155 +++ frontend/pages/recipe/_slug/index.vue | 897 +---------------- frontend/pages/recipe/_slug/old.vue | 905 ++++++++++++++++++ frontend/types/api.ts | 2 + frontend/types/ts-shim.d.ts | 4 +- template.vue | 27 + 23 files changed, 3046 insertions(+), 902 deletions(-) create mode 100644 frontend/components/Domain/Recipe/RecipeIngredientHtml.vue create mode 100644 frontend/components/Domain/Recipe/RecipePage/RecipePage.vue create mode 100644 frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageComments.vue create mode 100644 frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageEditorToolbar.vue create mode 100644 frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageFooter.vue create mode 100644 frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageHeader.vue create mode 100644 frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageIngredientEditor.vue create mode 100644 frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageIngredientToolsView.vue create mode 100644 frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageInstructions.vue create mode 100644 frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageOrganizers.vue create mode 100644 frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageScale.vue create mode 100644 frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageTitleContent.vue create mode 100644 frontend/components/Domain/Recipe/RecipePage/index.ts create mode 100644 frontend/composables/recipe-page/shared-state.ts create mode 100644 frontend/pages/recipe/_slug/old.vue create mode 100644 template.vue diff --git a/frontend/components/Domain/Recipe/RecipeActionMenu.vue b/frontend/components/Domain/Recipe/RecipeActionMenu.vue index 394509f82831..6781ec08213e 100644 --- a/frontend/components/Domain/Recipe/RecipeActionMenu.vue +++ b/frontend/components/Domain/Recipe/RecipeActionMenu.vue @@ -20,7 +20,7 @@ -
+
+ + diff --git a/frontend/components/Domain/Recipe/RecipeNotes.vue b/frontend/components/Domain/Recipe/RecipeNotes.vue index 0f22f153d6bb..db29f38ddcc2 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 000000000000..300272ecb6fe --- /dev/null +++ b/frontend/components/Domain/Recipe/RecipePage/RecipePage.vue @@ -0,0 +1,319 @@ + + + + + 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 000000000000..077f2c27acb1 --- /dev/null +++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageComments.vue @@ -0,0 +1,117 @@ + + + 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 000000000000..23fd471d8ace --- /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 000000000000..8a557daed856 --- /dev/null +++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageFooter.vue @@ -0,0 +1,113 @@ + + + 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 000000000000..37a0f68f3a98 --- /dev/null +++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageHeader.vue @@ -0,0 +1,112 @@ + + + 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 000000000000..176822a75742 --- /dev/null +++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageIngredientEditor.vue @@ -0,0 +1,148 @@ + + + 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 000000000000..2245bfde5dd9 --- /dev/null +++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageIngredientToolsView.vue @@ -0,0 +1,60 @@ + + + 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 000000000000..36ba0a0e6d1f --- /dev/null +++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageInstructions.vue @@ -0,0 +1,700 @@ + + + + + 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 000000000000..0d32c426fc38 --- /dev/null +++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageOrganizers.vue @@ -0,0 +1,93 @@ + + + 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 000000000000..7f6f9c96b757 --- /dev/null +++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageScale.vue @@ -0,0 +1,101 @@ + + + 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 000000000000..182d960b6fae --- /dev/null +++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageTitleContent.vue @@ -0,0 +1,79 @@ + + + diff --git a/frontend/components/Domain/Recipe/RecipePage/index.ts b/frontend/components/Domain/Recipe/RecipePage/index.ts new file mode 100644 index 000000000000..836372a2a928 --- /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 bbc648325d02..0816393d089c 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 fd2275eca9da..65429fd3390e 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 000000000000..14b3f9cd9f5a --- /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 ba3e7ba70ff2..793115ec82f9 100644 --- a/frontend/pages/recipe/_slug/index.vue +++ b/frontend/pages/recipe/_slug/index.vue @@ -1,905 +1,32 @@ - + diff --git a/frontend/pages/recipe/_slug/old.vue b/frontend/pages/recipe/_slug/old.vue new file mode 100644 index 000000000000..ba3e7ba70ff2 --- /dev/null +++ b/frontend/pages/recipe/_slug/old.vue @@ -0,0 +1,905 @@ + + + + + diff --git a/frontend/types/api.ts b/frontend/types/api.ts index 6d13a2572887..2881927f7e77 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 b39982764200..0660bd67a54e 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 000000000000..aae72df7e065 --- /dev/null +++ b/template.vue @@ -0,0 +1,27 @@ + + +