mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 02:03:35 -04:00 
			
		
		
		
	feature/editor-improvements (#289)
* pin editor buttons on scroll * scaler scratch * fix langauge assignment 1st pass * set lang on navigate * refactor/breakup router * unify style for language selectro * refactor/code-cleanup * refactor/page specific components to page folder * Fix time card layout issue * fix timecard display * update mobile cards / fix overflow errors Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
		
							
								
								
									
										42
									
								
								dev/ingredientScaler/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								dev/ingredientScaler/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import { recipeIngredient } from "./recipeIngredient"; | ||||
| import { recipeNumber } from "./recipeNumber"; | ||||
|  | ||||
| export const ingredientScaler = { | ||||
|   process(ingredientArray, scale) { | ||||
|     console.log(scale); | ||||
|     let workingArray = ingredientArray.map(x => | ||||
|       ingredientScaler.markIngredient(x) | ||||
|     ); | ||||
|     return workingArray.map(x => ingredientScaler.adjustIngredients(x, scale)); | ||||
|   }, | ||||
|  | ||||
|   adjustIngredients(ingredient, scale) { | ||||
|     var scaledQuantity = new recipeNumber(ingredient.quantity).multiply(scale); | ||||
|     const newText = ingredient.text.replace( | ||||
|       ingredient.quantity, | ||||
|       scaledQuantity | ||||
|     ); | ||||
|     return { ...ingredient, quantity: scaledQuantity, text: newText }; | ||||
|   }, | ||||
|  | ||||
|   markIngredient(ingredient) { | ||||
|     console.log(ingredient); | ||||
|     const returnVar = ingredient.replace( | ||||
|       /^([\d/?[^\s&]*)(?: |\s)(\w*)/g, | ||||
|       (match, quantity, unit) => { | ||||
|         return `${unit}${quantity},${match}`; | ||||
|       } | ||||
|     ); | ||||
|     const split = returnVar.split(","); | ||||
|     const [unit, quantity, match] = split; | ||||
|     console.log("Split", unit, quantity, match); | ||||
|     const n = new recipeNumber(quantity); | ||||
|     const i = new recipeIngredient(n, unit); | ||||
|     const serializedQuantity = n.isFraction() ? n.toImproperFraction() : n; | ||||
|     return { | ||||
|       unit: i, | ||||
|       quantity: serializedQuantity.toString(), | ||||
|       text: match, | ||||
|     }; | ||||
|   }, | ||||
| }; | ||||
							
								
								
									
										75
									
								
								dev/ingredientScaler/recipeIngredient.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								dev/ingredientScaler/recipeIngredient.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| export const recipeIngredient = function(quantity, unit) { | ||||
|   this.quantity = quantity; | ||||
|   this.unit = unit; | ||||
| }; | ||||
|  | ||||
| recipeIngredient.prototype.isSingular = function() { | ||||
|   return this.quantity > 0 && this.quantity <= 1; | ||||
| }; | ||||
|  | ||||
| recipeIngredient.prototype.pluralize = function() { | ||||
|   if (this.isSingular()) { | ||||
|     return this.unit; | ||||
|   } else { | ||||
|     return `${this.unit}s`; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| recipeIngredient.prototype.getSingularUnit = function() { | ||||
|   if (this.isSingular()) { | ||||
|     return this.unit; | ||||
|   } else { | ||||
|     return this.unit.replace(/s$/, ""); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| recipeIngredient.prototype.toString = function() { | ||||
|   return `${this.quantity.toString()} ${this.pluralize()}`; | ||||
| }; | ||||
|  | ||||
| recipeIngredient.prototype.convertUnits = function() { | ||||
|   const conversion = recipeIngredient.CONVERSIONS[this.unit] || {}; | ||||
|   if (conversion.min && this.quantity < conversion.min.value) { | ||||
|     this.unit = conversion.min.next; | ||||
|     this.quantity.multiply(conversion.to[this.unit]); | ||||
|   } else if (conversion.max && this.quantity >= conversion.max.value) { | ||||
|     this.unit = conversion.max.next; | ||||
|     this.quantity.multiply(conversion.to[this.unit]); | ||||
|   } | ||||
|   return this; | ||||
| }; | ||||
|  | ||||
| recipeIngredient.CONVERSIONS = { | ||||
|   cup: { | ||||
|     to: { | ||||
|       tablespoon: 16, | ||||
|     }, | ||||
|     min: { | ||||
|       value: 1 / 4, | ||||
|       next: "tablespoon", | ||||
|     }, | ||||
|   }, | ||||
|   tablespoon: { | ||||
|     to: { | ||||
|       teaspoon: 3, | ||||
|       cup: 1 / 16, | ||||
|     }, | ||||
|     min: { | ||||
|       value: 1, | ||||
|       next: "teaspoon", | ||||
|     }, | ||||
|     max: { | ||||
|       value: 4, | ||||
|       next: "cup", | ||||
|     }, | ||||
|   }, | ||||
|   teaspoon: { | ||||
|     to: { | ||||
|       tablespoon: 1 / 3, | ||||
|     }, | ||||
|     max: { | ||||
|       value: 3, | ||||
|       next: "tablespoon", | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
							
								
								
									
										166
									
								
								dev/ingredientScaler/recipeNumber.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								dev/ingredientScaler/recipeNumber.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | ||||
| export const recipeNumber = function(number) { | ||||
|   const match = number.match( | ||||
|     /^(?:(\d+)|(?:(\d+)(?: | ))?(?:(\d+)\/(\d+))?)$/ | ||||
|   ); | ||||
|   if (!match || !match[0] || match[4] == "0") { | ||||
|     throw `Invalid number: "${number}".`; | ||||
|   } | ||||
|   this.wholeNumber = +(match[1] || match[2]); | ||||
|   this.numerator = +match[3]; | ||||
|   this.denominator = +match[4]; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Determines if the number is a fraction. | ||||
|  * @this {recipeNumber} | ||||
|  * @return {boolean} If the number is a fraction. | ||||
|  */ | ||||
| recipeNumber.prototype.isFraction = function() { | ||||
|   return !!(this.numerator && this.denominator); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Determines if the fraction is proper, which is defined as | ||||
|  * the numerator being strictly less than the denominator. | ||||
|  * @this {recipeNumber} | ||||
|  * @return {boolean} If the fraction is proper. | ||||
|  */ | ||||
| recipeNumber.prototype.isProperFraction = function() { | ||||
|   return this.numerator < this.denominator; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Determines if the fraction is improper, which is defined as | ||||
|  * the numerator being greater than or equal to the denominator. | ||||
|  * @this {recipeNumber} | ||||
|  * @return {boolean} If the fraction is improper. | ||||
|  */ | ||||
| recipeNumber.prototype.isImproperFraction = function() { | ||||
|   return this.numerator >= this.denominator; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Determines if the fraction is mixed, which is defined as | ||||
|  * a whole number with a proper fraction. | ||||
|  * @this {recipeNumber} | ||||
|  * @return {boolean} If the fraction is mixed. | ||||
|  */ | ||||
| recipeNumber.prototype.isMixedFraction = function() { | ||||
|   return this.isProperFraction() && !isNaN(this.wholeNumber); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Simplifies fractions. Examples: | ||||
|  *   3/2 = 1 1/2 | ||||
|  *   4/2 = 2 | ||||
|  *   1 3/2 = 2 1/2 | ||||
|  *   0/1 = 0 | ||||
|  *   1 0/1 = 1 | ||||
|  * @this {recipeNumber} | ||||
|  * @return {recipeNumber} The instance. | ||||
|  */ | ||||
| recipeNumber.prototype.simplifyFraction = function() { | ||||
|   if (this.isImproperFraction()) { | ||||
|     this.wholeNumber |= 0; | ||||
|     this.wholeNumber += Math.floor(this.numerator / this.denominator); | ||||
|     const modulus = this.numerator % this.denominator; | ||||
|     if (modulus) { | ||||
|       this.numerator = modulus; | ||||
|     } else { | ||||
|       this.numerator = this.denominator = NaN; | ||||
|     } | ||||
|   } else if (this.numerator == 0) { | ||||
|     this.wholeNumber |= 0; | ||||
|     this.numerator = this.denominator = NaN; | ||||
|   } | ||||
|   return this; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Reduces a fraction. Examples: | ||||
|  *   2/6 = 1/3 | ||||
|  *   6/2 = 3/1 | ||||
|  * @this {recipeNumber} | ||||
|  * @return {recipeNumber} The instance. | ||||
|  */ | ||||
| recipeNumber.prototype.reduceFraction = function() { | ||||
|   if (this.isFraction()) { | ||||
|     const gcd = recipeNumber.gcd(this.numerator, this.denominator); | ||||
|     this.numerator /= gcd; | ||||
|     this.denominator /= gcd; | ||||
|   } | ||||
|   return this; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Converts proper fractions to improper fractions. Examples: | ||||
|  *   1 1/2 = 3/2 | ||||
|  *   3/2 = 3/2 | ||||
|  *   1/2 = 1/2 | ||||
|  *   2 = 2 | ||||
|  * | ||||
|  * @this {recipeNumber} | ||||
|  * @return {recipeNumber} The instance. | ||||
|  */ | ||||
| recipeNumber.prototype.toImproperFraction = function() { | ||||
|   if (!isNaN(this.wholeNumber)) { | ||||
|     this.numerator |= 0; | ||||
|     this.denominator = this.denominator || 1; | ||||
|     this.numerator += this.wholeNumber * this.denominator; | ||||
|     this.wholeNumber = NaN; | ||||
|   } | ||||
|   return this; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Multiplies the number by some decimal value. | ||||
|  * @param {number} multiplier The multiplier. | ||||
|  * @this {recipeNumber} | ||||
|  * @return {recipeNumber} The instance. | ||||
|  */ | ||||
| recipeNumber.prototype.multiply = function(multiplier) { | ||||
|   this.toImproperFraction(); | ||||
|   this.numerator *= multiplier; | ||||
|   return this.reduceFraction().simplifyFraction(); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Gets a string representation of the number. | ||||
|  * @this {recipeNumber} | ||||
|  * @return {string} The string representation of the number. | ||||
|  */ | ||||
| recipeNumber.prototype.toString = function() { | ||||
|   let number = ""; | ||||
|   let fraction = ""; | ||||
|   if (!isNaN(this.wholeNumber)) { | ||||
|     number += this.wholeNumber; | ||||
|   } | ||||
|   if (this.isFraction()) { | ||||
|     fraction = `${this.numerator}/${this.denominator}`; | ||||
|   } | ||||
|   if (number && fraction) { | ||||
|     number += ` ${fraction}`; | ||||
|   } | ||||
|   return number || fraction; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Gets a numeric representation of the number. | ||||
|  * @this {recipeNumber} | ||||
|  * @return {number} The numeric representation of the number. | ||||
|  */ | ||||
| recipeNumber.prototype.valueOf = function() { | ||||
|   let value = this.wholeNumber || 0; | ||||
|   value += this.numerator / this.denominator || 0; | ||||
|   return value; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Euclid's algorithm to find the greatest common divisor of two numbers. | ||||
|  * @param {number} a One number. | ||||
|  * @param {number} b Another number. | ||||
|  * @return {number} The GCD of the numbers. | ||||
|  */ | ||||
| recipeNumber.gcd = function gcd(a, b) { | ||||
|   return b ? recipeNumber.gcd(b, a % b) : a; | ||||
| }; | ||||
							
								
								
									
										11
									
								
								dev/scripts/publish-release-branch.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								dev/scripts/publish-release-branch.sh
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| git checkout dev | ||||
| git merge --strategy=ours master    # keep the content of this branch, but record a merge | ||||
| git checkout master | ||||
| git merge dev             # fast-forward master up to the merge | ||||
|  | ||||
|  | ||||
| ## TODOs | ||||
|  | ||||
| # Create New Branch v0.x.x | ||||
| # Push Branch Version to Github | ||||
| # Create Pull Request | ||||
| @@ -9,7 +9,7 @@ | ||||
|       > | ||||
|  | ||||
|       <v-slide-x-reverse-transition> | ||||
|         <AddRecipeFab v-if="loggedIn" /> | ||||
|         <TheRecipeFab v-if="loggedIn" /> | ||||
|       </v-slide-x-reverse-transition> | ||||
|       <router-view></router-view> | ||||
|     </v-main> | ||||
| @@ -19,7 +19,7 @@ | ||||
|  | ||||
| <script> | ||||
| import TheAppBar from "@/components/UI/TheAppBar"; | ||||
| import AddRecipeFab from "@/components/UI/AddRecipeFab"; | ||||
| import TheRecipeFab from "@/components/UI/TheRecipeFab"; | ||||
| import Vuetify from "./plugins/vuetify"; | ||||
| import { user } from "@/mixins/user"; | ||||
|  | ||||
| @@ -28,7 +28,7 @@ export default { | ||||
|  | ||||
|   components: { | ||||
|     TheAppBar, | ||||
|     AddRecipeFab, | ||||
|     TheRecipeFab, | ||||
|   }, | ||||
|  | ||||
|   mixins: [user], | ||||
| @@ -40,13 +40,12 @@ export default { | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   created() { | ||||
|   async created() { | ||||
|     window.addEventListener("keyup", e => { | ||||
|       if (e.key == "/" && !document.activeElement.id.startsWith("input")) { | ||||
|         this.search = !this.search; | ||||
|       } | ||||
|     }); | ||||
|     this.$store.dispatch("initLang", { currentVueComponent: this }); | ||||
|   }, | ||||
|  | ||||
|   async mounted() { | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { baseURL } from "./api-utils"; | ||||
| import { apiReq } from "./api-utils"; | ||||
| import { store } from "@/store"; | ||||
|  | ||||
| const settingsBase = baseURL + "site-settings"; | ||||
|  | ||||
| @@ -19,6 +20,7 @@ export const siteSettingsAPI =  { | ||||
|  | ||||
|   async update(body) { | ||||
|     let response = await apiReq.put(settingsURLs.updateSiteSettings, body); | ||||
|     store.dispatch("requestSiteSettings"); | ||||
|     return response.data; | ||||
|   }, | ||||
|  | ||||
|   | ||||
| @@ -1,38 +0,0 @@ | ||||
| <template> | ||||
|   <v-card> | ||||
|     <v-card-title>Last Scrapped JSON Data</v-card-title> | ||||
|     <v-card-text> | ||||
|       <VJsoneditor | ||||
|         @error="logError()" | ||||
|         v-model="lastRecipeJson" | ||||
|         height="1500px" | ||||
|         :options="jsonEditorOptions" | ||||
|       /> | ||||
|     </v-card-text> | ||||
|   </v-card> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import VJsoneditor from "v-jsoneditor"; | ||||
| import { api } from "@/api"; | ||||
| export default { | ||||
|   components: { VJsoneditor }, | ||||
|   data() { | ||||
|     return { | ||||
|       lastRecipeJson: {}, | ||||
|       jsonEditorOptions: { | ||||
|         mode: "code", | ||||
|         search: false, | ||||
|         mainMenuBar: false, | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
|  | ||||
|   async mounted() { | ||||
|     this.lastRecipeJson = await api.meta.getLastJson(); | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
| </style> | ||||
| @@ -1,37 +0,0 @@ | ||||
| <template> | ||||
|   <v-card> | ||||
|     <v-card-title>Last Scrapped JSON Data</v-card-title> | ||||
|     <v-card-text> | ||||
|       <VJsoneditor | ||||
|         @error="logError()" | ||||
|         v-model="lastRecipeJson" | ||||
|         height="1500px" | ||||
|         :options="jsonEditorOptions" | ||||
|       /> | ||||
|     </v-card-text> | ||||
|   </v-card> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import VJsoneditor from "v-jsoneditor"; | ||||
| export default { | ||||
|   components: { VJsoneditor }, | ||||
|   data() { | ||||
|     return { | ||||
|       lastRecipeJson: "", | ||||
|       jsonEditorOptions: { | ||||
|         mode: "code", | ||||
|         search: false, | ||||
|         mainMenuBar: false, | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
|  | ||||
|   async mounted() { | ||||
|     this.lastRecipeJson = "Hello \n 123 \n 567" | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
| </style> | ||||
							
								
								
									
										48
									
								
								frontend/src/components/FormHelpers/LanguageSelector.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								frontend/src/components/FormHelpers/LanguageSelector.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| <template> | ||||
|   <v-select | ||||
|     dense | ||||
|     :items="allLanguages" | ||||
|     item-text="name" | ||||
|     :label="$t('settings.language')" | ||||
|     prepend-icon="mdi-translate" | ||||
|     :value="selectedItem" | ||||
|     @input="setLanguage" | ||||
|   > | ||||
|   </v-select> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| const SELECT_EVENT = "select-lang"; | ||||
| export default { | ||||
|   props: { | ||||
|     siteSettings: { | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   data: function() { | ||||
|     return { | ||||
|       selectedItem: 0, | ||||
|       items: [ | ||||
|         { | ||||
|           name: "English", | ||||
|           value: "en-US", | ||||
|         }, | ||||
|       ], | ||||
|     }; | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.selectedItem = this.$store.getters.getActiveLang; | ||||
|   }, | ||||
|   computed: { | ||||
|     allLanguages() { | ||||
|       return this.$store.getters.getAllLangs; | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   methods: { | ||||
|     setLanguage(selectedLanguage) { | ||||
|       this.$emit(SELECT_EVENT, selectedLanguage); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| @@ -45,7 +45,7 @@ | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import DataTable from "@/components/Admin/Backup/ImportSummaryDialog/DataTable"; | ||||
| import DataTable from "@/components/ImportSummaryDialog"; | ||||
| export default { | ||||
|   components: { | ||||
|     DataTable, | ||||
| @@ -31,11 +31,7 @@ | ||||
|                 v-on="on" | ||||
|               ></v-text-field> | ||||
|             </template> | ||||
|             <DatePicker  | ||||
|               v-model="startDate" | ||||
|               no-title | ||||
|               @input="menu2 = false" | ||||
|               /> | ||||
|             <DatePicker v-model="startDate" no-title @input="menu2 = false" /> | ||||
|           </v-menu> | ||||
|         </v-col> | ||||
|         <v-col cols="12" lg="6" md="6" sm="12"> | ||||
| @@ -59,11 +55,7 @@ | ||||
|                 v-on="on" | ||||
|               ></v-text-field> | ||||
|             </template> | ||||
|             <DatePicker  | ||||
|               v-model="endDate" | ||||
|               no-title | ||||
|               @input="menu2 = false" | ||||
|               /> | ||||
|             <DatePicker v-model="endDate" no-title @input="menu2 = false" /> | ||||
|           </v-menu> | ||||
|         </v-col> | ||||
|       </v-row> | ||||
| @@ -87,7 +79,7 @@ | ||||
|  | ||||
| <script> | ||||
| const CREATE_EVENT = "created"; | ||||
| import DatePicker from "../UI/DatePicker"; | ||||
| import DatePicker from "@/components/FormHelpers/DatePicker"; | ||||
| import { api } from "@/api"; | ||||
| import utils from "@/utils"; | ||||
| import MealPlanCard from "./MealPlanCard"; | ||||
|   | ||||
| @@ -1,5 +1,20 @@ | ||||
| <template> | ||||
|   <v-toolbar class="card-btn" flat height="0" extension-height="0"> | ||||
|   <v-expand-transition> | ||||
|     <v-toolbar | ||||
|       class="card-btn pt-1" | ||||
|       flat | ||||
|       :height="isSticky ? null : '0'" | ||||
|       :extension-height="isSticky ? '20' : '0'" | ||||
|       color="rgb(255, 0, 0, 0.0)" | ||||
|     > | ||||
|       <ConfirmationDialog | ||||
|         :title="$t('recipe.delete-recipe')" | ||||
|         :message="$t('recipe.delete-ConfirmationDialog')" | ||||
|         color="error" | ||||
|         icon="mdi-alert-circle" | ||||
|         ref="deleteRecipieConfirm" | ||||
|         v-on:confirm="deleteRecipe()" | ||||
|       /> | ||||
|       <template v-slot:extension> | ||||
|         <v-col></v-col> | ||||
|         <div v-if="open"> | ||||
| @@ -13,14 +28,7 @@ | ||||
|           > | ||||
|             <v-icon>mdi-delete</v-icon> | ||||
|           </v-btn> | ||||
|         <Confirmation | ||||
|           :title="$t('recipe.delete-recipe')" | ||||
|           :message="$t('recipe.delete-confirmation')" | ||||
|           color="error" | ||||
|           icon="mdi-alert-circle" | ||||
|           ref="deleteRecipieConfirm" | ||||
|           v-on:confirm="deleteRecipe()" | ||||
|         /> | ||||
|  | ||||
|           <v-btn class="mr-2" fab dark small color="success" @click="save"> | ||||
|             <v-icon>mdi-content-save</v-icon> | ||||
|           </v-btn> | ||||
| @@ -33,10 +41,11 @@ | ||||
|         </v-btn> | ||||
|       </template> | ||||
|     </v-toolbar> | ||||
|   </v-expand-transition> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import Confirmation from "../../components/UI/Confirmation.vue"; | ||||
| import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog.vue"; | ||||
|  | ||||
| export default { | ||||
|   props: { | ||||
| @@ -47,7 +56,25 @@ export default { | ||||
|   }, | ||||
|  | ||||
|   components: { | ||||
|     Confirmation, | ||||
|     ConfirmationDialog, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       stickyTop: 50, | ||||
|       scrollPosition: null, | ||||
|     }; | ||||
|   }, | ||||
|   mounted() { | ||||
|     window.addEventListener("scroll", this.updateScroll); | ||||
|   }, | ||||
|   destroy() { | ||||
|     window.removeEventListener("scroll", this.updateScroll); | ||||
|   }, | ||||
|  | ||||
|   computed: { | ||||
|     isSticky() { | ||||
|       return this.scrollPosition >= 500; | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   methods: { | ||||
| @@ -57,6 +84,9 @@ export default { | ||||
|     save() { | ||||
|       this.$emit("save"); | ||||
|     }, | ||||
|     updateScroll() { | ||||
|       this.scrollPosition = window.scrollY; | ||||
|     }, | ||||
|  | ||||
|     deleteRecipeConfrim() { | ||||
|       this.$refs.deleteRecipieConfirm.open(); | ||||
|   | ||||
| @@ -1,23 +1,39 @@ | ||||
| <template> | ||||
|   <v-card | ||||
|     class="mx-auto" | ||||
|     hover | ||||
|     :to="`/recipe/${slug}`" | ||||
|     max-height="125" | ||||
|     @click="$emit('selected')" | ||||
|   > | ||||
|     <v-list-item> | ||||
|       <v-list-item-avatar rounded size="125" class="mt-0 ml-n4"> | ||||
|         <v-img :src="getImage(slug)"> </v-img> | ||||
|       </v-list-item-avatar> | ||||
|       <v-list-item-content class="align-self-start"> | ||||
|         <v-list-item-title> | ||||
|           {{ name }} | ||||
|         </v-list-item-title> | ||||
|         <v-rating length="5" size="16" dense :value="rating"></v-rating> | ||||
|         <div class="text"> | ||||
|           <v-list-item-action-text> | ||||
|             {{ description | truncate(115) }} | ||||
|           </v-list-item-action-text> | ||||
|     <v-list-item three-line> | ||||
|       <v-list-item-avatar | ||||
|         tile | ||||
|         size="125" | ||||
|         color="grey" | ||||
|         class="v-mobile-img rounded-sm my-0 ml-n4" | ||||
|       > | ||||
|         <v-img :src="getImage(slug)" lazy-src=""></v-img | ||||
|       ></v-list-item-avatar> | ||||
|       <v-list-item-content> | ||||
|         <v-list-item-title class=" mb-1">{{ name }}</v-list-item-title> | ||||
|         <v-list-item-subtitle> {{ description }} </v-list-item-subtitle> | ||||
|         <div class="d-flex justify-center align-center"> | ||||
|           <RecipeChips | ||||
|             :items="tags" | ||||
|             :title="false" | ||||
|             :limit="1" | ||||
|             :small="true" | ||||
|             :isCategory="false" | ||||
|           /> | ||||
|           <v-rating | ||||
|             color="secondary" | ||||
|             class="ml-auto" | ||||
|             background-color="secondary lighten-3" | ||||
|             dense | ||||
|             length="5" | ||||
|             size="15" | ||||
|             :value="rating" | ||||
|           ></v-rating> | ||||
|         </div> | ||||
|       </v-list-item-content> | ||||
|     </v-list-item> | ||||
| @@ -25,8 +41,12 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import RecipeChips from "@/components/Recipe/RecipeViewer/RecipeChips"; | ||||
| import { api } from "@/api"; | ||||
| export default { | ||||
|   components: { | ||||
|     RecipeChips, | ||||
|   }, | ||||
|   props: { | ||||
|     name: String, | ||||
|     slug: String, | ||||
| @@ -36,6 +56,9 @@ export default { | ||||
|     route: { | ||||
|       default: true, | ||||
|     }, | ||||
|     tags: { | ||||
|       default: true, | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   methods: { | ||||
| @@ -47,6 +70,11 @@ export default { | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
| .v-mobile-img { | ||||
|   padding-top: 0; | ||||
|   padding-bottom: 0; | ||||
|   padding-left: 0; | ||||
| } | ||||
| .v-card--reveal { | ||||
|   align-items: center; | ||||
|   bottom: 0; | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
|           <div> | ||||
|             Recipe Image | ||||
|           </div> | ||||
|           <UploadBtn | ||||
|           <TheUploadBtn | ||||
|             class="ml-auto" | ||||
|             url="none" | ||||
|             file-name="image" | ||||
| @@ -44,12 +44,12 @@ | ||||
| <script> | ||||
| const REFRESH_EVENT = "refresh"; | ||||
| const UPLOAD_EVENT = "upload"; | ||||
| import UploadBtn from "@/components/UI/UploadBtn"; | ||||
| import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn"; | ||||
| import { api } from "@/api"; | ||||
| // import axios from "axios"; | ||||
| export default { | ||||
|   components: { | ||||
|     UploadBtn, | ||||
|     TheUploadBtn, | ||||
|   }, | ||||
|   props: { | ||||
|     slug: String, | ||||
|   | ||||
| @@ -33,7 +33,7 @@ | ||||
|         class="my-3" | ||||
|         :label="$t('recipe.recipe-name')" | ||||
|         v-model="value.name" | ||||
|         :rules="[rules.required]" | ||||
|         :rules="[existsRule]" | ||||
|       > | ||||
|       </v-text-field> | ||||
|       <v-textarea | ||||
| @@ -94,7 +94,7 @@ | ||||
|                       class="mr-n1" | ||||
|                       slot="prepend" | ||||
|                       color="error" | ||||
|                       @click="removeIngredient(index)" | ||||
|                       @click="removeByIndex(value.recipeIngredient, index)" | ||||
|                     > | ||||
|                       mdi-delete | ||||
|                     </v-icon> | ||||
| @@ -107,7 +107,7 @@ | ||||
|           <v-btn color="secondary" fab dark small @click="addIngredient"> | ||||
|             <v-icon>mdi-plus</v-icon> | ||||
|           </v-btn> | ||||
|           <BulkAdd @bulk-data="appendIngredients" /> | ||||
|           <BulkAdd @bulk-data="addIngredient" /> | ||||
|  | ||||
|           <h2 class="mt-6">{{ $t("recipe.categories") }}</h2> | ||||
|           <CategoryTagSelector | ||||
| @@ -140,7 +140,7 @@ | ||||
|                   color="white" | ||||
|                   class="mr-2" | ||||
|                   elevation="0" | ||||
|                   @click="removeNote(index)" | ||||
|                   @click="removeByIndex(value.notes, index)" | ||||
|                 > | ||||
|                   <v-icon color="error">mdi-delete</v-icon> | ||||
|                 </v-btn> | ||||
| @@ -183,7 +183,7 @@ | ||||
|                     color="white" | ||||
|                     class="mr-2" | ||||
|                     elevation="0" | ||||
|                     @click="removeStep(index)" | ||||
|                     @click="removeByIndex(value.recipeInstructions, index)" | ||||
|                   > | ||||
|                     <v-icon size="24" color="error">mdi-delete</v-icon> | ||||
|                   </v-btn> | ||||
| @@ -218,6 +218,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| const UPLOAD_EVENT = "upload"; | ||||
| import draggable from "vuedraggable"; | ||||
| import utils from "@/utils"; | ||||
| import BulkAdd from "./BulkAdd"; | ||||
| @@ -225,6 +226,7 @@ import ExtrasEditor from "./ExtrasEditor"; | ||||
| import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector"; | ||||
| import NutritionEditor from "./NutritionEditor"; | ||||
| import ImageUploadBtn from "./ImageUploadBtn.vue"; | ||||
| import { validators } from "@/mixins/validators"; | ||||
| export default { | ||||
|   components: { | ||||
|     BulkAdd, | ||||
| @@ -237,26 +239,20 @@ export default { | ||||
|   props: { | ||||
|     value: Object, | ||||
|   }, | ||||
|   mixins: [validators], | ||||
|   data() { | ||||
|     return { | ||||
|       drag: false, | ||||
|       fileObject: null, | ||||
|       rules: { | ||||
|         required: v => !!v || this.$i18n.t("recipe.key-name-required"), | ||||
|         whiteSpace: v => | ||||
|           !v || | ||||
|           v.split(" ").length <= 1 || | ||||
|           this.$i18n.t("recipe.no-white-space-allowed"), | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
|   methods: { | ||||
|     uploadImage(fileObject) { | ||||
|       this.$emit("upload", fileObject); | ||||
|       this.$emit(UPLOAD_EVENT, fileObject); | ||||
|     }, | ||||
|     toggleDisabled(stepIndex) { | ||||
|       if (this.disabledSteps.includes(stepIndex)) { | ||||
|         let index = this.disabledSteps.indexOf(stepIndex); | ||||
|         const index = this.disabledSteps.indexOf(stepIndex); | ||||
|         if (index !== -1) { | ||||
|           this.disabledSteps.splice(index, 1); | ||||
|         } | ||||
| @@ -265,66 +261,40 @@ export default { | ||||
|       } | ||||
|     }, | ||||
|     isDisabled(stepIndex) { | ||||
|       if (this.disabledSteps.includes(stepIndex)) { | ||||
|         return "disabled-card"; | ||||
|       } else { | ||||
|         return; | ||||
|       } | ||||
|       return this.disabledSteps.includes(stepIndex) ? "disabled-card" : null; | ||||
|     }, | ||||
|     generateKey(item, index) { | ||||
|       return utils.generateUniqueKey(item, index); | ||||
|     }, | ||||
|  | ||||
|     appendIngredients(ingredients) { | ||||
|     addIngredient(ingredients = null) { | ||||
|       if (ingredients) { | ||||
|         this.value.recipeIngredient.push(...ingredients); | ||||
|     }, | ||||
|     addIngredient() { | ||||
|       let list = this.value.recipeIngredient; | ||||
|       list.push(""); | ||||
|     }, | ||||
|  | ||||
|     removeIngredient(index) { | ||||
|       this.value.recipeIngredient.splice(index, 1); | ||||
|       } else { | ||||
|         this.value.recipeIngredient.push(""); | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     appendSteps(steps) { | ||||
|       let processSteps = []; | ||||
|       steps.forEach(element => { | ||||
|         processSteps.push({ text: element }); | ||||
|       }); | ||||
|  | ||||
|       this.value.recipeInstructions.push(...processSteps); | ||||
|       this.value.recipeInstructions.push( | ||||
|         ...steps.map(x => ({ | ||||
|           text: x, | ||||
|         })) | ||||
|       ); | ||||
|     }, | ||||
|     addStep() { | ||||
|       let list = this.value.recipeInstructions; | ||||
|       list.push({ text: "" }); | ||||
|       this.value.recipeInstructions.push({ text: "" }); | ||||
|     }, | ||||
|     removeStep(index) { | ||||
|       this.value.recipeInstructions.splice(index, 1); | ||||
|     }, | ||||
|  | ||||
|     addNote() { | ||||
|       let list = this.value.notes; | ||||
|       list.push({ text: "" }); | ||||
|     }, | ||||
|     removeNote(index) { | ||||
|       this.value.notes.splice(index, 1); | ||||
|     }, | ||||
|     removeCategory(index) { | ||||
|       this.value.recipeCategory.splice(index, 1); | ||||
|     }, | ||||
|     removeTags(index) { | ||||
|       this.value.tags.splice(index, 1); | ||||
|       this.value.notes.push({ text: "" }); | ||||
|     }, | ||||
|     saveExtras(extras) { | ||||
|       this.value.extras = extras; | ||||
|     }, | ||||
|     removeByIndex(list, index) { | ||||
|       list.splice(index, 1); | ||||
|     }, | ||||
|     validateRecipe() { | ||||
|       if (this.$refs.form.validate()) { | ||||
|         return true; | ||||
|       } else { | ||||
|         return false; | ||||
|       } | ||||
|       return this.$refs.form.validate(); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -1,57 +1,26 @@ | ||||
| <template> | ||||
|   <v-card | ||||
|     color="accent" | ||||
|     class="custom-transparent d-flex justify-start align-center text-center " | ||||
|     class="custom-transparent d-flex justify-start align-center text-center time-card-flex" | ||||
|     tile | ||||
|     :width="`${timeCardWidth}`" | ||||
|     height="55" | ||||
|     v-if="totalTime || prepTime || performTime" | ||||
|     v-if="showCards" | ||||
|   > | ||||
|     <v-card flat color="rgb(255, 0, 0, 0.0)"> | ||||
|       <v-icon large color="white" class="mx-2"> mdi-clock-outline </v-icon> | ||||
|     </v-card> | ||||
|  | ||||
|     <v-divider vertical color="white" class="py-1" v-if="totalTime"> | ||||
|     </v-divider> | ||||
|     <v-card flat color="rgb(255, 0, 0, 0.0)" class=" my-2 " v-if="totalTime"> | ||||
|       <v-card-text class="white--text"> | ||||
|         <div> | ||||
|           <strong> {{ $t("recipe.total-time") }} </strong> | ||||
|         </div> | ||||
|         <div>{{ totalTime }}</div> | ||||
|       </v-card-text> | ||||
|     </v-card> | ||||
|  | ||||
|     <v-divider vertical color="white" class="py-1" v-if="prepTime"> </v-divider> | ||||
|  | ||||
|     <v-card | ||||
|       v-for="(time, index) in allTimes" | ||||
|       :key="index" | ||||
|       class="d-flex justify-start align-center text-center time-card-flex" | ||||
|       flat | ||||
|       color="rgb(255, 0, 0, 0.0)" | ||||
|       class="white--text my-2 " | ||||
|       v-if="prepTime" | ||||
|     > | ||||
|       <v-card-text class="white--text"> | ||||
|       <v-card-text class="caption white--text py-2"> | ||||
|         <div> | ||||
|           <strong> {{ $t("recipe.prep-time") }} </strong> | ||||
|           <strong> {{ time.name }} </strong> | ||||
|         </div> | ||||
|         <div>{{ prepTime }}</div> | ||||
|       </v-card-text> | ||||
|     </v-card> | ||||
|  | ||||
|     <v-divider vertical color="white" class="my-1" v-if="performTime"> | ||||
|     </v-divider> | ||||
|  | ||||
|     <v-card | ||||
|       flat | ||||
|       color="rgb(255, 0, 0, 0.0)" | ||||
|       class="white--text py-2 " | ||||
|       v-if="performTime" | ||||
|     > | ||||
|       <v-card-text class="white--text"> | ||||
|         <div> | ||||
|           <strong> {{ $t("recipe.perform-time") }} </strong> | ||||
|         </div> | ||||
|         <div>{{ performTime }}</div> | ||||
|         <div>{{ time.value }}</div> | ||||
|       </v-card-text> | ||||
|     </v-card> | ||||
|   </v-card> | ||||
| @@ -64,52 +33,52 @@ export default { | ||||
|     totalTime: String, | ||||
|     performTime: String, | ||||
|   }, | ||||
|   watch: { | ||||
|     showCards(val) { | ||||
|       console.log(val); | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     timeLength() { | ||||
|       let times = []; | ||||
|       let timeArray = [this.totalTime, this.prepTime, this.performTime]; | ||||
|       timeArray.forEach(element => { | ||||
|         if (element) { | ||||
|           times.push(element); | ||||
|         } | ||||
|       }); | ||||
|  | ||||
|       return times.length; | ||||
|     showCards() { | ||||
|       return [this.prepTime, this.totalTime, this.performTime].some( | ||||
|         x => !this.isEmpty(x) | ||||
|       ); | ||||
|     }, | ||||
|     iconColumn() { | ||||
|       switch (this.timeLength) { | ||||
|         case 0: | ||||
|           return null; | ||||
|         case 1: | ||||
|           return 4; | ||||
|         case 2: | ||||
|           return 3; | ||||
|         case 3: | ||||
|           return 2; | ||||
|         default: | ||||
|           return 1; | ||||
|       } | ||||
|     allTimes() { | ||||
|       return [ | ||||
|         this.validateTotalTime, | ||||
|         this.validatePrepTime, | ||||
|         this.validatePerformTime, | ||||
|       ].filter(x => x !== null); | ||||
|     }, | ||||
|     timeCardWidth() { | ||||
|       let timeArray = [this.totalTime, this.prepTime, this.performTime]; | ||||
|       let width = 80; | ||||
|       timeArray.forEach(element => { | ||||
|         if (element) { | ||||
|           width += 95; | ||||
|         } | ||||
|       }); | ||||
|  | ||||
|       if (this.$vuetify.breakpoint.name === "xs") { | ||||
|         return "100%"; | ||||
|       } | ||||
|  | ||||
|       return `${width}px`; | ||||
|     validateTotalTime() { | ||||
|       return !this.isEmpty(this.totalTime) | ||||
|         ? { name: this.$t("recipe.total-time"), value: this.totalTime } | ||||
|         : null; | ||||
|     }, | ||||
|     validatePrepTime() { | ||||
|       return !this.isEmpty(this.prepTime) | ||||
|         ? { name: this.$t("recipe.prep-time"), value: this.prepTime } | ||||
|         : null; | ||||
|     }, | ||||
|     validatePerformTime() { | ||||
|       return !this.isEmpty(this.performTime) | ||||
|         ? { name: this.$t("recipe.perform-time"), value: this.performTime } | ||||
|         : null; | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     isEmpty(str) { | ||||
|       return !str || str.length === 0; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| .time-card-flex { | ||||
|   width: fit-content; | ||||
| } | ||||
| .custom-transparent { | ||||
|   opacity: 0.7; | ||||
| } | ||||
|   | ||||
| @@ -73,6 +73,7 @@ | ||||
|             :slug="recipe.slug" | ||||
|             :rating="recipe.rating" | ||||
|             :image="recipe.image" | ||||
|             :tags="recipe.tags" | ||||
|           /> | ||||
|         </v-col> | ||||
|       </v-row> | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
|     @keydown.esc="cancel" | ||||
|   > | ||||
|     <v-card> | ||||
|       <v-app-bar v-if="Boolean(title)" :color="color" dense flat dark> | ||||
|       <v-app-bar v-if="Boolean(title)" :color="color" dense  dark> | ||||
|         <v-icon v-if="Boolean(icon)" left> {{ icon }}</v-icon> | ||||
|         <v-toolbar-title v-text="title" /> | ||||
|       </v-app-bar> | ||||
| @@ -36,13 +36,13 @@ | ||||
| const CLOSE_EVENT = "close"; | ||||
| const OPEN_EVENT = "open"; | ||||
| /** | ||||
|  * Confirmation Component used to add a second validaion step to an action. | ||||
|  * ConfirmationDialog Component used to add a second validaion step to an action. | ||||
|  * @version 1.0.1 | ||||
|  * @author [zackbcom](https://github.com/zackbcom) | ||||
|  * @since Version 1.0.0 | ||||
|  */ | ||||
| export default { | ||||
|   name: "Confirmation", | ||||
|   name: "ConfirmationDialog", | ||||
|   props: { | ||||
|     /** | ||||
|      * Message to be in body. | ||||
| @@ -1,87 +0,0 @@ | ||||
| <template> | ||||
|   <div class="text-center"> | ||||
|     <v-menu | ||||
|       transition="slide-x-transition" | ||||
|       bottom | ||||
|       right | ||||
|       offset-y | ||||
|       close-delay="200" | ||||
|     > | ||||
|       <template v-slot:activator="{ on, attrs }"> | ||||
|         <v-btn v-bind="attrs" v-on="on" icon> | ||||
|           <v-icon>mdi-translate</v-icon> | ||||
|         </v-btn> | ||||
|       </template> | ||||
|  | ||||
|       <v-list> | ||||
|         <v-list-item-group v-model="selectedItem" color="primary"> | ||||
|           <v-list-item | ||||
|             v-for="(item, i) in allLanguages" | ||||
|             :key="i" | ||||
|             link | ||||
|             @click="setLanguage(item.value)" | ||||
|           > | ||||
|             <v-list-item-content> | ||||
|               <v-list-item-title> | ||||
|                 {{ item.name }} | ||||
|               </v-list-item-title> | ||||
|             </v-list-item-content> | ||||
|           </v-list-item> | ||||
|         </v-list-item-group> | ||||
|       </v-list> | ||||
|     </v-menu> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| const SELECT_EVENT = "select-lang"; | ||||
| export default { | ||||
|   props: { | ||||
|     siteSettings: { | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   data: function() { | ||||
|     return { | ||||
|       selectedItem: 0, | ||||
|       items: [ | ||||
|         { | ||||
|           name: "English", | ||||
|           value: "en-US", | ||||
|         }, | ||||
|       ], | ||||
|     }; | ||||
|   }, | ||||
|   mounted() { | ||||
|     let active = this.$store.getters.getActiveLang; | ||||
|     this.allLanguages.forEach((element, index) => { | ||||
|       if (element.value === active) { | ||||
|         this.selectedItem = index; | ||||
|         return; | ||||
|       } | ||||
|     }); | ||||
|   }, | ||||
|   computed: { | ||||
|     allLanguages() { | ||||
|       return this.$store.getters.getAllLangs; | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   methods: { | ||||
|     setLanguage(selectedLanguage) { | ||||
|       if (this.siteSettings) { | ||||
|         this.$emit(SELECT_EVENT, selectedLanguage); | ||||
|       } else { | ||||
|         this.$store.dispatch("setLang", {  | ||||
|           currentVueComponent: this,  | ||||
|           language: selectedLanguage }); | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| <style> | ||||
| .menu-text { | ||||
|   text-align: left !important; | ||||
| } | ||||
| </style> | ||||
| @@ -1,66 +0,0 @@ | ||||
| <template> | ||||
|   <v-dialog | ||||
|     v-model="dialog" | ||||
|     max-width="900px" | ||||
|     :fullscreen="$vuetify.breakpoint.xsOnly" | ||||
|   > | ||||
|     <v-card> | ||||
|       <v-toolbar dark color="primary" v-show="$vuetify.breakpoint.xsOnly"> | ||||
|         <v-btn icon dark @click="dialog = false"> | ||||
|           <v-icon>mdi-close</v-icon> | ||||
|         </v-btn> | ||||
|         <v-toolbar-title>{{ title }}</v-toolbar-title> | ||||
|         <v-spacer></v-spacer> | ||||
|         <v-toolbar-items></v-toolbar-items> | ||||
|       </v-toolbar> | ||||
|       <v-card-title v-show="$vuetify.breakpoint.smAndUp"> | ||||
|         {{ title }} | ||||
|       </v-card-title> | ||||
|       <v-card-text class="mt-3"> | ||||
|         <v-row> | ||||
|           <v-col> | ||||
|             <v-alert outlined dense type="success"> | ||||
|               <h4>{{ successHeader }}</h4> | ||||
|               <p v-for="success in this.success" :key="success" class="my-1"> | ||||
|                 - {{ success }} | ||||
|               </p> | ||||
|             </v-alert> | ||||
|           </v-col> | ||||
|           <v-col> | ||||
|             <v-alert v-if="failed[0]" outlined dense type="error"> | ||||
|               <h4>{{ failedHeader }}</h4> | ||||
|               <p v-for="fail in this.failed" :key="fail" class="my-1"> | ||||
|                 - {{ fail }} | ||||
|               </p> | ||||
|             </v-alert> | ||||
|           </v-col> | ||||
|         </v-row> | ||||
|       </v-card-text> | ||||
|     </v-card> | ||||
|   </v-dialog> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     title: String, | ||||
|     successHeader: String, | ||||
|     success: Array, | ||||
|     failedHeader: String, | ||||
|     failed: Array, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       dialog: false, | ||||
|     }; | ||||
|   }, | ||||
|   methods: { | ||||
|     open() { | ||||
|       this.dialog = true; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
| </style> | ||||
| @@ -35,7 +35,7 @@ | ||||
|         <v-icon>mdi-magnify</v-icon> | ||||
|       </v-btn> | ||||
|  | ||||
|       <SiteMenu /> | ||||
|       <TheSiteMenu /> | ||||
|     </v-app-bar> | ||||
|     <v-app-bar | ||||
|       v-else | ||||
| @@ -67,13 +67,13 @@ | ||||
|         <v-icon>mdi-magnify</v-icon> | ||||
|       </v-btn> | ||||
|  | ||||
|       <SiteMenu /> | ||||
|       <TheSiteMenu /> | ||||
|     </v-app-bar> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import SiteMenu from "@/components/UI/SiteMenu"; | ||||
| import TheSiteMenu from "@/components/UI/TheSiteMenu"; | ||||
| import SearchBar from "@/components/UI/Search/SearchBar"; | ||||
| import SearchDialog from "@/components/UI/Search/SearchDialog"; | ||||
| import { user } from "@/mixins/user"; | ||||
| @@ -82,7 +82,7 @@ export default { | ||||
|  | ||||
|   mixins: [user], | ||||
|   components: { | ||||
|     SiteMenu, | ||||
|     TheSiteMenu, | ||||
|     SearchBar, | ||||
|     SearchDialog, | ||||
|   }, | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import App from "./App.vue"; | ||||
| import vuetify from "./plugins/vuetify"; | ||||
| import store from "./store"; | ||||
| import VueRouter from "vue-router"; | ||||
| import { routes } from "./routes"; | ||||
| import { router } from "./routes"; | ||||
| import i18n from "./i18n"; | ||||
| import FlashMessage from "@smartweb/vue-flash-message"; | ||||
| import "@mdi/font/css/materialdesignicons.css"; | ||||
| @@ -13,25 +13,6 @@ Vue.use(FlashMessage); | ||||
| Vue.config.productionTip = false; | ||||
| Vue.use(VueRouter); | ||||
|  | ||||
| const router = new VueRouter({ | ||||
|   routes, | ||||
|   mode: process.env.NODE_ENV === "production" ? "history" : "hash", | ||||
| }); | ||||
|  | ||||
| const DEFAULT_TITLE = 'Mealie'; | ||||
| const TITLE_SEPARATOR = '🍴'; | ||||
| const TITLE_SUFFIX = " " + TITLE_SEPARATOR + " " + DEFAULT_TITLE; | ||||
| router.afterEach( (to) => { | ||||
|   Vue.nextTick( async () => { | ||||
|     if(typeof to.meta.title === 'function' ) { | ||||
|       const title  = await to.meta.title(to); | ||||
|       document.title = title + TITLE_SUFFIX; | ||||
|     } else { | ||||
|       document.title = to.meta.title ? to.meta.title + TITLE_SUFFIX : DEFAULT_TITLE; | ||||
|     } | ||||
|   }); | ||||
| });   | ||||
|  | ||||
| const vueApp = new Vue({ | ||||
|   vuetify, | ||||
|   store, | ||||
| @@ -56,5 +37,4 @@ let titleCase = function(value) { | ||||
| Vue.filter("truncate", truncate); | ||||
| Vue.filter("titleCase", titleCase); | ||||
|  | ||||
| export { vueApp }; | ||||
| export { router }; | ||||
| export { router, vueApp }; | ||||
|   | ||||
| @@ -4,12 +4,18 @@ export const validators = { | ||||
|       emailRule: v => | ||||
|         !v || | ||||
|         /^[^@\s]+@[^@\s.]+.[^@.\s]+$/.test(v) || | ||||
|        this.$t('user.e-mail-must-be-valid'), | ||||
|         this.$t("user.e-mail-must-be-valid"), | ||||
|  | ||||
|       existsRule: value => !!value || this.$t('general.field-required'), | ||||
|       existsRule: value => !!value || this.$t("general.field-required"), | ||||
|  | ||||
|       minRule: v => | ||||
|         v.length >= 8 || this.$t('user.use-8-characters-or-more-for-your-password'), | ||||
|         v.length >= 8 || | ||||
|         this.$t("user.use-8-characters-or-more-for-your-password"), | ||||
|  | ||||
|       whiteSpace: v => | ||||
|         !v || | ||||
|         v.split(" ").length <= 1 || | ||||
|         this.$t("recipe.no-white-space-allowed"), | ||||
|     }; | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -38,7 +38,7 @@ | ||||
|  | ||||
| <script> | ||||
| import { api } from "@/api"; | ||||
| import TheDownloadBtn from "@/components/UI/TheDownloadBtn"; | ||||
| import TheDownloadBtn from "@/components/UI/Buttons/TheDownloadBtn"; | ||||
| export default { | ||||
|   components: { TheDownloadBtn }, | ||||
|   data() { | ||||
|   | ||||
| @@ -58,8 +58,8 @@ | ||||
| 
 | ||||
| 
 | ||||
| <script> | ||||
| import ImportOptions from "@/components/Admin/Backup/ImportOptions"; | ||||
| import TheDownloadBtn from "@/components/UI/TheDownloadBtn.vue"; | ||||
| import ImportOptions from "./ImportOptions"; | ||||
| import TheDownloadBtn from "@/components/UI/Buttons/TheDownloadBtn.vue"; | ||||
| import { backupURLs } from "@/api/backup"; | ||||
| export default { | ||||
|   components: { ImportOptions, TheDownloadBtn }, | ||||
| @@ -42,7 +42,7 @@ | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import ImportOptions from "@/components/Admin/Backup/ImportOptions"; | ||||
| import ImportOptions from "./ImportOptions"; | ||||
| import { api } from "@/api"; | ||||
| export default { | ||||
|   components: { ImportOptions }, | ||||
| @@ -20,7 +20,7 @@ | ||||
|       <v-card-title class="mt-n6"> | ||||
|         {{ $t("settings.available-backups") }} | ||||
|         <span> | ||||
|           <UploadBtn | ||||
|           <TheUploadBtn | ||||
|             class="mt-1" | ||||
|             url="/api/backups/upload" | ||||
|             @uploaded="getAvailableBackups" | ||||
| @@ -33,14 +33,7 @@ | ||||
|         @finished="processFinished" | ||||
|         :backups="availableBackups" | ||||
|       /> | ||||
|       <SuccessFailureAlert | ||||
|         ref="report" | ||||
|         :title="$t('settings.backup.backup-restore-report')" | ||||
|         :success-header="$t('settings.backup.successfully-imported')" | ||||
|         :success="successfulImports" | ||||
|         :failed-header="$t('settings.backup.failed-imports')" | ||||
|         :failed="failedImports" | ||||
|       /> | ||||
|  | ||||
|       <ImportSummaryDialog ref="report" :import-data="importData" /> | ||||
|     </v-card-text> | ||||
|   </v-card> | ||||
| @@ -48,16 +41,14 @@ | ||||
|  | ||||
| <script> | ||||
| import { api } from "@/api"; | ||||
| import SuccessFailureAlert from "@/components/UI/SuccessFailureAlert"; | ||||
| import ImportSummaryDialog from "@/components/Admin/Backup/ImportSummaryDialog"; | ||||
| import UploadBtn from "@/components/UI/UploadBtn"; | ||||
| import AvailableBackupCard from "@/components/Admin/Backup/AvailableBackupCard"; | ||||
| import NewBackupCard from "@/components/Admin/Backup/NewBackupCard"; | ||||
| import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn"; | ||||
| import ImportSummaryDialog from "@/components/ImportSummaryDialog"; | ||||
| import AvailableBackupCard from "@/pages/Admin/Backup/AvailableBackupCard"; | ||||
| import NewBackupCard from "@/pages/Admin/Backup/NewBackupCard"; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     SuccessFailureAlert, | ||||
|     UploadBtn, | ||||
|     TheUploadBtn, | ||||
|     AvailableBackupCard, | ||||
|     NewBackupCard, | ||||
|     ImportSummaryDialog, | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <Confirmation | ||||
|     <ConfirmationDialog | ||||
|       ref="deleteGroupConfirm" | ||||
|       :title="$t('user.confirm-group-deletion')" | ||||
|       :message=" | ||||
| @@ -55,10 +55,10 @@ | ||||
| 
 | ||||
| <script> | ||||
| const RENDER_EVENT = "update"; | ||||
| import Confirmation from "@/components/UI/Confirmation"; | ||||
| import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog"; | ||||
| import { api } from "@/api"; | ||||
| export default { | ||||
|   components: { Confirmation }, | ||||
|   components: { ConfirmationDialog }, | ||||
|   props: { | ||||
|     group: { | ||||
|       default: { | ||||
| @@ -85,7 +85,7 @@ | ||||
| <script> | ||||
| import { validators } from "@/mixins/validators"; | ||||
| import { api } from "@/api"; | ||||
| import GroupCard from "@/components/Admin/ManageUsers/GroupCard"; | ||||
| import GroupCard from "./GroupCard"; | ||||
| export default { | ||||
|   components: { GroupCard }, | ||||
|   mixins: [validators], | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
|   <v-card outlined class="mt-n1"> | ||||
|     <Confirmation | ||||
|     <ConfirmationDialog | ||||
|       ref="deleteUserDialog" | ||||
|       :title="$t('user.confirm-link-deletion')" | ||||
|       :message=" | ||||
| @@ -107,11 +107,11 @@ | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import Confirmation from "@/components/UI/Confirmation"; | ||||
| import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog"; | ||||
| import { api } from "@/api"; | ||||
| import { validators } from "@/mixins/validators"; | ||||
| export default { | ||||
|   components: { Confirmation }, | ||||
|   components: { ConfirmationDialog }, | ||||
|   mixins: [validators], | ||||
|   data() { | ||||
|     return { | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
|   <v-card outlined class="mt-n1"> | ||||
|     <Confirmation | ||||
|     <ConfirmationDialog | ||||
|       ref="deleteUserDialog" | ||||
|       :title="$t('user.confirm-user-deletion')" | ||||
|       :message=" | ||||
| @@ -144,11 +144,11 @@ | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import Confirmation from "@/components/UI/Confirmation"; | ||||
| import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog"; | ||||
| import { api } from "@/api"; | ||||
| import { validators } from "@/mixins/validators"; | ||||
| export default { | ||||
|   components: { Confirmation }, | ||||
|   components: { ConfirmationDialog }, | ||||
|   mixins: [validators], | ||||
|   data() { | ||||
|     return { | ||||
| @@ -11,17 +11,17 @@ | ||||
|         <v-tabs-slider></v-tabs-slider> | ||||
|  | ||||
|         <v-tab> | ||||
|           {{$t('user.users')}} | ||||
|           {{ $t("user.users") }} | ||||
|           <v-icon>mdi-account</v-icon> | ||||
|         </v-tab> | ||||
|  | ||||
|         <v-tab> | ||||
|           {{$t('user.sign-up-links')}} | ||||
|           {{ $t("user.sign-up-links") }} | ||||
|           <v-icon>mdi-account-plus-outline</v-icon> | ||||
|         </v-tab> | ||||
|  | ||||
|         <v-tab> | ||||
|           {{$t('user.groups')}} | ||||
|           {{ $t("user.groups") }} | ||||
|           <v-icon>mdi-account-group</v-icon> | ||||
|         </v-tab> | ||||
|       </v-tabs> | ||||
| @@ -42,9 +42,9 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import TheUserTable from "@/components/Admin/ManageUsers/TheUserTable"; | ||||
| import GroupDashboard from "@/components/Admin/ManageUsers/GroupDashboard"; | ||||
| import TheSignUpTable from "@/components/Admin/ManageUsers/TheSignUpTable"; | ||||
| import TheUserTable from "./TheUserTable"; | ||||
| import GroupDashboard from "./GroupDashboard"; | ||||
| import TheSignUpTable from "./TheSignUpTable"; | ||||
| export default { | ||||
|   components: { TheUserTable, GroupDashboard, TheSignUpTable }, | ||||
|   data() { | ||||
|   | ||||
| @@ -82,7 +82,7 @@ | ||||
|  | ||||
| <script> | ||||
| import { api } from "@/api"; | ||||
| import TimePickerDialog from "@/components/Admin/MealPlanner/TimePickerDialog"; | ||||
| import TimePickerDialog from "@/components/FormHelpers/TimePickerDialog"; | ||||
| import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector"; | ||||
| export default { | ||||
|   components: { | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|       {{ title }} | ||||
|       <v-spacer></v-spacer> | ||||
|       <span> | ||||
|         <UploadBtn | ||||
|         <TheUploadBtn | ||||
|           class="mt-1" | ||||
|           :url="`/api/migrations/${folder}/upload`" | ||||
|           fileName="archive" | ||||
| @@ -66,10 +66,10 @@ | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import UploadBtn from "../../UI/UploadBtn"; | ||||
| import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn"; | ||||
| import utils from "@/utils"; | ||||
| import { api } from "@/api"; | ||||
| import MigrationDialog from "@/components/Admin/Migration/MigrationDialog.vue"; | ||||
| import MigrationDialog from "./MigrationDialog"; | ||||
| export default { | ||||
|   props: { | ||||
|     folder: String, | ||||
| @@ -78,7 +78,7 @@ export default { | ||||
|     available: Array, | ||||
|   }, | ||||
|   components: { | ||||
|     UploadBtn, | ||||
|     TheUploadBtn, | ||||
|     MigrationDialog, | ||||
|   }, | ||||
|   data() { | ||||
| @@ -42,7 +42,7 @@ | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import DataTable from "@/components/Admin/Backup/ImportSummaryDialog/DataTable"; | ||||
| import DataTable from "@/components/ImportSummaryDialog"; | ||||
| export default { | ||||
|   components: { | ||||
|     DataTable, | ||||
| @@ -1,13 +1,5 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <SuccessFailureAlert | ||||
|       :title="$t('migration.migration-report')" | ||||
|       ref="report" | ||||
|       :failedHeader="$t('migration.failed-imports')" | ||||
|       :failed="failed" | ||||
|       :successHeader="$t('migration.successful-imports')" | ||||
|       :success="success" | ||||
|     /> | ||||
|     <v-card :loading="loading"> | ||||
|       <v-card-title class="headline"> | ||||
|         {{ $t("migration.recipe-migration") }} | ||||
| @@ -42,13 +34,11 @@ | ||||
|  | ||||
|  | ||||
| <script> | ||||
| import MigrationCard from "@/components/Admin/Migration/MigrationCard"; | ||||
| import SuccessFailureAlert from "@/components/UI/SuccessFailureAlert"; | ||||
| import MigrationCard from "./MigrationCard"; | ||||
| import { api } from "@/api"; | ||||
| export default { | ||||
|   components: { | ||||
|     MigrationCard, | ||||
|     SuccessFailureAlert, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|   | ||||
| @@ -68,7 +68,7 @@ | ||||
|         </v-card-text> | ||||
|  | ||||
|         <v-card-actions> | ||||
|           <UploadBtn | ||||
|           <TheUploadBtn | ||||
|             icon="mdi-image-area" | ||||
|             :text="$t('user.upload-photo')" | ||||
|             :url="userProfileImage" | ||||
| @@ -145,13 +145,13 @@ | ||||
|  | ||||
| <script> | ||||
| // import AvatarPicker from '@/components/AvatarPicker' | ||||
| import UploadBtn from "@/components/UI/UploadBtn"; | ||||
| import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn"; | ||||
| import { api } from "@/api"; | ||||
| import { validators } from "@/mixins/validators"; | ||||
| import { initials } from "@/mixins/initials"; | ||||
| export default { | ||||
|   components: { | ||||
|     UploadBtn, | ||||
|     TheUploadBtn, | ||||
|   }, | ||||
|   mixins: [validators, initials], | ||||
|   data() { | ||||
|   | ||||
| @@ -63,7 +63,7 @@ | ||||
| 
 | ||||
| <script> | ||||
| import draggable from "vuedraggable"; | ||||
| import CreatePageDialog from "@/components/Admin/General/CreatePageDialog"; | ||||
| import CreatePageDialog from "./CreatePageDialog"; | ||||
| import { api } from "@/api"; | ||||
| export default { | ||||
|   components: { | ||||
| @@ -117,12 +117,12 @@ | ||||
|       </v-row> | ||||
|     </v-card-text> | ||||
|     <v-card-text> | ||||
|       <h2 class="mt-1 mb-4">{{$t('settings.locale-settings')}}</h2> | ||||
|       <h2 class="mt-1 mb-4">{{ $t("settings.locale-settings") }}</h2> | ||||
|       <v-row> | ||||
|         <v-col cols="1"> | ||||
|           <LanguageMenu @select-lang="writeLang" :site-settings="true" /> | ||||
|         <v-col cols="12" md="3" sm="12"> | ||||
|           <LanguageSelector @select-lang="writeLang" :site-settings="true" /> | ||||
|         </v-col> | ||||
|         <v-col sm="3"> | ||||
|         <v-col cols="12" md="3" sm="12"> | ||||
|           <v-select | ||||
|             dense | ||||
|             prepend-icon="mdi-calendar-week-begin" | ||||
| @@ -147,14 +147,14 @@ | ||||
| 
 | ||||
| <script> | ||||
| import { api } from "@/api"; | ||||
| import LanguageMenu from "@/components/UI/LanguageMenu"; | ||||
| import LanguageSelector from "@/components/FormHelpers/LanguageSelector"; | ||||
| import draggable from "vuedraggable"; | ||||
| import NewCategoryTagDialog from "@/components/UI/Dialogs/NewCategoryTagDialog.vue"; | ||||
| 
 | ||||
| export default { | ||||
|   components: { | ||||
|     draggable, | ||||
|     LanguageMenu, | ||||
|     LanguageSelector, | ||||
|     NewCategoryTagDialog, | ||||
|   }, | ||||
|   data() { | ||||
| @@ -178,33 +178,33 @@ export default { | ||||
|     allDays() { | ||||
|       return [ | ||||
|         { | ||||
|           name: this.$t('general.sunday'), | ||||
|           name: this.$t("general.sunday"), | ||||
|           value: 0, | ||||
|         }, | ||||
|         { | ||||
|           name: this.$t('general.monday'), | ||||
|           name: this.$t("general.monday"), | ||||
|           value: 1, | ||||
|         }, | ||||
|         { | ||||
|           name: this.$t('general.tuesday'), | ||||
|           name: this.$t("general.tuesday"), | ||||
|           value: 2, | ||||
|         }, | ||||
|         { | ||||
|           name: this.$t('general.wednesday'), | ||||
|           name: this.$t("general.wednesday"), | ||||
|           value: 3, | ||||
|         }, | ||||
|         { | ||||
|           name: this.$t('general.thursday'), | ||||
|           name: this.$t("general.thursday"), | ||||
|           value: 4, | ||||
|         }, | ||||
|         { | ||||
|           name: this.$t('general.friday'), | ||||
|           name: this.$t("general.friday"), | ||||
|           value: 5, | ||||
|         }, | ||||
|         { | ||||
|           name: this.$t('general.saturday'), | ||||
|           name: this.$t("general.saturday"), | ||||
|           value: 6, | ||||
|         } | ||||
|         }, | ||||
|       ]; | ||||
|     }, | ||||
|   }, | ||||
| @@ -223,10 +223,8 @@ export default { | ||||
|       this.settings.categories.splice(index, 1); | ||||
|     }, | ||||
|     async saveSettings() { | ||||
|       await api.siteSettings.update(this.settings); | ||||
|       this.$store.dispatch("setLang", {  | ||||
|         currentVueComponent: this,  | ||||
|         language: this.settings.language }); | ||||
|       const newSettings = await api.siteSettings.update(this.settings); | ||||
|       console.log("New Settings", newSettings); | ||||
|       this.getOptions(); | ||||
|     }, | ||||
|   }, | ||||
| @@ -20,8 +20,8 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import HomePageSettings from "@/components/Admin/General/HomePageSettings"; | ||||
| import CustomPageCreator from "@/components/Admin/General/CustomPageCreator"; | ||||
| import HomePageSettings from "./HomePageSettings"; | ||||
| import CustomPageCreator from "./CustomPageCreator"; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <Confirmation | ||||
|     <ConfirmationDialog | ||||
|       :title="$t('settings.theme.delete-theme')" | ||||
|       :message="$t('settings.theme.are-you-sure-you-want-to-delete-this-theme')" | ||||
|       color="error" | ||||
| @@ -44,7 +44,7 @@ | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import Confirmation from "@/components/UI/Confirmation"; | ||||
| import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog"; | ||||
| import { api } from "@/api"; | ||||
| 
 | ||||
| const DELETE_EVENT = "delete"; | ||||
| @@ -52,7 +52,7 @@ const APPLY_EVENT = "apply"; | ||||
| const EDIT_EVENT = "edit"; | ||||
| export default { | ||||
|   components: { | ||||
|     Confirmation, | ||||
|     ConfirmationDialog, | ||||
|   }, | ||||
|   props: { | ||||
|     theme: Object, | ||||
| @@ -135,9 +135,9 @@ | ||||
|  | ||||
| <script> | ||||
| import { api } from "@/api"; | ||||
| import ColorPickerDialog from "@/components/Admin/Theme/ColorPickerDialog"; | ||||
| import NewThemeDialog from "@/components/Admin/Theme/NewThemeDialog"; | ||||
| import ThemeCard from "@/components/Admin/Theme/ThemeCard"; | ||||
| import ColorPickerDialog from "@/components/FormHelpers/ColorPickerDialog"; | ||||
| import NewThemeDialog from "./NewThemeDialog"; | ||||
| import ThemeCard from "./ThemeCard"; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import About from "@/pages/Admin/About"; | ||||
| import { store } from "../store"; | ||||
| import i18n from '@/i18n.js'; | ||||
|  | ||||
| export default { | ||||
| export const adminRoutes =  { | ||||
|   path: "/admin", | ||||
|   component: Admin, | ||||
|   beforeEnter: (to, _from, next) => { | ||||
|   | ||||
							
								
								
									
										18
									
								
								frontend/src/routes/auth.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								frontend/src/routes/auth.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import LoginPage from "@/pages/LoginPage"; | ||||
| import SignUpPage from "@/pages/SignUpPage"; | ||||
| import { store } from "../store"; | ||||
|  | ||||
| export const authRoutes = [ | ||||
|   { | ||||
|     path: "/logout", | ||||
|     beforeEnter: (_to, _from, next) => { | ||||
|       store.commit("setToken", ""); | ||||
|       store.commit("setIsLoggedIn", false); | ||||
|       next("/"); | ||||
|     }, | ||||
|   }, | ||||
|   { path: "/login", component: LoginPage }, | ||||
|  | ||||
|   { path: "/sign-up", redirect: "/" }, | ||||
|   { path: "/sign-up/:token", component: SignUpPage }, | ||||
| ]; | ||||
							
								
								
									
										15
									
								
								frontend/src/routes/general.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/routes/general.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import i18n from "@/i18n.js"; | ||||
| import SearchPage from "@/pages/SearchPage"; | ||||
| import HomePage from "@/pages/HomePage"; | ||||
|  | ||||
| export const generalRoutes = [ | ||||
|   { path: "/", name: "home", component: HomePage }, | ||||
|   { path: "/mealie", component: HomePage }, | ||||
|   { | ||||
|     path: "/search", | ||||
|     component: SearchPage, | ||||
|     meta: { | ||||
|       title: i18n.t("search.search"), | ||||
|     }, | ||||
|   }, | ||||
| ]; | ||||
| @@ -1,87 +1,54 @@ | ||||
| import HomePage from "@/pages/HomePage"; | ||||
| import Page404 from "@/pages/404Page"; | ||||
| import SearchPage from "@/pages/SearchPage"; | ||||
| import ViewRecipe from "@/pages/Recipe/ViewRecipe"; | ||||
| import NewRecipe from "@/pages/Recipe/NewRecipe"; | ||||
| import CustomPage from "@/pages/Recipes/CustomPage"; | ||||
| import AllRecipes from "@/pages/Recipes/AllRecipes"; | ||||
| import CategoryPage from "@/pages/Recipes/CategoryPage"; | ||||
| import TagPage from "@/pages/Recipes/TagPage"; | ||||
| import Planner from "@/pages/MealPlan/Planner"; | ||||
| import Debug from "@/pages/Debug"; | ||||
| import LoginPage from "@/pages/LoginPage"; | ||||
| import SignUpPage from "@/pages/SignUpPage"; | ||||
| import ThisWeek from "@/pages/MealPlan/ThisWeek"; | ||||
| import { api } from "@/api"; | ||||
| import Admin from "./admin"; | ||||
| import { adminRoutes } from "./admin"; | ||||
| import { authRoutes } from "./auth"; | ||||
| import { recipeRoutes } from "./recipes"; | ||||
| import { mealRoutes } from "./meal"; | ||||
| import { generalRoutes } from "./general"; | ||||
| import { store } from "../store"; | ||||
| import i18n from '@/i18n.js'; | ||||
| import VueRouter from "vue-router"; | ||||
| import VueI18n from "@/i18n"; | ||||
| import Vuetify from "@/plugins/vuetify"; | ||||
| import Vue from "vue"; | ||||
|  | ||||
| export const routes = [ | ||||
|   { path: "/", name: "home", component: HomePage }, | ||||
|   { | ||||
|     path: "/logout", | ||||
|     beforeEnter: (_to, _from, next) => { | ||||
|       store.commit("setToken", ""); | ||||
|       store.commit("setIsLoggedIn", false); | ||||
|       next("/"); | ||||
|     }, | ||||
|   }, | ||||
|   { path: "/mealie", component: HomePage }, | ||||
|   { path: "/login", component: LoginPage }, | ||||
|   { path: "/sign-up", redirect: "/" }, | ||||
|   { path: "/sign-up/:token", component: SignUpPage }, | ||||
|   { path: "/debug", component: Debug }, | ||||
|   {  | ||||
|     path: "/search",  | ||||
|     component: SearchPage, | ||||
|     meta: { | ||||
|       title: i18n.t('search.search'), | ||||
|     }, | ||||
|   }, | ||||
|   { path: "/recipes/all", component: AllRecipes }, | ||||
|   { path: "/pages/:customPage", component: CustomPage }, | ||||
|   { path: "/recipes/tag/:tag", component: TagPage }, | ||||
|   { path: "/recipes/category/:category", component: CategoryPage }, | ||||
|   {  | ||||
|     path: "/recipe/:recipe",  | ||||
|     component: ViewRecipe, | ||||
|     meta: { | ||||
|       title: async route => { | ||||
|         const recipe = await api.recipes.requestDetails(route.params.recipe); | ||||
|         return recipe.name; | ||||
|       }, | ||||
|     }  | ||||
|   }, | ||||
|   { path: "/new/", component: NewRecipe }, | ||||
|   {  | ||||
|     path: "/meal-plan/planner", | ||||
|     component: Planner, | ||||
|     meta: { | ||||
|       title:  i18n.t('meal-plan.meal-planner'), | ||||
|     }  | ||||
|   }, | ||||
|   {  | ||||
|     path: "/meal-plan/this-week",  | ||||
|     component: ThisWeek, | ||||
|     meta: { | ||||
|       title: i18n.t('meal-plan.dinner-this-week'), | ||||
|     }  | ||||
|   ...generalRoutes, | ||||
|   adminRoutes, | ||||
|   ...authRoutes, | ||||
|   ...mealRoutes, | ||||
|   ...recipeRoutes, | ||||
|  | ||||
|   }, | ||||
|   Admin, | ||||
|   { | ||||
|     path: "/meal-plan/today", | ||||
|     beforeEnter: async (_to, _from, next) => { | ||||
|       await todaysMealRoute().then(redirect => { | ||||
|         next(redirect); | ||||
|       }); | ||||
|     }, | ||||
|   }, | ||||
|   { path: "*", component: Page404 }, | ||||
| ]; | ||||
|  | ||||
| async function todaysMealRoute() { | ||||
|   const response = await api.mealPlans.today(); | ||||
|   return "/recipe/" + response.data; | ||||
| const router = new VueRouter({ | ||||
|   routes, | ||||
|   mode: process.env.NODE_ENV === "production" ? "history" : "hash", | ||||
| }); | ||||
|  | ||||
| const DEFAULT_TITLE = "Mealie"; | ||||
| const TITLE_SEPARATOR = "🍴"; | ||||
| const TITLE_SUFFIX = " " + TITLE_SEPARATOR + " " + DEFAULT_TITLE; | ||||
| router.afterEach(to => { | ||||
|   Vue.nextTick(async () => { | ||||
|     if (typeof to.meta.title === "function") { | ||||
|       const title = await to.meta.title(to); | ||||
|       document.title = title + TITLE_SUFFIX; | ||||
|     } else { | ||||
|       document.title = to.meta.title | ||||
|         ? to.meta.title + TITLE_SUFFIX | ||||
|         : DEFAULT_TITLE; | ||||
|     } | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| function loadLocale() { | ||||
|   VueI18n.locale = store.getters.getActiveLang; | ||||
|   Vuetify.framework.lang.current = store.getters.getActiveLang; | ||||
| } | ||||
|  | ||||
| router.beforeEach((__, _, next) => { | ||||
|   loadLocale(); | ||||
|   next(); | ||||
| }); | ||||
|  | ||||
| export { router }; | ||||
|   | ||||
							
								
								
									
										34
									
								
								frontend/src/routes/meal.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								frontend/src/routes/meal.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import Planner from "@/pages/MealPlan/Planner"; | ||||
| import ThisWeek from "@/pages/MealPlan/ThisWeek"; | ||||
| import i18n from "@/i18n.js"; | ||||
| import { api } from "@/api"; | ||||
|  | ||||
| export const mealRoutes = [ | ||||
|   { | ||||
|     path: "/meal-plan/planner", | ||||
|     component: Planner, | ||||
|     meta: { | ||||
|       title: i18n.t("meal-plan.meal-planner"), | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     path: "/meal-plan/this-week", | ||||
|     component: ThisWeek, | ||||
|     meta: { | ||||
|       title: i18n.t("meal-plan.dinner-this-week"), | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     path: "/meal-plan/today", | ||||
|     beforeEnter: async (_to, _from, next) => { | ||||
|       await todaysMealRoute().then(redirect => { | ||||
|         next(redirect); | ||||
|       }); | ||||
|     }, | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| async function todaysMealRoute() { | ||||
|   const response = await api.mealPlans.today(); | ||||
|   return "/recipe/" + response.data; | ||||
| } | ||||
							
								
								
									
										29
									
								
								frontend/src/routes/recipes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/src/routes/recipes.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| import ViewRecipe from "@/pages/Recipe/ViewRecipe"; | ||||
| import NewRecipe from "@/pages/Recipe/NewRecipe"; | ||||
| import CustomPage from "@/pages/Recipes/CustomPage"; | ||||
| import AllRecipes from "@/pages/Recipes/AllRecipes"; | ||||
| import CategoryPage from "@/pages/Recipes/CategoryPage"; | ||||
| import TagPage from "@/pages/Recipes/TagPage"; | ||||
| import { api } from "@/api"; | ||||
|  | ||||
| export const recipeRoutes = [ | ||||
|   // Recipes | ||||
|   { path: "/recipes/all", component: AllRecipes }, | ||||
|   { path: "/recipes/tag/:tag", component: TagPage }, | ||||
|   { path: "/recipes/category/:category", component: CategoryPage }, | ||||
|   // Misc | ||||
|   { path: "/new/", component: NewRecipe }, | ||||
|   { path: "/pages/:customPage", component: CustomPage }, | ||||
|  | ||||
|   // Recipe Page | ||||
|   { | ||||
|     path: "/recipe/:recipe", | ||||
|     component: ViewRecipe, | ||||
|     meta: { | ||||
|       title: async route => { | ||||
|         const recipe = await api.recipes.requestDetails(route.params.recipe); | ||||
|         return recipe.name; | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| ]; | ||||
| @@ -12,7 +12,7 @@ Vue.use(Vuex); | ||||
| const store = new Vuex.Store({ | ||||
|   plugins: [ | ||||
|     createPersistedState({ | ||||
|       paths: ["userSettings", "language.lang", "siteSettings"], | ||||
|       paths: ["userSettings", "siteSettings"], | ||||
|     }), | ||||
|   ], | ||||
|   modules: { | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| import VueI18n from "../../i18n"; | ||||
|  | ||||
| // This is the data store for the options for language selection. Property is reference only, you cannot set this property. | ||||
| const state = { | ||||
|   lang: "en-US", | ||||
|   allLangs: [ | ||||
|     { | ||||
|       name: "English", | ||||
| @@ -42,33 +40,11 @@ const state = { | ||||
|   ], | ||||
| }; | ||||
|  | ||||
| const mutations = { | ||||
|   setLang(state, payload) { | ||||
|     VueI18n.locale = payload; | ||||
|     state.lang = payload; | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| const actions = { | ||||
|   initLang({ getters }, { currentVueComponent }) { | ||||
|     VueI18n.locale = getters.getActiveLang; | ||||
|     currentVueComponent.$vuetify.lang.current = getters.getActiveLang; | ||||
|   }, | ||||
|   setLang({ commit }, { language, currentVueComponent }) { | ||||
|     VueI18n.locale = language; | ||||
|     currentVueComponent.$vuetify.lang.current = language; | ||||
|     commit('setLang', language); | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| const getters = { | ||||
|   getActiveLang: state => state.lang, | ||||
|   getAllLangs: state => state.allLangs, | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|   state, | ||||
|   mutations, | ||||
|   actions, | ||||
|   getters, | ||||
| }; | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| import { api } from "@/api"; | ||||
| import VueI18n from "@/i18n"; | ||||
| import Vuetify from "@/plugins/vuetify"; | ||||
|  | ||||
| const state = { | ||||
|   siteSettings: { | ||||
|     language: "en", | ||||
|     language: "en-US", | ||||
|     firstDayOfWeek: 0, | ||||
|     showRecent: true, | ||||
|     cardsPerSection: 9, | ||||
| @@ -13,17 +15,20 @@ const state = { | ||||
| const mutations = { | ||||
|   setSettings(state, payload) { | ||||
|     state.siteSettings = payload; | ||||
|     VueI18n.locale = payload.language; | ||||
|     Vuetify.framework.lang.current = payload.language; | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| const actions = { | ||||
|   async requestSiteSettings() { | ||||
|   async requestSiteSettings({ commit }) { | ||||
|     let settings = await api.siteSettings.get(); | ||||
|     this.commit("setSettings", settings); | ||||
|     commit("setSettings", settings); | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| const getters = { | ||||
|   getActiveLang: state => state.siteSettings.language, | ||||
|   getSiteSettings: state => state.siteSettings, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { api } from "@/api"; | ||||
| import Vuetify from "../../plugins/vuetify"; | ||||
| import Vuetify from "@/plugins/vuetify"; | ||||
| import axios from "axios"; | ||||
|  | ||||
| function inDarkMode(payload) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user