mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-27 08:14:30 -04:00 
			
		
		
		
	refactor(frontend): ♻️ rewrite search componenets to typescript
This commit is contained in:
		| @@ -1,7 +1,7 @@ | ||||
| // TODO: Possibly add confirmation dialog? I'm not sure that it's really requried for events... | ||||
|  | ||||
| <template> | ||||
|   <v-container class="mt-10"> | ||||
|   <v-container v-if="statistics" class="mt-10"> | ||||
|     <v-row v-if="statistics"> | ||||
|       <v-col cols="12" sm="12" md="4"> | ||||
|         <BaseStatCard :icon="$globals.icons.primary"> | ||||
| @@ -121,7 +121,7 @@ export default defineComponent({ | ||||
|       const events = useAsync(async () => { | ||||
|         const { data } = await api.events.getEvents(); | ||||
|         return data; | ||||
|       }); | ||||
|       }, useAsyncKey()); | ||||
|       return events; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,35 +1,25 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <RecipeCardSection | ||||
|       v-if="recentRecipes" | ||||
|       :icon="$globals.icons.primary" | ||||
|       :title="$t('general.recent')" | ||||
|       :recipes="recipes" | ||||
|       :recipes="recentRecipes" | ||||
|     ></RecipeCardSection> | ||||
|   </v-container> | ||||
| </template> | ||||
|    | ||||
|   <script lang="ts"> | ||||
| import { defineComponent, useAsync } from "@nuxtjs/composition-api"; | ||||
| import { defineComponent } from "@nuxtjs/composition-api"; | ||||
| import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| import { useRecipes, recentRecipes } from "~/composables/use-recipes"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   components: { RecipeCardSection }, | ||||
|   setup() { | ||||
|     const api = useApiSingleton(); | ||||
|     const { assignSorted } = useRecipes(false); | ||||
|  | ||||
|     const recipes = useAsync(async () => { | ||||
|       const { data } = await api.recipes.getAll(); | ||||
|       return data; | ||||
|     }); | ||||
|  | ||||
|     // const recipes = ref<Recipe[] | null>([]); | ||||
|     // onMounted(async () => { | ||||
|     //   const { data } = await api.recipes.getAll(); | ||||
|     //   recipes.value = data; | ||||
|     // }); | ||||
|  | ||||
|     return { api, recipes }; | ||||
|     return { recentRecipes, assignSorted }; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|   | ||||
| @@ -1,37 +1,26 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <RecipeCardSection | ||||
|       v-if="allRecipes" | ||||
|       :icon="$globals.icons.primary" | ||||
|       :title="$t('page.all-recipes')" | ||||
|       :recipes="recipes" | ||||
|       :recipes="allRecipes" | ||||
|       @sort="assignSorted" | ||||
|     ></RecipeCardSection> | ||||
|   </v-container> | ||||
| </template> | ||||
|    | ||||
|   <script lang="ts"> | ||||
| import { defineComponent, onMounted, ref } from "@nuxtjs/composition-api"; | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from "@nuxtjs/composition-api"; | ||||
| import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| import { Recipe } from "~/types/api-types/admin"; | ||||
| import { useRecipes, allRecipes } from "~/composables/use-recipes"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   components: { RecipeCardSection }, | ||||
|   setup() { | ||||
|     const api = useApiSingleton(); | ||||
|     const { assignSorted } = useRecipes(true); | ||||
|  | ||||
|     const recipes = ref<Recipe[] | null>([]); | ||||
|     onMounted(async () => { | ||||
|       const { data } = await api.recipes.getAll(); | ||||
|       recipes.value = data; | ||||
|     }); | ||||
|  | ||||
|     return { api, recipes }; | ||||
|   }, | ||||
|   methods: { | ||||
|     assignSorted(val: Array<Recipe>) { | ||||
|       this.recipes = val; | ||||
|     }, | ||||
|     return { allRecipes, assignSorted }; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|   | ||||
							
								
								
									
										41
									
								
								frontend/pages/recipes/categories/_slug.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								frontend/pages/recipes/categories/_slug.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <RecipeCardSection | ||||
|       v-if="category" | ||||
|       :icon="$globals.icons.tags" | ||||
|       :title="category.name" | ||||
|       :recipes="category.recipes" | ||||
|       @sort="assignSorted" | ||||
|     ></RecipeCardSection> | ||||
|   </v-container> | ||||
| </template> | ||||
|      | ||||
| <script lang="ts"> | ||||
| import { defineComponent, useAsync, useRoute } from "@nuxtjs/composition-api"; | ||||
| import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| import { Recipe } from "~/types/api-types/recipe"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   components: { RecipeCardSection }, | ||||
|   setup() { | ||||
|     const api = useApiSingleton(); | ||||
|     const route = useRoute(); | ||||
|     const slug = route.value.params.slug; | ||||
|  | ||||
|     const category = useAsync(async () => { | ||||
|       const { data } = await api.categories.getOne(slug); | ||||
|       return data; | ||||
|     }, slug); | ||||
|     return { category }; | ||||
|   }, | ||||
|   methods: { | ||||
|     assignSorted(val: Array<Recipe>) { | ||||
|       this.category.recipes = val; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|      | ||||
| <style scoped> | ||||
| </style> | ||||
							
								
								
									
										47
									
								
								frontend/pages/recipes/categories/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								frontend/pages/recipes/categories/index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| <template> | ||||
|   <v-container v-if="categories"> | ||||
|     <v-app-bar color="transparent" flat class="mt-n1 rounded"> | ||||
|       <v-icon large left> | ||||
|         {{ $globals.icons.tags }} | ||||
|       </v-icon> | ||||
|       <v-toolbar-title class="headline"> {{ $t("recipe.categories") }} </v-toolbar-title> | ||||
|       <v-spacer></v-spacer> | ||||
|     </v-app-bar> | ||||
|     <v-slide-x-transition hide-on-leave> | ||||
|       <v-row> | ||||
|         <v-col v-for="item in categories" :key="item.id" cols="12" :sm="12" :md="6" :lg="4" :xl="3"> | ||||
|           <v-card hover :to="`/recipes/categories/${item.slug}`"> | ||||
|             <v-card-actions> | ||||
|               <v-icon> | ||||
|                 {{ $globals.icons.tags }} | ||||
|               </v-icon> | ||||
|               <v-card-title class="py-1">{{ item.name }}</v-card-title> | ||||
|               <v-spacer></v-spacer> | ||||
|             </v-card-actions> | ||||
|           </v-card> | ||||
|         </v-col> | ||||
|       </v-row> | ||||
|     </v-slide-x-transition> | ||||
|   </v-container> | ||||
| </template> | ||||
|    | ||||
| <script lang="ts"> | ||||
| import { defineComponent, useAsync } from "@nuxtjs/composition-api"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| import { useAsyncKey } from "~/composables/use-utils"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   setup() { | ||||
|     const api = useApiSingleton(); | ||||
|  | ||||
|     const categories = useAsync(async () => { | ||||
|       const { data } = await api.categories.getAll(); | ||||
|       return data; | ||||
|     }, useAsyncKey()); | ||||
|     return { categories, api }; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|    | ||||
| <style scoped> | ||||
| </style> | ||||
| @@ -1,16 +0,0 @@ | ||||
| <template> | ||||
|   <div></div> | ||||
| </template> | ||||
|    | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from "@nuxtjs/composition-api"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   setup() { | ||||
|     return {}; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|    | ||||
| <style scoped> | ||||
| </style> | ||||
| @@ -1,16 +0,0 @@ | ||||
| <template> | ||||
|   <div></div> | ||||
| </template> | ||||
|    | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from "@nuxtjs/composition-api"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   setup() { | ||||
|     return {}; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|    | ||||
| <style scoped> | ||||
| </style> | ||||
							
								
								
									
										41
									
								
								frontend/pages/recipes/tags/_slug.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								frontend/pages/recipes/tags/_slug.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <RecipeCardSection | ||||
|       v-if="tag" | ||||
|       :icon="$globals.icons.tags" | ||||
|       :title="tag.name" | ||||
|       :recipes="tag.recipes" | ||||
|       @sort="assignSorted" | ||||
|     ></RecipeCardSection> | ||||
|   </v-container> | ||||
| </template> | ||||
|      | ||||
| <script lang="ts"> | ||||
| import { defineComponent, useAsync, useRoute } from "@nuxtjs/composition-api"; | ||||
| import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| import { Recipe } from "~/types/api-types/admin"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   components: { RecipeCardSection }, | ||||
|   setup() { | ||||
|     const api = useApiSingleton(); | ||||
|     const route = useRoute(); | ||||
|     const slug = route.value.params.slug; | ||||
|  | ||||
|     const tag = useAsync(async () => { | ||||
|       const { data } = await api.tags.getOne(slug); | ||||
|       return data; | ||||
|     }, slug); | ||||
|     return { tag }; | ||||
|   }, | ||||
|   methods: { | ||||
|     assignSorted(val: Array<Recipe>) { | ||||
|       this.tag.recipes = val; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|      | ||||
| <style scoped> | ||||
| </style> | ||||
							
								
								
									
										47
									
								
								frontend/pages/recipes/tags/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								frontend/pages/recipes/tags/index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| <template> | ||||
|   <v-container v-if="tags"> | ||||
|     <v-app-bar color="transparent" flat class="mt-n1 rounded"> | ||||
|       <v-icon large left> | ||||
|         {{ $globals.icons.tags }} | ||||
|       </v-icon> | ||||
|       <v-toolbar-title class="headline"> {{ $t("tag.tags") }} </v-toolbar-title> | ||||
|       <v-spacer></v-spacer> | ||||
|     </v-app-bar> | ||||
|     <v-slide-x-transition hide-on-leave> | ||||
|       <v-row> | ||||
|         <v-col v-for="item in tags" :key="item.id" cols="12" :sm="12" :md="6" :lg="4" :xl="3"> | ||||
|           <v-card hover :to="`/recipes/tags/${item.slug}`"> | ||||
|             <v-card-actions> | ||||
|               <v-icon> | ||||
|                 {{ $globals.icons.tags }} | ||||
|               </v-icon> | ||||
|               <v-card-title class="py-1">{{ item.name }}</v-card-title> | ||||
|               <v-spacer></v-spacer> | ||||
|             </v-card-actions> | ||||
|           </v-card> | ||||
|         </v-col> | ||||
|       </v-row> | ||||
|     </v-slide-x-transition> | ||||
|   </v-container> | ||||
| </template> | ||||
|    | ||||
| <script lang="ts"> | ||||
| import { defineComponent, useAsync } from "@nuxtjs/composition-api"; | ||||
| import { useApiSingleton } from "~/composables/use-api"; | ||||
| import { useAsyncKey } from "~/composables/use-utils"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   setup() { | ||||
|     const api = useApiSingleton(); | ||||
|  | ||||
|     const tags = useAsync(async () => { | ||||
|       const { data } = await api.tags.getAll(); | ||||
|       return data; | ||||
|     }, useAsyncKey()); | ||||
|     return { tags, api }; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|    | ||||
| <style scoped> | ||||
| </style> | ||||
| @@ -1,16 +1,169 @@ | ||||
| <template> | ||||
|   <div></div> | ||||
|   <v-container> | ||||
|     <v-row dense> | ||||
|       <v-col> | ||||
|         <v-text-field | ||||
|           v-model="searchString" | ||||
|           outlined | ||||
|           color="primary accent-3" | ||||
|           :placeholder="$t('search.search-placeholder')" | ||||
|           :append-icon="$globals.icons.search" | ||||
|         > | ||||
|         </v-text-field> | ||||
|       </v-col> | ||||
|       <v-col cols="12" md="2" sm="12"> | ||||
|         <v-text-field v-model="maxResults" class="mt-0 pt-0" :label="$t('search.max-results')" type="number" outlined /> | ||||
|       </v-col> | ||||
|     </v-row> | ||||
|  | ||||
|     <v-row dense class="my-0 flex-row align-center justify-space-around"> | ||||
|       <v-col> | ||||
|         <h3 class="pl-2 text-center headline"> | ||||
|           {{ $t("category.category-filter") }} | ||||
|         </h3> | ||||
|         <RecipeSearchFilterSelector class="mb-1" @update="updateCatParams" /> | ||||
|         <RecipeCategoryTagSelector v-model="includeCategories" :solo="true" :dense="false" :return-object="false" /> | ||||
|       </v-col> | ||||
|       <v-col> | ||||
|         <h3 class="pl-2 text-center headline"> | ||||
|           {{ $t("search.tag-filter") }} | ||||
|         </h3> | ||||
|         <RecipeSearchFilterSelector class="mb-1" @update="updateTagParams" /> | ||||
|         <RecipeCategoryTagSelector | ||||
|           v-model="includeTags" | ||||
|           :solo="true" | ||||
|           :dense="false" | ||||
|           :return-object="false" | ||||
|           :tag-selector="true" | ||||
|         /> | ||||
|       </v-col> | ||||
|     </v-row> | ||||
|  | ||||
|     <RecipeCardSection | ||||
|       class="mt-n9" | ||||
|       :title-icon="$globals.icons.magnify" | ||||
|       :recipes="showRecipes" | ||||
|       :hard-limit="maxResults" | ||||
|       @sort="assignFuzzy" | ||||
|     /> | ||||
|   </v-container> | ||||
| </template> | ||||
|      | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from "@nuxtjs/composition-api"; | ||||
|  | ||||
| <script> | ||||
| import Fuse from "fuse.js"; | ||||
| import { defineComponent } from "vue-demi"; | ||||
| import RecipeSearchFilterSelector from "~/components/Domain/Recipe/RecipeSearchFilterSelector.vue"; | ||||
| import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategoryTagSelector.vue"; | ||||
| import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; | ||||
| import { useRecipes, allRecipes } from "~/composables/use-recipes"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   components: { | ||||
|     RecipeCategoryTagSelector, | ||||
|     RecipeSearchFilterSelector, | ||||
|     RecipeCardSection, | ||||
|   }, | ||||
|   setup() { | ||||
|     return {}; | ||||
|     const { assignSorted } = useRecipes(true); | ||||
|  | ||||
|     return { assignSorted, allRecipes }; | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       maxResults: 21, | ||||
|       searchResults: [], | ||||
|       catFilter: { | ||||
|         exclude: false, | ||||
|         matchAny: false, | ||||
|       }, | ||||
|       tagFilter: { | ||||
|         exclude: false, | ||||
|         matchAny: false, | ||||
|       }, | ||||
|       sortedResults: [], | ||||
|       includeCategories: [], | ||||
|       includeTags: [], | ||||
|       options: { | ||||
|         shouldSort: true, | ||||
|         threshold: 0.6, | ||||
|         location: 0, | ||||
|         distance: 100, | ||||
|         findAllMatches: true, | ||||
|         maxPatternLength: 32, | ||||
|         minMatchCharLength: 2, | ||||
|         keys: ["name", "description"], | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     searchString: { | ||||
|       set(q) { | ||||
|         this.$router.replace({ query: { ...this.$route.query, q } }); | ||||
|       }, | ||||
|       get() { | ||||
|         return this.$route.query.q || ""; | ||||
|       }, | ||||
|     }, | ||||
|     filteredRecipes() { | ||||
|       return this.allRecipes.filter((recipe) => { | ||||
|         const includesTags = this.check(this.includeTags, recipe.tags, this.tagFilter.matchAny, this.tagFilter.exclude); | ||||
|         const includesCats = this.check( | ||||
|           this.includeCategories, | ||||
|           recipe.recipeCategory, | ||||
|           this.catFilter.matchAny, | ||||
|           this.catFilter.exclude | ||||
|         ); | ||||
|         return [includesTags, includesCats].every((x) => x === true); | ||||
|       }); | ||||
|     }, | ||||
|     fuse() { | ||||
|       return new Fuse(this.filteredRecipes, this.options); | ||||
|     }, | ||||
|     fuzzyRecipes() { | ||||
|       if (this.searchString.trim() === "") { | ||||
|         return this.filteredRecipes; | ||||
|       } | ||||
|       const result = this.fuse.search(this.searchString.trim()); | ||||
|       return result.map((x) => x.item); | ||||
|     }, | ||||
|     isSearching() { | ||||
|       return this.searchString && this.searchString.length > 0; | ||||
|     }, | ||||
|     showRecipes() { | ||||
|       if (this.sortedResults.length > 0) { | ||||
|         return this.sortedResults; | ||||
|       } else { | ||||
|         return this.fuzzyRecipes; | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     assignFuzzy(val) { | ||||
|       this.sortedResults = val; | ||||
|     }, | ||||
|     check(filterBy, recipeList, matchAny, exclude) { | ||||
|       let isMatch = true; | ||||
|       if (filterBy.length === 0) return isMatch; | ||||
|  | ||||
|       if (recipeList) { | ||||
|         if (matchAny) { | ||||
|           isMatch = filterBy.some((t) => recipeList.includes(t)); // Checks if some items are a match | ||||
|         } else { | ||||
|           isMatch = filterBy.every((t) => recipeList.includes(t)); // Checks if every items is a match | ||||
|         } | ||||
|         return exclude ? !isMatch : isMatch; | ||||
|       } else; | ||||
|       return false; | ||||
|     }, | ||||
|  | ||||
|     updateTagParams(params) { | ||||
|       this.tagFilter = params; | ||||
|     }, | ||||
|     updateCatParams(params) { | ||||
|       this.catFilter = params; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|      | ||||
| <style scoped> | ||||
| </style> | ||||
|  | ||||
| <style></style> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user