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 @@ + + +