mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-30 17:53:31 -04:00 
			
		
		
		
	* added basic recipe print settings added print settings dialog refactored print view to live inside print container * removed unwanted padding * changed preferences layout
		
			
				
	
	
		
			294 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			294 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | |
|   <div :class="dense ? 'wrapper' : 'wrapper pa-3'">
 | |
|     <section>
 | |
|       <v-container class="ma-0 pa-0">
 | |
|         <v-row>
 | |
|           <v-col
 | |
|             v-if="preferences.imagePosition && preferences.imagePosition != ImagePosition.hidden"
 | |
|             :order="preferences.imagePosition == ImagePosition.left ? -1 : 1"
 | |
|             cols="4"
 | |
|             align-self="center"
 | |
|           >
 | |
|             <img :key="imageKey" :src="recipeImageUrl" style="min-height: 50; max-width: 100%;" />
 | |
|           </v-col>
 | |
|           <v-col order=0>
 | |
|             <v-card-title class="headline pl-0">
 | |
|               <v-icon left color="primary">
 | |
|                 {{ $globals.icons.primary }}
 | |
|               </v-icon>
 | |
|               {{ recipe.name }}
 | |
|             </v-card-title>
 | |
|             <RecipeTimeCard :prep-time="recipe.prepTime" :total-time="recipe.totalTime" :perform-time="recipe.performTime" color="white" />
 | |
|             <v-card-text v-if="preferences.showDescription" class="px-0">
 | |
|               <SafeMarkdown :source="recipe.description" />
 | |
|             </v-card-text>
 | |
|           </v-col>
 | |
|         </v-row>
 | |
|       </v-container>
 | |
|     </section>
 | |
| 
 | |
|     <!-- Ingredients -->
 | |
|     <section>
 | |
|       <v-card-title class="headline pl-0"> {{ $t("recipe.ingredients") }} </v-card-title>
 | |
|       <div
 | |
|         v-for="(ingredientSection, sectionIndex) in ingredientSections"
 | |
|         :key="`ingredient-section-${sectionIndex}`"
 | |
|         class="print-section"
 | |
|       >
 | |
|         <h4 v-if="ingredientSection.ingredients[0].title" class="ingredient-title mt-2">
 | |
|           {{ ingredientSection.ingredients[0].title }}
 | |
|         </h4>
 | |
|         <div
 | |
|           class="ingredient-grid"
 | |
|           :style="{ gridTemplateRows: `repeat(${Math.ceil(ingredientSection.ingredients.length / 2)}, min-content)` }"
 | |
|         >
 | |
|           <template v-for="(ingredient, ingredientIndex) in ingredientSection.ingredients">
 | |
|             <!-- eslint-disable-next-line vue/no-v-html -->
 | |
|             <p :key="`ingredient-${ingredientIndex}`" class="ingredient-body" v-html="parseText(ingredient)" />
 | |
|           </template>
 | |
|         </div>
 | |
|       </div>
 | |
|     </section>
 | |
| 
 | |
|     <!-- Instructions -->
 | |
|     <section>
 | |
|       <div
 | |
|         v-for="(instructionSection, sectionIndex) in instructionSections"
 | |
|         :key="`instruction-section-${sectionIndex}`"
 | |
|         :class="{ 'print-section': instructionSection.sectionName }"
 | |
|       >
 | |
|         <v-card-title v-if="!sectionIndex" class="headline pl-0">{{ $t("recipe.instructions") }}</v-card-title>
 | |
|         <div v-for="(step, stepIndex) in instructionSection.instructions" :key="`instruction-${stepIndex}`">
 | |
|           <div class="print-section">
 | |
|             <h4 v-if="step.title" :key="`instruction-title-${stepIndex}`" class="instruction-title mb-2">
 | |
|               {{ step.title }}
 | |
|             </h4>
 | |
|             <h5>{{ $t("recipe.step-index", { step: stepIndex + instructionSection.stepOffset + 1 }) }}</h5>
 | |
|             <SafeMarkdown :source="step.text" class="recipe-step-body" />
 | |
|           </div>
 | |
