Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Recipe Finder (aka Cocktail Builder) #4542

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
75a970b
re-organize recipe routes
michael-genson Nov 7, 2024
12b2167
refactored order_by method/schema
michael-genson Nov 7, 2024
1d535ff
added suggestions to recipes repo
michael-genson Nov 7, 2024
a637729
added route for suggestions
michael-genson Nov 7, 2024
627bd99
added public route
michael-genson Nov 7, 2024
5301f0d
move files around
michael-genson Nov 7, 2024
9cb73ce
dev:generate
michael-genson Nov 7, 2024
89375d4
initial tests
michael-genson Nov 8, 2024
851ec2d
more tests
michael-genson Nov 8, 2024
62d44f2
fix ranking for user foods
michael-genson Nov 8, 2024
cfa37e0
even more tests
michael-genson Nov 8, 2024
3f98fd3
added explore tests
michael-genson Nov 8, 2024
49563e5
return food/tool objects
michael-genson Nov 8, 2024
b3da652
update types
michael-genson Nov 8, 2024
6174783
require at least one user-provided food
michael-genson Nov 9, 2024
9c7cc24
added to frontend apis
michael-genson Nov 9, 2024
41e78b8
added wip finder page
michael-genson Nov 9, 2024
ac177fb
add filters and style tweaks
michael-genson Nov 10, 2024
f630af1
slow down debounce
michael-genson Nov 10, 2024
d54dccc
Merge remote-tracking branch 'upstream/mealie-next' into feat/cocktai…
michael-genson Nov 11, 2024
3d255b1
remove warning
michael-genson Nov 11, 2024
3156f53
adjust language
michael-genson Nov 11, 2024
adc2016
add JSON object to QFB
michael-genson Nov 11, 2024
34bc0c3
improvements to BaseDialog
michael-genson Nov 11, 2024
644ae67
added query filter builder as "other filters"
michael-genson Nov 11, 2024
8b8a340
fix wrong attr
michael-genson Nov 11, 2024
02bbf11
added user preferences for finder
michael-genson Nov 11, 2024
4cca33e
simplify recipe suggestion
michael-genson Nov 11, 2024
b325fab
add translations
michael-genson Nov 11, 2024
4ffe487
moved limit to settings
michael-genson Nov 11, 2024
21c539a
re-organize translations and add page title
michael-genson Nov 11, 2024
19b2b62
remove initial recipe load jitter
michael-genson Nov 11, 2024
e05e862
remove onHand filter for non-logged-in users
michael-genson Nov 11, 2024
2931fc9
update tests to handle new user-food filter
michael-genson Nov 11, 2024
d226939
added user food test
michael-genson Nov 11, 2024
6632817
prefer plural names
michael-genson Nov 11, 2024
004deb6
clean up which id lists are which
michael-genson Nov 11, 2024
f87fbc7
added missing group_id/household_id
michael-genson Nov 11, 2024
b2ed784
lint
michael-genson Nov 11, 2024
651c601
fix suggestion test
michael-genson Nov 11, 2024
e8bbd36
fix route order
michael-genson Nov 11, 2024
2e2a2a6
flakey test fix
michael-genson Nov 12, 2024
9789804
fix import issues
michael-genson Nov 12, 2024
edbd0b4
Merge branch 'mealie-next' into feat/cocktail-builder
michael-genson Nov 13, 2024
3bb792c
Merge branch 'mealie-next' into feat/cocktail-builder
michael-genson Nov 16, 2024
1fa9ca1
Merge branch 'mealie-next' into feat/cocktail-builder
michael-genson Nov 18, 2024
7952c3b
Merge branch 'mealie-next' into feat/cocktail-builder
michael-genson Nov 20, 2024
c1ff2c3
Merge branch 'mealie-next' into feat/cocktail-builder
michael-genson Nov 25, 2024
fdc6923
gave some love to mobile users
michael-genson Nov 25, 2024
e3d6d35
tweaked spacing
michael-genson Nov 25, 2024
b206ddb
bumped default missing foods/tools to 20
michael-genson Nov 25, 2024
367a14e
added checkboxes to missing foods/tools
michael-genson Nov 25, 2024
885b60a
lint
michael-genson Nov 25, 2024
8ca7de6
more lint
michael-genson Nov 25, 2024
695a831
halved debounce
michael-genson Nov 29, 2024
4103294
fixed color on light mode
michael-genson Nov 29, 2024
4fa5477
Merge branch 'mealie-next' into feat/cocktail-builder
michael-genson Nov 29, 2024
abc2582
Merge branch 'mealie-next' into feat/cocktail-builder
michael-genson Dec 2, 2024
b54417c
Merge branch 'mealie-next' into feat/cocktail-builder
michael-genson Dec 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions frontend/assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,11 @@
.v-card__title {
word-break: normal !important;
}

