mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-28 04:33:14 -05:00
first steps to advanced search
This commit is contained in:
@@ -45,11 +45,12 @@ import {ApiApi, ApiRecipeListRequest, Keyword, Recipe, RecipeOverview} from "@/o
|
||||
import {homePageCols} from "@/utils/breakpoint_utils";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {DateTime} from "luxon";
|
||||
import {tr} from "vuetify/locale";
|
||||
|
||||
//TODO mode ideas "last year/month/cooked long ago"
|
||||
const props = defineProps(
|
||||
{
|
||||
mode: {type: String as PropType<'recent' | 'new' | 'keyword' | 'rating'>, required: true},
|
||||
mode: {type: String as PropType<'recent' | 'new' | 'keyword' | 'rating' | 'random'>, required: true},
|
||||
skeletons: {type: Number, default: 0},
|
||||
}
|
||||
)
|
||||
@@ -68,6 +69,8 @@ const title = computed(() => {
|
||||
switch (props.mode) {
|
||||
case 'recent':
|
||||
return t('Recently_Viewed')
|
||||
case 'random':
|
||||
return t('Random Recipes')
|
||||
case 'new':
|
||||
return t('New')
|
||||
case 'rating':
|
||||
@@ -87,6 +90,8 @@ const icon = computed(() => {
|
||||
switch (props.mode) {
|
||||
case 'recent':
|
||||
return 'fa-solid fa-eye'
|
||||
case 'random':
|
||||
return 'fa-solid fa-dice'
|
||||
case 'new':
|
||||
return 'fa-solid fa-calendar-alt'
|
||||
case 'rating':
|
||||
@@ -116,9 +121,11 @@ function loadRecipes() {
|
||||
|
||||
switch (props.mode) {
|
||||
case 'recent':
|
||||
// TODO implement correct parameter
|
||||
requestParameters.numRecent = 16
|
||||
break;
|
||||
case 'random':
|
||||
requestParameters.random = 'true'
|
||||
break;
|
||||
case 'new':
|
||||
requestParameters._new = 'true'
|
||||
break;
|
||||
|
||||
@@ -91,21 +91,20 @@ const searchResults = computed(() => {
|
||||
let searchResults = [] as Array<SearchResult>
|
||||
|
||||
if (searchQuery.value != '' && searchQuery.value != null) {
|
||||
// TODO add link to advanced search once it exists
|
||||
//searchResults.push({name: searchQuery.value, icon: 'fas fa-search', suffix: 'Advanced Search'} as SearchResult)
|
||||
|
||||
flatRecipes.value.filter(fr => fr.name.toLowerCase().includes(searchQuery.value.toLowerCase())).slice(0, 10).forEach(r => {
|
||||
searchResults.push({name: r.name, image: r.image, recipeId: r.id} as SearchResult)
|
||||
searchResults.push({name: r.name, image: r.image, recipeId: r.id, type: "recipe"} as SearchResult)
|
||||
})
|
||||
|
||||
if (searchResults.length < 3) {
|
||||
asyncSearchResults.value.slice(0, 5).forEach(r => {
|
||||
if (searchResults.findIndex(x => x.recipeId == r.id) == -1) {
|
||||
searchResults.push({name: r.name, image: r.image, recipeId: r.id})
|
||||
searchResults.push({name: r.name, image: r.image, recipeId: r.id, type: "recipe"})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
searchResults.push({name: searchQuery.value, icon: 'fas fa-search', type: "link_advanced_search"} as SearchResult)
|
||||
|
||||
} else {
|
||||
// show first 5 recipes by default
|
||||
|
||||
@@ -212,10 +211,17 @@ function cardVariant(index: number) {
|
||||
function goToSelectedRecipe(index: number) {
|
||||
dialog.value = false
|
||||
let searchResult = searchResults.value[index]
|
||||
console.log('going to', searchResult.recipeId)
|
||||
if (searchResult.recipeId != null) {
|
||||
router.push({name: 'view_recipe', params: {'id': searchResult.recipeId}})
|
||||
|
||||
if (searchResult.type == 'link_advanced_search') {
|
||||
router.push({name: 'view_search', query: {'query': searchQuery.value}})
|
||||
} else {
|
||||
console.log('going to', searchResult.recipeId)
|
||||
if (searchResult.recipeId != null) {
|
||||
router.push({name: 'view_recipe', params: {'id': searchResult.recipeId}})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
:on-create="createObject"
|
||||
:createOption="props.allowCreate"
|
||||
:delay="300"
|
||||
:object="true"
|
||||
:object="props.object"
|
||||
:valueProp="itemValue"
|
||||
:label="itemLabel"
|
||||
:searchable="true"
|
||||
@@ -72,6 +72,7 @@ const props = defineProps({
|
||||
|
||||
mode: {type: String as PropType<'single' | 'multiple' | 'tags'>, default: 'single'},
|
||||
appendToBody: {type: Boolean, default: false},
|
||||
object: {type: Boolean, default: true},
|
||||
|
||||
allowCreate: {type: Boolean, default: false},
|
||||
|
||||
|
||||
46
vue3/src/components/model_editors/CustomFilterEditor.vue
Normal file
46
vue3/src/components/model_editors/CustomFilterEditor.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<model-editor-base
|
||||
:loading="loading"
|
||||
:dialog="dialog"
|
||||
@save="saveObject"
|
||||
@delete="deleteObject"
|
||||
@close="emit('close')"
|
||||
:is-update="isUpdate()"
|
||||
:is-changed="editingObjChanged"
|
||||
:model-class="modelClass"
|
||||
:object-name="editingObjName()">
|
||||
<v-card-text>
|
||||
<v-form :disabled="loading">
|
||||
Coming Soon
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</model-editor-base>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, PropType} from "vue";
|
||||
import {CustomFilter} from "@/openapi";
|
||||
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
|
||||
const props = defineProps({
|
||||
item: {type: {} as PropType<CustomFilter>, required: false, default: null},
|
||||
itemId: {type: [Number, String], required: false, default: undefined},
|
||||
dialog: {type: Boolean, default: false}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['create', 'save', 'delete', 'close'])
|
||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<CustomFilter>('CustomFilter', emit)
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
setupState(props.item, props.itemId, {})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -179,7 +179,6 @@ const selectedFood = ref<null | Food>(null)
|
||||
const selectedUnit = ref<null | Unit>(null)
|
||||
|
||||
const deleteConfirmDialog = ref(false)
|
||||
const deleteConfirmIngredient = ref({} as EditorIngredient)
|
||||
|
||||
onMounted(() => {
|
||||
getAndLoadParameters()
|
||||
|
||||
@@ -1,43 +1,137 @@
|
||||
<template>
|
||||
|
||||
<v-container>
|
||||
<v-btn @click="testApi">Test API</v-btn>
|
||||
</v-container>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-card :loading="loading">
|
||||
<v-card-title>
|
||||
{{ $t('Search') }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form :disabled="loading">
|
||||
<v-text-field :label="$t('Search')" v-model="searchParameters.query" clearable></v-text-field>
|
||||
|
||||
<model-select model="Keyword" mode="tags" v-model="searchParameters.keywords" :object="false"></model-select>
|
||||
<model-select model="Food" mode="tags" v-model="searchParameters.foods"></model-select>
|
||||
<model-select model="Unit" mode="tags" v-model="searchParameters.units"></model-select>
|
||||
<model-select model="RecipeBook" mode="tags" v-model="searchParameters.books"></model-select>
|
||||
|
||||
<!-- <v-number-input :label="$t('times_cooked')" v-model="searchParameters.timescooked" clearable></v-number-input>-->
|
||||
<!-- <v-date-input :label="$t('last_cooked')" v-model="searchParameters.cookedon" clearable></v-date-input>-->
|
||||
<!-- <v-date-input :label="$t('last_viewed')" v-model="searchParameters.viewedon" clearable></v-date-input>-->
|
||||
<!-- <v-date-input :label="$t('created_on')" v-model="searchParameters.createdon" clearable></v-date-input>-->
|
||||
<!-- <v-date-input :label="$t('updatedon')" v-model="searchParameters.updatedon" clearable></v-date-input>-->
|
||||
|
||||
<v-checkbox :label="$t('make_now')" v-model="searchParameters.makenow"></v-checkbox>
|
||||
</v-form>
|
||||
|
||||
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn @click="reset()">{{ $t('Reset') }}</v-btn>
|
||||
<v-btn @click="searchRecipes()">{{ $t('Search') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="recipes.length > 0">
|
||||
<v-col>
|
||||
<v-card>
|
||||
<v-data-table-server
|
||||
:loading="loading"
|
||||
:items="recipes"
|
||||
:headers="tableHeaders"
|
||||
:page="searchParameters.page"
|
||||
:items-per-page="searchParameters.pageSize"
|
||||
:items-length="tableItemCount"
|
||||
@click:row="handleRowClick"
|
||||
>
|
||||
<template #item.image="{item}">
|
||||
<v-avatar :image="item.image"></v-avatar>
|
||||
</template>
|
||||
|
||||
<template #item.keywords="{item}">
|
||||
<keywords-bar :keywords="item.keywords"></keywords-bar>
|
||||
</template>
|
||||
|
||||
<template #item.action="{item}">
|
||||
<recipe-context-menu :recipe="item"></recipe-context-menu>
|
||||
</template>
|
||||
</v-data-table-server>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue'
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
import {ApiApi, ApiRecipeListRequest, RecipeOverview} from "@/openapi";
|
||||
import {useUrlSearchParams} from "@vueuse/core";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
|
||||
import ModelSelect from "@/components/inputs/ModelSelect.vue";
|
||||
import RecipeCard from "@/components/display/RecipeCard.vue";
|
||||
import {ApiApi, Recipe, RecipeOverview} from "@/openapi";
|
||||
import {VNumberInput} from 'vuetify/labs/VNumberInput'
|
||||
import {VDateInput} from 'vuetify/labs/VDateInput'
|
||||
import RecipeContextMenu from "@/components/inputs/RecipeContextMenu.vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import KeywordsBar from "@/components/display/KeywordsBar.vue";
|
||||
|
||||
const {t} = useI18n()
|
||||
const router = useRouter()
|
||||
const urlSearchParams = useUrlSearchParams('history', {})
|
||||
|
||||
export default defineComponent({
|
||||
name: "SearchPage",
|
||||
components: {ModelSelect, RecipeCard},
|
||||
data() {
|
||||
return {
|
||||
test: {
|
||||
text: String,
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const searchParameters = ref({} as ApiRecipeListRequest)
|
||||
const loading = ref(false)
|
||||
|
||||
},
|
||||
methods: {
|
||||
testApi: function () {
|
||||
const api = new ApiApi()
|
||||
api.apiMealPlanList().then(r => {
|
||||
if (r.length > 0 && r[0].id != undefined) {
|
||||
api.apiMealPlanUpdate({id: r[0].id, mealPlanRequest: r[0]})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
const tableHeaders = [
|
||||
{title: t('Image'), width: '1%', noBreak: true, key: 'image',},
|
||||
{title: t('Name'), key: 'name',},
|
||||
{title: t('Keywords'), key: 'keywords',},
|
||||
{title: t('Actions'), key: 'action', width: '1%', noBreak: true, align: 'end'},
|
||||
]
|
||||
|
||||
const tableItemCount = ref(0)
|
||||
|
||||
const recipes = ref([] as RecipeOverview[])
|
||||
|
||||
watch(() => searchParameters.value.page, () => {
|
||||
searchRecipes()
|
||||
})
|
||||
|
||||
watch(() => searchParameters.value.pageSize, () => {
|
||||
searchRecipes()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (urlSearchParams.query && typeof urlSearchParams.query === "string") {
|
||||
searchParameters.value.query = urlSearchParams.query
|
||||
}
|
||||
})
|
||||
|
||||
function searchRecipes() {
|
||||
let api = new ApiApi()
|
||||
loading.value = true
|
||||
api.apiRecipeList(searchParameters.value).then((r) => {
|
||||
recipes.value = r.results
|
||||
tableItemCount.value = r.count
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function reset() {
|
||||
searchParameters.value = {} as ApiRecipeListRequest
|
||||
recipes.value = []
|
||||
}
|
||||
|
||||
function handleRowClick(event: PointerEvent, data: any) {
|
||||
router.push({name: 'view_recipe', params: {id: recipes.value[data.index].id}})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<horizontal-recipe-scroller :skeletons="4" mode="recent"></horizontal-recipe-scroller>
|
||||
<horizontal-recipe-scroller :skeletons="4" mode="new"></horizontal-recipe-scroller>
|
||||
<horizontal-recipe-scroller :skeletons="4" mode="keyword"></horizontal-recipe-scroller>
|
||||
<horizontal-recipe-scroller :skeletons="4" mode="random"></horizontal-recipe-scroller>
|
||||
<horizontal-recipe-scroller :skeletons="2" mode="rating"></horizontal-recipe-scroller>
|
||||
<horizontal-recipe-scroller :skeletons="4" mode="keyword"></horizontal-recipe-scroller>
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
AccessToken,
|
||||
ApiApi, Automation, type AutomationTypeEnum,
|
||||
ApiApi, Automation, type AutomationTypeEnum, CustomFilter,
|
||||
Food,
|
||||
Ingredient,
|
||||
InviteLink, Keyword,
|
||||
MealPlan,
|
||||
MealType,
|
||||
Property, PropertyType,
|
||||
Recipe, ShoppingListEntry,
|
||||
Recipe, RecipeBook, RecipeBookEntry, ShoppingListEntry,
|
||||
Step,
|
||||
Supermarket,
|
||||
SupermarketCategory,
|
||||
@@ -131,6 +131,9 @@ export type EditorSupportedModels =
|
||||
| 'UserFile'
|
||||
| 'ShoppingListEntry'
|
||||
| 'User'
|
||||
| 'RecipeBook'
|
||||
| 'RecipeBookEntry'
|
||||
| 'CustomFilter'
|
||||
|
||||
// used to type methods/parameters in conjunction with configuration type
|
||||
export type EditorSupportedTypes =
|
||||
@@ -154,6 +157,9 @@ export type EditorSupportedTypes =
|
||||
| UserFile
|
||||
| ShoppingListEntry
|
||||
| User
|
||||
| RecipeBook
|
||||
| RecipeBookEntry
|
||||
| CustomFilter
|
||||
|
||||
export const TFood = {
|
||||
name: 'Food',
|
||||
@@ -293,6 +299,56 @@ export const TMealPlan = {
|
||||
} as Model
|
||||
registerModel(TMealPlan)
|
||||
|
||||
export const TRecipeBook = {
|
||||
name: 'RecipeBook',
|
||||
localizationKey: 'Recipe_Book',
|
||||
icon: 'fa-solid fa-book-open',
|
||||
|
||||
isPaginated: true,
|
||||
toStringKeys: ['name'],
|
||||
|
||||
disableListView: true,
|
||||
|
||||
tableHeaders: [
|
||||
{title: 'Name', key: 'name'},
|
||||
{title: 'Actions', key: 'action', align: 'end'},
|
||||
]
|
||||
} as Model
|
||||
registerModel(TRecipeBook)
|
||||
|
||||
export const TRecipeBookEntry = {
|
||||
name: 'RecipeBookEntry',
|
||||
localizationKey: 'Recipe_Book',
|
||||
icon: 'fa-solid fa-book-open',
|
||||
|
||||
isPaginated: true,
|
||||
toStringKeys: ['book.name', 'recipe.name'],
|
||||
|
||||
disableListView: true,
|
||||
|
||||
tableHeaders: [
|
||||
{title: 'Book', key: 'book.name'},
|
||||
{title: 'Recipe', key: 'recipe.name'},
|
||||
{title: 'Actions', key: 'action', align: 'end'},
|
||||
]
|
||||
} as Model
|
||||
registerModel(TRecipeBookEntry)
|
||||
|
||||
export const TCustomFilter = {
|
||||
name: 'CustomFilter',
|
||||
localizationKey: 'Custom Filter',
|
||||
icon: 'fa-solid fa-filter',
|
||||
|
||||
isPaginated: true,
|
||||
toStringKeys: ['name'],
|
||||
|
||||
tableHeaders: [
|
||||
{title: 'Name', key: 'name'},
|
||||
{title: 'Actions', key: 'action', align: 'end'},
|
||||
]
|
||||
} as Model
|
||||
registerModel(TCustomFilter)
|
||||
|
||||
export const TUser = {
|
||||
name: 'User',
|
||||
localizationKey: 'User',
|
||||
|
||||
@@ -5,4 +5,5 @@ export interface SearchResult {
|
||||
description?: string,
|
||||
icon?: string,
|
||||
image?: string|null,
|
||||
type: 'recipe'|'link_advanced_search'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user