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 394509f82..6781ec082 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 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 @@ + + + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ - + 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 @@ + + + + + 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 @@ + + +