From bde885dc84fb72c78dbe65e09d841a20dd8ece99 Mon Sep 17 00:00:00 2001 From: hay-kot Date: Sun, 8 Aug 2021 20:52:44 -0800 Subject: [PATCH] refactor(frontend): :recycle: rewrite search componenets to typescript --- frontend/api/class-interfaces/categories.ts | 45 +++++ frontend/api/class-interfaces/tags.ts | 45 +++++ frontend/api/index.ts | 15 ++ frontend/api/requests.ts | 5 +- .../Domain/Recipe/RecipeCardSection.vue | 2 +- .../Domain/Recipe/RecipeCategoryTagDialog.vue | 38 ++-- .../Recipe/RecipeCategoryTagSelector.vue | 150 ++++++++++++++++ .../Recipe/RecipeSearchFilterSelector.vue | 57 ++++++ frontend/composables/use-api.ts | 9 +- frontend/composables/use-recipe-context.ts | 22 ++- frontend/composables/use-recipes.ts | 51 ++++++ frontend/composables/use-tags-categories.ts | 60 +++++++ frontend/layouts/default.vue | 4 +- frontend/package.json | 3 +- frontend/pages/admin/dashboard.vue | 4 +- frontend/pages/index.vue | 22 +-- frontend/pages/recipes/all.vue | 25 +-- frontend/pages/recipes/categories/_slug.vue | 41 +++++ frontend/pages/recipes/categories/index.vue | 47 +++++ frontend/pages/recipes/category.vue | 16 -- frontend/pages/recipes/tag.vue | 16 -- frontend/pages/recipes/tags/_slug.vue | 41 +++++ frontend/pages/recipes/tags/index.vue | 47 +++++ frontend/pages/search.vue | 169 +++++++++++++++++- frontend/yarn.lock | 5 + 25 files changed, 826 insertions(+), 113 deletions(-) create mode 100644 frontend/api/class-interfaces/categories.ts create mode 100644 frontend/api/class-interfaces/tags.ts rename frontend.old/src/components/UI/Dialogs/NewCategoryTagDialog.vue => frontend/components/Domain/Recipe/RecipeCategoryTagDialog.vue (64%) create mode 100644 frontend/components/Domain/Recipe/RecipeCategoryTagSelector.vue create mode 100644 frontend/components/Domain/Recipe/RecipeSearchFilterSelector.vue create mode 100644 frontend/composables/use-recipes.ts create mode 100644 frontend/composables/use-tags-categories.ts create mode 100644 frontend/pages/recipes/categories/_slug.vue create mode 100644 frontend/pages/recipes/categories/index.vue delete mode 100644 frontend/pages/recipes/category.vue delete mode 100644 frontend/pages/recipes/tag.vue create mode 100644 frontend/pages/recipes/tags/_slug.vue create mode 100644 frontend/pages/recipes/tags/index.vue diff --git a/frontend/api/class-interfaces/categories.ts b/frontend/api/class-interfaces/categories.ts new file mode 100644 index 00000000..5356b2c8 --- /dev/null +++ b/frontend/api/class-interfaces/categories.ts @@ -0,0 +1,45 @@ +import { BaseCRUDAPI } from "./_base"; + +const prefix = "/api"; + +export interface Category { + name: string; + id: number; + slug: string; +} + +export interface CreateCategory { + name: string; +} + +const routes = { + categories: `${prefix}/categories`, + categoriesEmpty: `${prefix}/categories/empty`, + + categoriesCategory: (category: string) => `${prefix}/categories/${category}`, +}; + +export class CategoriesAPI extends BaseCRUDAPI { + baseRoute: string = routes.categories; + itemRoute = routes.categoriesCategory; + + /** Returns a list of categories that do not contain any recipes + */ + async getEmptyCategories() { + return await this.requests.get(routes.categoriesEmpty); + } + + /** Returns a list of recipes associated with the provided category. + */ + async getAllRecipesByCategory(category: string) { + return await this.requests.get(routes.categoriesCategory(category)); + } + + /** Removes a recipe category from the database. Deleting a + * category does not impact a recipe. The category will be removed + * from any recipes that contain it + */ + async deleteRecipeCategory(category: string) { + return await this.requests.delete(routes.categoriesCategory(category)); + } +} diff --git a/frontend/api/class-interfaces/tags.ts b/frontend/api/class-interfaces/tags.ts new file mode 100644 index 00000000..df26ca16 --- /dev/null +++ b/frontend/api/class-interfaces/tags.ts @@ -0,0 +1,45 @@ +import { BaseCRUDAPI } from "./_base"; + +const prefix = "/api"; + +export interface Tag { + name: string; + id: number; + slug: string; +} + +export interface CreateTag { + name: string; +} + +const routes = { + tags: `${prefix}/tags`, + tagsEmpty: `${prefix}/tags/empty`, + + tagsTag: (tag: string) => `${prefix}/tags/${tag}`, +}; + +export class TagsAPI extends BaseCRUDAPI { + baseRoute: string = routes.tags; + itemRoute = routes.tagsTag; + + /** Returns a list of categories that do not contain any recipes + */ + async getEmptyCategories() { + return await this.requests.get(routes.tagsEmpty); + } + + /** Returns a list of recipes associated with the provided category. + */ + async getAllRecipesByCategory(category: string) { + return await this.requests.get(routes.tagsTag(category)); + } + + /** Removes a recipe category from the database. Deleting a + * category does not impact a recipe. The category will be removed + * from any recipes that contain it + */ + async deleteRecipeCategory(category: string) { + return await this.requests.delete(routes.tagsTag(category)); + } +} diff --git a/frontend/api/index.ts b/frontend/api/index.ts index 1fc08199..7c14654a 100644 --- a/frontend/api/index.ts +++ b/frontend/api/index.ts @@ -5,6 +5,8 @@ import { DebugAPI } from "./class-interfaces/debug"; import { EventsAPI } from "./class-interfaces/events"; import { BackupAPI } from "./class-interfaces/backups"; import { UploadFile } from "./class-interfaces/upload"; +import { CategoriesAPI } from "./class-interfaces/categories"; +import { TagsAPI } from "./class-interfaces/tags"; import { ApiRequestInstance } from "~/types/api"; class Api { @@ -15,6 +17,10 @@ class Api { public debug: DebugAPI; public events: EventsAPI; public backups: BackupAPI; + public categories: CategoriesAPI; + public tags: TagsAPI; + + // Utils public upload: UploadFile; constructor(requests: ApiRequestInstance) { @@ -22,12 +28,21 @@ class Api { return Api.instance; } + // Recipes this.recipes = new RecipeAPI(requests); + this.categories = new CategoriesAPI(requests); + this.tags = new TagsAPI(requests); + + // Users this.users = new UserApi(requests); this.groups = new GroupAPI(requests); + + // Admin this.debug = new DebugAPI(requests); this.events = new EventsAPI(requests); this.backups = new BackupAPI(requests); + + // Utils this.upload = new UploadFile(requests); Object.freeze(this); diff --git a/frontend/api/requests.ts b/frontend/api/requests.ts index 544a99ca..3e9345e9 100644 --- a/frontend/api/requests.ts +++ b/frontend/api/requests.ts @@ -1,6 +1,5 @@ import axios, { AxiosResponse } from "axios"; - interface RequestResponse { response: AxiosResponse | null; data: T | null; @@ -21,9 +20,9 @@ const request = { }; export const requests = { - async get(url: string, queryParams = {}): Promise> { + async get(url: string, params = {}): Promise> { let error = null; - const response = await axios.get(url, { params: { queryParams } }).catch((e) => { + const response = await axios.get(url, { ...params }).catch((e) => { error = e; }); if (response != null) { diff --git a/frontend/components/Domain/Recipe/RecipeCardSection.vue b/frontend/components/Domain/Recipe/RecipeCardSection.vue index cf1d9b16..5e009476 100644 --- a/frontend/components/Domain/Recipe/RecipeCardSection.vue +++ b/frontend/components/Domain/Recipe/RecipeCardSection.vue @@ -126,7 +126,7 @@ export default { default: null, }, hardLimit: { - type: Number, + type: [String, Number], default: 99999, }, mobileCards: { diff --git a/frontend.old/src/components/UI/Dialogs/NewCategoryTagDialog.vue b/frontend/components/Domain/Recipe/RecipeCategoryTagDialog.vue similarity index 64% rename from frontend.old/src/components/UI/Dialogs/NewCategoryTagDialog.vue rename to frontend/components/Domain/Recipe/RecipeCategoryTagDialog.vue index 75c36227..fd7dbaaa 100644 --- a/frontend.old/src/components/UI/Dialogs/NewCategoryTagDialog.vue +++ b/frontend/components/Domain/Recipe/RecipeCategoryTagDialog.vue @@ -1,7 +1,7 @@ diff --git a/frontend/components/Domain/Recipe/RecipeCategoryTagSelector.vue b/frontend/components/Domain/Recipe/RecipeCategoryTagSelector.vue new file mode 100644 index 00000000..cfdfe75a --- /dev/null +++ b/frontend/components/Domain/Recipe/RecipeCategoryTagSelector.vue @@ -0,0 +1,150 @@ + + + + diff --git a/frontend/components/Domain/Recipe/RecipeSearchFilterSelector.vue b/frontend/components/Domain/Recipe/RecipeSearchFilterSelector.vue new file mode 100644 index 00000000..ffb794fd --- /dev/null +++ b/frontend/components/Domain/Recipe/RecipeSearchFilterSelector.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/frontend/composables/use-api.ts b/frontend/composables/use-api.ts index a0797ca3..dc32dd69 100644 --- a/frontend/composables/use-api.ts +++ b/frontend/composables/use-api.ts @@ -23,9 +23,9 @@ const request = { function getRequests(axoisInstance: NuxtAxiosInstance): ApiRequestInstance { const requests = { - async get(url: string, queryParams = {}): Promise> { + async get(url: string, params = {}): Promise> { let error = null; - const response = await axoisInstance.get(url, { params: { queryParams } }).catch((e) => { + const response = await axoisInstance.get(url, params).catch((e) => { error = e; }); if (response != null) { @@ -53,12 +53,9 @@ function getRequests(axoisInstance: NuxtAxiosInstance): ApiRequestInstance { return requests; } - - - export const useApiSingleton = function (): Api { const { $axios } = useContext(); const requests = getRequests($axios); - return new Api(requests) + return new Api(requests); }; diff --git a/frontend/composables/use-recipe-context.ts b/frontend/composables/use-recipe-context.ts index a0a1e902..767b64f3 100644 --- a/frontend/composables/use-recipe-context.ts +++ b/frontend/composables/use-recipe-context.ts @@ -1,37 +1,35 @@ -import { useAsync, ref } from "@nuxtjs/composition-api"; +import { useAsync, ref, reactive } from "@nuxtjs/composition-api"; import { useApiSingleton } from "~/composables/use-api"; import { Recipe } from "~/types/api-types/recipe"; export const useRecipeContext = function () { const api = useApiSingleton(); - const loading = ref(false) - + const loading = ref(false); function getBySlug(slug: string) { - loading.value = true + loading.value = true; const recipe = useAsync(async () => { const { data } = await api.recipes.getOne(slug); return data; }, slug); - loading.value = false + loading.value = false; return recipe; } - async function deleteRecipe(slug: string) { - loading.value = true + async function deleteRecipe(slug: string) { + loading.value = true; const { data } = await api.recipes.deleteOne(slug); - loading.value = false + loading.value = false; return data; } async function updateRecipe(slug: string, recipe: Recipe) { - loading.value = true + loading.value = true; const { data } = await api.recipes.updateOne(slug, recipe); - loading.value = false + loading.value = false; return data; } - - return {loading, getBySlug, deleteRecipe, updateRecipe} + return { loading, getBySlug, deleteRecipe, updateRecipe }; }; diff --git a/frontend/composables/use-recipes.ts b/frontend/composables/use-recipes.ts new file mode 100644 index 00000000..a94cc2a0 --- /dev/null +++ b/frontend/composables/use-recipes.ts @@ -0,0 +1,51 @@ +import { useAsync, ref } from "@nuxtjs/composition-api"; +import { useAsyncKey } from "./use-utils"; +import { useApiSingleton } from "~/composables/use-api"; +import { Recipe } from "~/types/api-types/recipe"; + +export const allRecipes = ref([]); +export const recentRecipes = ref([]); + +export const useRecipes = (all = false, fetchRecipes = true) => { + const api = useApiSingleton(); + + // recipes is non-reactive!! + const { recipes, start, end } = (() => { + if (all) { + return { + recipes: allRecipes, + start: 0, + end: 9999, + }; + } else { + return { + recipes: recentRecipes, + start: 0, + end: 30, + }; + } + })(); + + async function refreshRecipes() { + const { data } = await api.recipes.getAll(start, end); + if (data) { + recipes.value = data; + } + } + + function getAllRecipes() { + useAsync(async () => { + await refreshRecipes(); + }, useAsyncKey()); + } + + function assignSorted(val: Array) { + recipes.value = val; + } + + if (fetchRecipes) { + getAllRecipes(); + } + + return { getAllRecipes, assignSorted }; +}; diff --git a/frontend/composables/use-tags-categories.ts b/frontend/composables/use-tags-categories.ts new file mode 100644 index 00000000..16655e42 --- /dev/null +++ b/frontend/composables/use-tags-categories.ts @@ -0,0 +1,60 @@ +import { Ref, ref, useAsync } from "@nuxtjs/composition-api"; +import { useApiSingleton } from "./use-api"; +import { useAsyncKey } from "./use-utils"; +import { CategoriesAPI, Category } from "~/api/class-interfaces/categories"; +import { Tag, TagsAPI } from "~/api/class-interfaces/tags"; + +export const allCategories = ref([]); +export const allTags = ref([]); + +function baseTagsCategories(reference: Ref | Ref, api: TagsAPI | CategoriesAPI) { + function useAsyncGetAll() { + useAsync(async () => { + await refreshItems(); + }, useAsyncKey()); + } + + async function refreshItems() { + const { data } = await api.getAll(); + reference.value = data; + } + + async function createOne(payload: { name: string }) { + const { data } = await api.createOne(payload); + if (data) { + refreshItems(); + } + } + + async function deleteOne(slug: string) { + const { data } = await api.deleteOne(slug); + if (data) { + refreshItems(); + } + } + + async function updateOne(slug: string, payload: { name: string }) { + // @ts-ignore // TODO: Fix Typescript Issue - Unsure how to fix this while also keeping mixins + const { data } = await api.updateOne(slug, payload); + if (data) { + refreshItems(); + } + } + + return { useAsyncGetAll, refreshItems, createOne, deleteOne, updateOne }; +} + +export const useTags = function () { + const api = useApiSingleton(); + return { + allTags, + ...baseTagsCategories(allTags, api.tags), + }; +}; +export const useCategories = function () { + const api = useApiSingleton(); + return { + allCategories, + ...baseTagsCategories(allCategories, api.categories), + }; +}; diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index f27db35f..3b0f44e9 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -53,12 +53,12 @@ export default defineComponent({ }, { icon: this.$globals.icons.tags, - to: "/recipes/category", + to: "/recipes/categories", title: this.$t("sidebar.categories"), }, { icon: this.$globals.icons.tags, - to: "/recipes/tag", + to: "/recipes/tags", title: this.$t("sidebar.tags"), }, ], diff --git a/frontend/package.json b/frontend/package.json index 9584b3fd..2ee79179 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "@vue/composition-api": "^1.0.5", "@vueuse/core": "^5.2.0", "core-js": "^3.15.1", + "fuse.js": "^6.4.6", "nuxt": "^2.15.7", "nuxt-i18n": "^6.28.0", "vuedraggable": "^2.24.3", @@ -48,4 +49,4 @@ "resolutions": { "vite": "2.3.8" } -} \ No newline at end of file +} diff --git a/frontend/pages/admin/dashboard.vue b/frontend/pages/admin/dashboard.vue index 8a4220b9..cc40245b 100644 --- a/frontend/pages/admin/dashboard.vue +++ b/frontend/pages/admin/dashboard.vue @@ -1,7 +1,7 @@ // TODO: Possibly add confirmation dialog? I'm not sure that it's really requried for events...