mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
Feature/recipe viewer (#244)
* fix dialog placement * markdown support in ingredients * fix line render issue * fix tag rendering bug * change ingredients to text area * no slug error * add tag pages * remove console.logs Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
30510202df
commit
049c269f6f
@ -21,7 +21,6 @@ export const metaAPI = {
|
|||||||
|
|
||||||
async getIsDemo() {
|
async getIsDemo() {
|
||||||
let response = await apiReq.get(debugURLs.demo);
|
let response = await apiReq.get(debugURLs.demo);
|
||||||
console.log(response);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -273,7 +273,6 @@ export default {
|
|||||||
await this.initialize();
|
await this.initialize();
|
||||||
},
|
},
|
||||||
resetPassword() {
|
resetPassword() {
|
||||||
console.log(this.activeId);
|
|
||||||
api.users.resetPassword(this.editedItem.id);
|
api.users.resetPassword(this.editedItem.id);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -125,7 +125,6 @@ export default {
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
groupSettings() {
|
groupSettings() {
|
||||||
console.log(this.$store.getters.getCurrentGroup);
|
|
||||||
return this.$store.getters.getCurrentGroup;
|
return this.$store.getters.getCurrentGroup;
|
||||||
},
|
},
|
||||||
actualStartDate() {
|
actualStartDate() {
|
||||||
|
@ -26,21 +26,16 @@
|
|||||||
</v-card-title>
|
</v-card-title>
|
||||||
|
|
||||||
<v-card-actions class="">
|
<v-card-actions class="">
|
||||||
<v-row dense align="center">
|
<v-rating
|
||||||
<v-col>
|
class="mr-2"
|
||||||
<v-rating
|
color="secondary"
|
||||||
class="mr-2"
|
background-color="secondary lighten-3"
|
||||||
color="secondary"
|
dense
|
||||||
background-color="secondary lighten-3"
|
length="5"
|
||||||
dense
|
size="15"
|
||||||
length="5"
|
:value="rating"
|
||||||
size="15"
|
></v-rating>
|
||||||
:value="rating"
|
<v-spacer></v-spacer>
|
||||||
></v-rating>
|
|
||||||
</v-col>
|
|
||||||
<v-col></v-col>
|
|
||||||
<v-col align="end"> </v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-hover>
|
</v-hover>
|
||||||
@ -55,6 +50,7 @@ export default {
|
|||||||
description: String,
|
description: String,
|
||||||
rating: Number,
|
rating: Number,
|
||||||
image: String,
|
image: String,
|
||||||
|
|
||||||
route: {
|
route: {
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
@ -83,14 +83,16 @@
|
|||||||
:key="generateKey('ingredient', index)"
|
:key="generateKey('ingredient', index)"
|
||||||
>
|
>
|
||||||
<v-row align="center">
|
<v-row align="center">
|
||||||
<v-text-field
|
<v-textarea
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
:label="$t('recipe.ingredient')"
|
:label="$t('recipe.ingredient')"
|
||||||
v-model="value.recipeIngredient[index]"
|
v-model="value.recipeIngredient[index]"
|
||||||
append-outer-icon="mdi-menu"
|
append-outer-icon="mdi-menu"
|
||||||
mdi-move-resize
|
mdi-move-resize
|
||||||
|
auto-grow
|
||||||
solo
|
solo
|
||||||
dense
|
dense
|
||||||
|
rows="2"
|
||||||
>
|
>
|
||||||
<v-icon
|
<v-icon
|
||||||
class="mr-n1"
|
class="mr-n1"
|
||||||
@ -100,7 +102,7 @@
|
|||||||
>
|
>
|
||||||
mdi-delete
|
mdi-delete
|
||||||
</v-icon>
|
</v-icon>
|
||||||
</v-text-field>
|
</v-textarea>
|
||||||
</v-row>
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
@ -235,6 +237,7 @@
|
|||||||
dense
|
dense
|
||||||
v-model="value.recipeInstructions[index]['text']"
|
v-model="value.recipeInstructions[index]['text']"
|
||||||
:key="generateKey('instructions', index)"
|
:key="generateKey('instructions', index)"
|
||||||
|
rows="4"
|
||||||
>
|
>
|
||||||
</v-textarea>
|
</v-textarea>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
@ -1,27 +1,52 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
|
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
|
||||||
<div
|
<v-list-item
|
||||||
v-for="(ingredient, index) in ingredients"
|
dense
|
||||||
|
v-for="(ingredient, index) in displayIngredients"
|
||||||
:key="generateKey('ingredient', index)"
|
:key="generateKey('ingredient', index)"
|
||||||
|
@click="ingredient.checked = !ingredient.checked"
|
||||||
>
|
>
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
hide-details
|
hide-details
|
||||||
class="ingredients"
|
v-model="ingredient.checked"
|
||||||
:label="ingredient"
|
class=" pt-0 ingredients my-auto py-auto"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
>
|
>
|
||||||
</v-checkbox>
|
</v-checkbox>
|
||||||
</div>
|
|
||||||
|
<v-list-item-content>
|
||||||
|
<vue-markdown
|
||||||
|
class="my-auto text-subtitle-1 mb-0"
|
||||||
|
:source="ingredient.text"
|
||||||
|
>
|
||||||
|
</vue-markdown>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import VueMarkdown from "@adapttive/vue-markdown";
|
||||||
import utils from "@/utils";
|
import utils from "@/utils";
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
VueMarkdown,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
ingredients: Array,
|
ingredients: Array,
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
displayIngredients: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.displayIngredients = this.ingredients.map(x => ({
|
||||||
|
text: x,
|
||||||
|
checked: false,
|
||||||
|
}));
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
generateKey(item, index) {
|
generateKey(item, index) {
|
||||||
return utils.generateUniqueKey(item, index);
|
return utils.generateUniqueKey(item, index);
|
||||||
@ -31,4 +56,11 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
p {
|
||||||
|
margin-bottom: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-card-text {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@ -2,12 +2,12 @@
|
|||||||
<div v-if="items && items.length > 0">
|
<div v-if="items && items.length > 0">
|
||||||
<h2 class="mt-4">{{ title }}</h2>
|
<h2 class="mt-4">{{ title }}</h2>
|
||||||
<v-chip
|
<v-chip
|
||||||
:to="`/recipes/${getSlug(category)}`"
|
|
||||||
label
|
label
|
||||||
class="ma-1"
|
class="ma-1"
|
||||||
color="accent"
|
color="accent"
|
||||||
dark
|
dark
|
||||||
v-for="category in items"
|
v-for="category in items"
|
||||||
|
:to="`/recipes/${urlParam}/${getSlug(category)}`"
|
||||||
:key="category"
|
:key="category"
|
||||||
>
|
>
|
||||||
{{ category }}
|
{{ category }}
|
||||||
@ -20,7 +20,7 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
items: Array,
|
items: Array,
|
||||||
title: String,
|
title: String,
|
||||||
category: {
|
isCategory: {
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -28,11 +28,23 @@ export default {
|
|||||||
allCategories() {
|
allCategories() {
|
||||||
return this.$store.getters.getAllCategories;
|
return this.$store.getters.getAllCategories;
|
||||||
},
|
},
|
||||||
|
allTags() {
|
||||||
|
return this.$store.getters.getAllTags;
|
||||||
|
},
|
||||||
|
urlParam() {
|
||||||
|
return this.isCategory ? 'category' : 'tag'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getSlug(name) {
|
getSlug(name) {
|
||||||
if (this.category) {
|
if (!name) return;
|
||||||
return this.allCategories.filter(x => x.name == name)[0].slug;
|
|
||||||
|
if (this.isCategory) {
|
||||||
|
const matches = this.allCategories.filter(x => x.name == name);
|
||||||
|
if (matches.length > 0) return matches[0].slug;
|
||||||
|
} else {
|
||||||
|
const matches = this.allTags.filter(x => x.name == name);
|
||||||
|
if (matches.length > 0) return matches[0].slug;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -34,7 +34,11 @@
|
|||||||
<Ingredients :ingredients="ingredients" />
|
<Ingredients :ingredients="ingredients" />
|
||||||
<div v-if="medium">
|
<div v-if="medium">
|
||||||
<RecipeChips :title="$t('recipe.categories')" :items="categories" />
|
<RecipeChips :title="$t('recipe.categories')" :items="categories" />
|
||||||
<RecipeChips :title="$t('recipe.tags')" :items="tags" />
|
<RecipeChips
|
||||||
|
:title="$t('recipe.tags')"
|
||||||
|
:items="tags"
|
||||||
|
:isCategory="false"
|
||||||
|
/>
|
||||||
<Notes :notes="notes" />
|
<Notes :notes="notes" />
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
@ -154,7 +154,6 @@ export default {
|
|||||||
return utils.getImageURL(image);
|
return utils.getImageURL(image);
|
||||||
},
|
},
|
||||||
selected(slug, name) {
|
selected(slug, name) {
|
||||||
console.log("Selected", slug, name);
|
|
||||||
this.$emit("selected", slug, name);
|
this.$emit("selected", slug, name);
|
||||||
},
|
},
|
||||||
async onFocus() {
|
async onFocus() {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="text-center">
|
<div class="text-center ">
|
||||||
<v-dialog v-model="dialog" width="600px" height="0">
|
<v-dialog v-model="dialog" class="search-dialog" width="600px" height="0">
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-app-bar dark color="primary">
|
<v-app-bar dark color="primary">
|
||||||
<v-toolbar-title class="headline">Search a Recipe</v-toolbar-title>
|
<v-toolbar-title class="headline">Search a Recipe</v-toolbar-title>
|
||||||
@ -49,8 +49,8 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scope>
|
||||||
.v-dialog__content {
|
.search-dialog {
|
||||||
margin-top: 10%;
|
margin-top: 10%;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -36,7 +36,6 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
siteSettings() {
|
siteSettings() {
|
||||||
console.log(this.$store.getters.getSiteSettings);
|
|
||||||
return this.$store.getters.getSiteSettings;
|
return this.$store.getters.getSiteSettings;
|
||||||
},
|
},
|
||||||
recentRecipes() {
|
recentRecipes() {
|
||||||
@ -54,7 +53,6 @@ export default {
|
|||||||
this.siteSettings.categories.forEach(async element => {
|
this.siteSettings.categories.forEach(async element => {
|
||||||
let recipes = await this.getRecipeByCategory(element.slug);
|
let recipes = await this.getRecipeByCategory(element.slug);
|
||||||
if (recipes.recipes.length < 0) recipes.recipes = [];
|
if (recipes.recipes.length < 0) recipes.recipes = [];
|
||||||
console.log(recipes);
|
|
||||||
this.recipeByCategory.push(recipes);
|
this.recipeByCategory.push(recipes);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
60
frontend/src/pages/Recipes/TagPage.vue
Normal file
60
frontend/src/pages/Recipes/TagPage.vue
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<CategorySidebar />
|
||||||
|
<CardSection
|
||||||
|
:sortable="true"
|
||||||
|
:title="title"
|
||||||
|
:recipes="recipes"
|
||||||
|
:card-limit="9999"
|
||||||
|
@sort="sortAZ"
|
||||||
|
@sort-recent="sortRecent"
|
||||||
|
/>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { api } from "@/api";
|
||||||
|
import CardSection from "@/components/UI/CardSection";
|
||||||
|
import CategorySidebar from "@/components/UI/CategorySidebar";
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CardSection,
|
||||||
|
CategorySidebar,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
title: "",
|
||||||
|
recipes: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
currentTag() {
|
||||||
|
return this.$route.params.tag;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
async currentCategory() {
|
||||||
|
this.getRecipes();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getRecipes();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getRecipes() {
|
||||||
|
let data = await api.tags.getRecipesInTag(this.currentTag);
|
||||||
|
this.title = data.name;
|
||||||
|
this.recipes = data.recipes;
|
||||||
|
},
|
||||||
|
sortAZ() {
|
||||||
|
this.recipes.sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||||
|
},
|
||||||
|
sortRecent() {
|
||||||
|
this.recipes.sort((a, b) => (a.dateAdded > b.dateAdded ? -1 : 1));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
@ -6,6 +6,7 @@ import NewRecipe from "@/pages/Recipe/NewRecipe";
|
|||||||
import CustomPage from "@/pages/Recipes/CustomPage";
|
import CustomPage from "@/pages/Recipes/CustomPage";
|
||||||
import AllRecipes from "@/pages/Recipes/AllRecipes";
|
import AllRecipes from "@/pages/Recipes/AllRecipes";
|
||||||
import CategoryPage from "@/pages/Recipes/CategoryPage";
|
import CategoryPage from "@/pages/Recipes/CategoryPage";
|
||||||
|
import TagPage from "@/pages/Recipes/TagPage";
|
||||||
import Planner from "@/pages/MealPlan/Planner";
|
import Planner from "@/pages/MealPlan/Planner";
|
||||||
import Debug from "@/pages/Debug";
|
import Debug from "@/pages/Debug";
|
||||||
import LoginPage from "@/pages/LoginPage";
|
import LoginPage from "@/pages/LoginPage";
|
||||||
@ -33,7 +34,8 @@ export const routes = [
|
|||||||
{ path: "/search", component: SearchPage },
|
{ path: "/search", component: SearchPage },
|
||||||
{ path: "/recipes/all", component: AllRecipes },
|
{ path: "/recipes/all", component: AllRecipes },
|
||||||
{ path: "/pages/:customPage", component: CustomPage },
|
{ path: "/pages/:customPage", component: CustomPage },
|
||||||
{ path: "/recipes/:category", component: CategoryPage },
|
{ path: "/recipes/tag/:tag", component: TagPage },
|
||||||
|
{ path: "/recipes/category/:category", component: CategoryPage },
|
||||||
{ path: "/recipe/:recipe", component: ViewRecipe },
|
{ path: "/recipe/:recipe", component: ViewRecipe },
|
||||||
{ path: "/new/", component: NewRecipe },
|
{ path: "/new/", component: NewRecipe },
|
||||||
{ path: "/meal-plan/planner", component: Planner },
|
{ path: "/meal-plan/planner", component: Planner },
|
||||||
|
@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends
|
|||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.routes.deps import get_current_user
|
from mealie.routes.deps import get_current_user
|
||||||
|
from mealie.schema.category import RecipeTagResponse
|
||||||
from mealie.schema.snackbar import SnackResponse
|
from mealie.schema.snackbar import SnackResponse
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
@ -19,7 +20,7 @@ async def get_all_recipe_tags(session: Session = Depends(generate_session)):
|
|||||||
return db.tags.get_all_limit_columns(session, ["slug", "name"])
|
return db.tags.get_all_limit_columns(session, ["slug", "name"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{tag}")
|
@router.get("/{tag}", response_model=RecipeTagResponse)
|
||||||
def get_all_recipes_by_tag(tag: str, session: Session = Depends(generate_session)):
|
def get_all_recipes_by_tag(tag: str, session: Session = Depends(generate_session)):
|
||||||
""" Returns a list of recipes associated with the provided tag. """
|
""" Returns a list of recipes associated with the provided tag. """
|
||||||
return db.tags.get(session, tag)
|
return db.tags.get(session, tag)
|
||||||
|
@ -27,5 +27,5 @@ class TagBase(CategoryBase):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class RecipeTagResponse(TagBase):
|
class RecipeTagResponse(RecipeCategoryResponse):
|
||||||
pass
|
pass
|
||||||
|
Loading…
x
Reference in New Issue
Block a user