.text-hide-overflow {
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
29 changes: 28 additions & 1 deletion frontend/components/Domain/QueryFilterBuilder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@
</v-container>
</v-card-text>
<v-card-actions>
<v-container fluid class="d-flex justify-end pa-0">
<v-container fluid class="d-flex justify-end pa-0 mx-2">
<v-checkbox
v-model="showAdvanced"
hide-details
Expand Down Expand Up @@ -431,6 +431,7 @@ export default defineComponent({
state.qfValid = !!qf;

context.emit("input", qf || undefined);
context.emit("inputJSON", qf ? buildQueryFilterJSON() : undefined);
},
{
deep: true
Expand Down Expand Up @@ -543,6 +544,32 @@ export default defineComponent({
initFieldsError(`Error initializing fields: ${(error || "").toString()}`);
}

function buildQueryFilterJSON(): QueryFilterJSON {
const parts = fields.value.map((field) => {
const part: QueryFilterJSONPart = {
attributeName: field.name,
leftParenthesis: field.leftParenthesis,
rightParenthesis: field.rightParenthesis,
logicalOperator: field.logicalOperator?.value,
relationalOperator: field.relationalOperatorValue?.value,
};

if (field.fieldOptions?.length || isOrganizerType(field.type)) {
part.value = field.values.map((value) => value.toString());
} else if (field.type === "boolean") {
part.value = field.value ? "true" : "false";
} else {
part.value = (field.value || "").toString();
}

return part;
});

const qfJSON = { parts } as QueryFilterJSON;
console.debug(`Built query filter JSON: ${JSON.stringify(qfJSON)}`);
return qfJSON;
}


const attrs = computed(() => {
const baseColMaxWidth = 55;
Expand Down
118 changes: 118 additions & 0 deletions frontend/components/Domain/Recipe/RecipeSuggestion.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<template>
<v-container class="elevation-3">
<v-row no-gutters>
<v-col cols="12">
<RecipeCardMobile
:name="recipe.name"
:description="recipe.description"
:slug="recipe.slug"
:rating="recipe.rating"
:image="recipe.image"
:recipe-id="recipe.id"
/>
</v-col>
<div v-for="(organizer, idx) in missingOrganizers" :key="idx">
<v-col
v-if="organizer.show"
cols="12"
>
<div class="d-flex flex-row flex-wrap align-center pt-2">
<v-icon class="ma-0 pa-0">{{ organizer.icon }}</v-icon>
<v-card-text class="mr-0 my-0 pl-1 py-0" style="width: min-content;">
{{ $tc("recipe-finder.missing") }}:
</v-card-text>
<v-chip
v-for="item in organizer.items"
:key="item.item.id"
label
color="secondary custom-transparent"
class="mr-2 my-1"
>
<v-checkbox dark :ripple="false" @click="handleCheckbox(item)">
<template #label>
{{ organizer.getLabel(item.item) }}
</template>
</v-checkbox>
</v-chip>
</div>
</v-col>
</div>
</v-row>
</v-container>
</template>

<script lang="ts">
import { computed, defineComponent, reactive, useContext } from "@nuxtjs/composition-api";
import RecipeCardMobile from "./RecipeCardMobile.vue";
import { IngredientFood, RecipeSummary, RecipeTool } from "~/lib/api/types/recipe";

interface Organizer {
type: "food" | "tool";
item: IngredientFood | RecipeTool;
selected: boolean;
}

export default defineComponent({
components: { RecipeCardMobile },
props: {
recipe: {
type: Object as () => RecipeSummary,
required: true,
},
missingFoods: {
type: Array as () => IngredientFood[] | null,
default: null,
},
missingTools: {
type: Array as () => RecipeTool[] | null,
default: null,
},
disableCheckbox: {
type: Boolean,
default: false,
},
},
setup(props, context) {
const { $globals } = useContext();
const missingOrganizers = computed(() => [
{
type: "food",
show: props.missingFoods?.length,
icon: $globals.icons.foods,
items: props.missingFoods ? props.missingFoods.map((food) => {
return reactive({type: "food", item: food, selected: false} as Organizer);
}) : [],
getLabel: (item: IngredientFood) => item.pluralName || item.name,
},
{
type: "tool",
show: props.missingTools?.length,
icon: $globals.icons.tools,
items: props.missingTools ? props.missingTools.map((tool) => {
return reactive({type: "tool", item: tool, selected: false} as Organizer);
}) : [],
getLabel: (item: RecipeTool) => item.name,
}
])

function handleCheckbox(organizer: Organizer) {
if (props.disableCheckbox) {
return;
}

organizer.selected = !organizer.selected;
if (organizer.selected) {
context.emit(`add-${organizer.type}`, organizer.item);
}
else {
context.emit(`remove-${organizer.type}`, organizer.item);
}
}

return {
missingOrganizers,
handleCheckbox,
};
}
});
</script>
8 changes: 7 additions & 1 deletion frontend/components/Layout/DefaultLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,13 @@ export default defineComponent({
icon: $globals.icons.silverwareForkKnife,
to: `/g/${groupSlug.value}`,
title: i18n.tc("general.recipes"),
restricted: true,
restricted: false,
},
{
icon: $globals.icons.search,
to: `/g/${groupSlug.value}/recipes/finder`,
title: i18n.tc("recipe-finder.recipe-finder"),
restricted: false,
},
{
icon: $globals.icons.calendarMultiselect,
Expand Down
10 changes: 8 additions & 2 deletions frontend/components/global/BaseDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@
</v-btn>
<v-spacer></v-spacer>

<slot name="custom-card-action"></slot>
<BaseButton v-if="$listeners.delete" delete secondary @click="deleteEvent" />
<BaseButton
v-if="$listeners.confirm"
:color="color"
type="submit"
:disabled="submitDisabled"
@click="
$emit('confirm');
dialog = false;
Expand All @@ -60,8 +62,12 @@
</template>
{{ $t("general.confirm") }}
</BaseButton>
<slot name="custom-card-action"></slot>
<BaseButton v-if="$listeners.submit" type="submit" :disabled="submitDisabled" @click="submitEvent">
<BaseButton
v-if="$listeners.submit"
type="submit"
:disabled="submitDisabled"
@click="submitEvent"
>
{{ submitText }}
<template v-if="submitIcon" #icon>
{{ submitIcon }}
Expand Down
33 changes: 33 additions & 0 deletions frontend/composables/use-users/preferences.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Ref, useContext } from "@nuxtjs/composition-api";
import { useLocalStorage, useSessionStorage } from "@vueuse/core";
import { RegisteredParser, TimelineEventType } from "~/lib/api/types/recipe";
import { QueryFilterJSON } from "~/lib/api/types/response";

export interface UserPrintPreferences {
imagePosition: string;
Expand Down Expand Up @@ -49,6 +50,17 @@ export interface UserCookbooksPreferences {
hideOtherHouseholds: boolean;
}

export interface UserRecipeFinderPreferences {
foodIds: string[];
toolIds: string[];
queryFilter: string;
queryFilterJSON: QueryFilterJSON;
maxMissingFoods: number;
maxMissingTools: number;
includeFoodsOnHand: boolean;
includeToolsOnHand: boolean;
}

export function useUserMealPlanPreferences(): Ref<UserMealPlanPreferences> {
const fromStorage = useLocalStorage(
"meal-planner-preferences",
Expand Down Expand Up @@ -171,3 +183,24 @@ export function useCookbookPreferences(): Ref<UserCookbooksPreferences> {

return fromStorage;
}

export function useRecipeFinderPreferences(): Ref<UserRecipeFinderPreferences> {
const fromStorage = useLocalStorage(
"recipe-finder-preferences",
{
foodIds: [],
toolIds: [],
queryFilter: "",
queryFilterJSON: { parts: [] } as QueryFilterJSON,
maxMissingFoods: 20,
maxMissingTools: 20,
includeFoodsOnHand: true,
includeToolsOnHand: true,
},
{ mergeDefaults: true }
// we cast to a Ref because by default it will return an optional type ref
// but since we pass defaults we know all properties are set.
) as unknown as Ref<UserRecipeFinderPreferences>;

return fromStorage;
}
17 changes: 17 additions & 0 deletions frontend/lang/messages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,23 @@
"reset-servings-count": "Reset Servings Count",
"not-linked-ingredients": "Additional Ingredients"
},
"recipe-finder": {
"recipe-finder": "Recipe Finder",
"recipe-finder-description": "Search for recipes based on ingredients you have on hand. You can also filter by tools you have available, and set a maximum number of missing ingredients or tools.",
"selected-ingredients": "Selected Ingredients",
"no-ingredients-selected": "No ingredients selected",
"missing": "Missing",
"no-recipes-found": "No recipes found",
"no-recipes-found-description": "Try adding more ingredients to your search or adjusting your filters",
"include-ingredients-on-hand": "Include Ingredients On Hand",
"include-tools-on-hand": "Include Tools On Hand",
"max-missing-ingredients": "Max Missing Ingredients",
"max-missing-tools": "Max Missing Tools",
"selected-tools": "Selected Tools",
"other-filters": "Other Filters",
"ready-to-make": "Ready to Make",
"almost-ready-to-make": "Almost Ready to Make"
},
"search": {
"advanced-search": "Advanced Search",
"and": "and",
Expand Down
8 changes: 7 additions & 1 deletion frontend/lib/api/public/explore/recipes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
import { route } from "../../base";
import { Recipe } from "~/lib/api/types/recipe";
import { Recipe, RecipeSuggestionQuery, RecipeSuggestionResponse } from "~/lib/api/types/recipe";
import { ApiRequestInstance, PaginationData } from "~/lib/api/types/non-generated";
import { RecipeSearchQuery } from "../../user/recipes/recipe";

Expand All @@ -23,4 +23,10 @@ export class PublicRecipeApi extends BaseCRUDAPIReadOnly<Recipe> {
async search(rsq: RecipeSearchQuery) {
return await this.requests.get<PaginationData<Recipe>>(route(routes.recipesGroupSlug(this.groupSlug), rsq));
}

async getSuggestions(q: RecipeSuggestionQuery, foods: string[] | null = null, tools: string[]| null = null) {
return await this.requests.get<RecipeSuggestionResponse>(
route(`${this.baseRoute}/suggestions`, { ...q, foods, tools })
);
}
}
29 changes: 29 additions & 0 deletions frontend/lib/api/types/recipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

export type ExportTypes = "json";
export type RegisteredParser = "nlp" | "brute" | "openai";
export type OrderByNullPosition = "first" | "last";
export type OrderDirection = "asc" | "desc";
export type TimelineEventType = "system" | "info" | "comment";
export type TimelineEventImage = "has image" | "does not have image";

Expand Down Expand Up @@ -380,6 +382,26 @@ export interface RecipeShareTokenSummary {
export interface RecipeSlug {
slug: string;
}
export interface RecipeSuggestionQuery {
orderBy?: string | null;
orderByNullPosition?: OrderByNullPosition | null;
orderDirection?: OrderDirection;
queryFilter?: string | null;
paginationSeed?: string | null;
limit?: number;
maxMissingFoods?: number;
maxMissingTools?: number;
includeFoodsOnHand?: boolean;
includeToolsOnHand?: boolean;
}
export interface RecipeSuggestionResponse {
items: RecipeSuggestionResponseItem[];
}
export interface RecipeSuggestionResponseItem {
recipe: RecipeSummary;
missingFoods: IngredientFood[];
missingTools: RecipeTool[];
}
export interface RecipeTagResponse {
name: string;
id: string;
Expand Down Expand Up @@ -519,3 +541,10 @@ export interface UnitFoodBase {
export interface UpdateImageResponse {
image: string;
}
export interface RequestQuery {
orderBy?: string | null;
orderByNullPosition?: OrderByNullPosition | null;
orderDirection?: OrderDirection;
queryFilter?: string | null;
paginationSeed?: string | null;
}
11 changes: 9 additions & 2 deletions frontend/lib/api/types/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ export interface FileTokenResponse {
fileToken: string;
}
export interface PaginationQuery {
page?: number;
perPage?: number;
orderBy?: string | null;
orderByNullPosition?: OrderByNullPosition | null;
orderDirection?: OrderDirection;
queryFilter?: string | null;
paginationSeed?: string | null;
page?: number;
perPage?: number;
}
export interface QueryFilterJSON {
parts?: QueryFilterJSONPart[];
Expand All @@ -47,6 +47,13 @@ export interface RecipeSearchQuery {
requireAllFoods?: boolean;
search?: string | null;
}
export interface RequestQuery {
orderBy?: string | null;
orderByNullPosition?: OrderByNullPosition | null;
orderDirection?: OrderDirection;
queryFilter?: string | null;
paginationSeed?: string | null;
}
export interface SuccessResponse {
message: string;
error?: boolean;
Expand Down
Loading
Loading