|         </div>
 | |
|       </div>
 | |
|     </section>
 | |
| 
 | |
|     <!-- Notes -->
 | |
|     <div v-if="preferences.showNotes">
 | |
|       <v-divider v-if="hasNotes" class="grey my-4"></v-divider>
 | |
| 
 | |
|       <section>
 | |
|         <div v-for="(note, index) in recipe.notes" :key="index + 'note'">
 | |
|           <div class="print-section">
 | |
|             <h4>{{ note.title }}</h4>
 | |
|             <SafeMarkdown :source="note.text" class="note-body" />
 | |
|           </div>
 | |
|         </div>
 | |
|       </section>
 | |
|     </div>
 | |
|   </div>
 | |
| </template>
 | |
| 
 | |
| <script lang="ts">
 | |
| import { defineComponent, computed } from "@nuxtjs/composition-api";
 | |
| import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
 | |
| import { useStaticRoutes } from "~/composables/api";
 | |
| import { Recipe, RecipeIngredient, RecipeStep } from "~/lib/api/types/recipe";
 | |
| import { NoUndefinedField } from "~/lib/api/types/non-generated";
 | |
| import { ImagePosition, useUserPrintPreferences } from "~/composables/use-users/preferences";
 | |
| import { parseIngredientText } from "~/composables/recipes";
 | |
| import { usePageState } from "~/composables/recipe-page/shared-state";
 | |
| 
 | |
| type IngredientSection = {
 | |
|   sectionName: string;
 | |
|   ingredients: RecipeIngredient[];
 | |
| };
 | |
| 
 | |
| type InstructionSection = {
 | |
|   sectionName: string;
 | |
|   stepOffset: number;
 | |
|   instructions: RecipeStep[];
 | |
| };
 | |
| 
 | |
| export default defineComponent({
 | |
|   components: {
 | |
|     RecipeTimeCard,
 | |
|   },
 | |
|   props: {
 | |
|     recipe: {
 | |
|       type: Object as () => NoUndefinedField<Recipe>,
 | |
|       required: true,
 | |
|     },
 | |
|     scale: {
 | |
|       type: Number,
 | |
|       default: 1,
 | |
|     },
 | |
|     dense: {
 | |
|       type: Boolean,
 | |
|       default: false
 | |
|     }
 | |
|   },
 | |
|   setup(props) {
 | |
|     const preferences = useUserPrintPreferences();
 | |
|     const { recipeImage } = useStaticRoutes();
 | |
|     const { imageKey } = usePageState(props.recipe.slug);
 | |
| 
 | |
|     const recipeImageUrl = computed(() => {
 | |
|       return recipeImage(props.recipe.id, props.recipe.image, imageKey.value);
 | |
|     });
 | |
| 
 | |
|     // Group ingredients by section so we can style them independently
 | |
|     const ingredientSections = computed<IngredientSection[]>(() => {
 | |
|       if (!props.recipe.recipeIngredient) {
 | |
|         return [];
 | |
|       }
 | |
| 
 | |
|       return props.recipe.recipeIngredient.reduce((sections, ingredient) => {
 | |
|         // if title append new section to the end of the array
 | |
|         if (ingredient.title) {
 | |
|           sections.push({
 | |
|             sectionName: ingredient.title,
 | |
|             ingredients: [ingredient],
 | |
|           });
 | |
| 
 | |
|           return sections;
 | |
|         }
 | |
| 
 | |
|         // append new section if first
 | |
|         if (sections.length === 0) {
 | |
|           sections.push({
 | |
|             sectionName: "",
 | |
|             ingredients: [ingredient],
 | |
|           });
 | |
| 
 | |
|           return sections;
 | |
|         }
 | |
| 
 | |
|         // otherwise add ingredient to last section in the array
 | |
|         sections[sections.length - 1].ingredients.push(ingredient);
 | |
|         return sections;
 | |
|       }, [] as IngredientSection[]);
 | |
|     });
 | |
| 
 | |
|     // Group instructions by section so we can style them independently
 | |
|     const instructionSections = computed<InstructionSection[]>(() => {
 | |
|       if (!props.recipe.recipeInstructions) {
 | |
|         return [];
 | |
|       }
 | |
| 
 | |
|       return props.recipe.recipeInstructions.reduce((sections, step) => {
 | |
|         const offset = (() => {
 | |
|           if (sections.length === 0) {
 | |
|             return 0;
 | |
|           }
 | |
| 
 | |
|           const lastOffset = sections[sections.length - 1].stepOffset;
 | |
|           const lastNumSteps = sections[sections.length - 1].instructions.length;
 | |
|           return lastOffset + lastNumSteps;
 | |
|         })();
 | |
| 
 | |
|         // if title append new section to the end of the array
 | |
|         if (step.title) {
 | |
|           sections.push({
 | |
|             sectionName: step.title,
 | |
|             stepOffset: offset,
 | |
|             instructions: [step],
 | |
|           });
 | |
| 
 | |
|           return sections;
 | |
|         }
 | |
| 
 | |
|         // append if first element
 | |
|         if (sections.length === 0) {
 | |
|           sections.push({
 | |
|             sectionName: "",
 | |
|             stepOffset: offset,
 | |
|             instructions: [step],
 | |
|           });
 | |
| 
 | |
|           return sections;
 | |
|         }
 | |
| 
 | |
|         // otherwise add step to last section in the array
 | |
|         sections[sections.length - 1].instructions.push(step);
 | |
|         return sections;
 | |
|       }, [] as InstructionSection[]);
 | |
|     });
 | |
| 
 | |
|     const hasNotes = computed(() => {
 | |
|       return props.recipe.notes && props.recipe.notes.length > 0;
 | |
|     });
 | |
| 
 | |
|     function parseText(ingredient: RecipeIngredient) {
 | |
|       return parseIngredientText(ingredient, props.recipe.settings?.disableAmount || false, props.scale);
 | |
|     }
 | |
| 
 | |
|     return {
 | |
|       hasNotes,
 | |
|       imageKey,
 | |
|       ImagePosition,
 | |
|       parseText,
 | |
|       parseIngredientText,
 | |
|       preferences,
 | |
|       recipeImageUrl,
 | |
|       ingredientSections,
 | |
|       instructionSections,
 | |
|     };
 | |
|   },
 | |
| });
 | |
| </script>
 | |
| 
 | |
| <style scoped>
 | |
| /* Makes all text solid black */
 | |
| .wrapper {
 | |
|   background-color: white;
 | |
| }
 | |
| 
 | |
| .wrapper,
 | |
| .wrapper >>> * {
 | |
|   opacity: 1 !important;
 | |
|   color: black !important;
 | |
| }
 | |
| 
 | |
| /* Prevents sections from being broken up between pages */
 | |
| .print-section {
 | |
|   page-break-inside: avoid;
 | |
| }
 | |
| 
 | |
| p {
 | |
|   padding-bottom: 0 !important;
 | |
|   margin-bottom: 0 !important;
 | |
| }
 | |
| 
 | |
| .v-card__text {
 | |
|   padding-bottom: 0;
 | |
|   margin-bottom: 0;
 | |
| }
 | |
| 
 | |
| .ingredient-grid {
 | |
|   display: grid;
 | |
|   grid-auto-flow: column;
 | |
|   grid-template-columns: 1fr 1fr;
 | |
|   grid-gap: 0.5rem;
 | |
| }
 | |
| 
 | |
| .ingredient-title,
 | |
| .instruction-title {
 | |
|   grid-column: 1 / span 2;
 | |
|   text-decoration: underline;
 | |
|   text-underline-offset: 4px;
 | |
| }
 | |
| 
 | |
| .ingredient-body,
 | |
| .recipe-step-body,
 | |
| .note-body {
 | |
|   font-size: 14px;
 | |
| }
 | |
| 
 | |
| ul {
 | |
|   padding-left: 1rem;
 | |
| }
 | |
| 
 | |
| li {
 | |
|   list-style-type: none;
 | |
|   margin-bottom: 0.25rem;
 | |
| }
 | |
| </style>
 |