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:
Hayden 2021-04-21 21:52:12 -08:00 committed by GitHub
parent a5306c31c6
commit 284df44209
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 778 additions and 664 deletions

View 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&]*)(?:&nbsp;|\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,
};
},
};

View 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",
},
},
};

View File

@ -0,0 +1,166 @@
export const recipeNumber = function(number) {
const match = number.match(
/^(?:(\d+)|(?:(\d+)(?: |&nbsp;))?(?:(\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;
};

View 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

View File

@ -9,7 +9,7 @@
> >
<v-slide-x-reverse-transition> <v-slide-x-reverse-transition>
<AddRecipeFab v-if="loggedIn" /> <TheRecipeFab v-if="loggedIn" />
</v-slide-x-reverse-transition> </v-slide-x-reverse-transition>
<router-view></router-view> <router-view></router-view>
</v-main> </v-main>
@ -19,7 +19,7 @@
<script> <script>
import TheAppBar from "@/components/UI/TheAppBar"; import TheAppBar from "@/components/UI/TheAppBar";
import AddRecipeFab from "@/components/UI/AddRecipeFab"; import TheRecipeFab from "@/components/UI/TheRecipeFab";
import Vuetify from "./plugins/vuetify"; import Vuetify from "./plugins/vuetify";
import { user } from "@/mixins/user"; import { user } from "@/mixins/user";
@ -28,7 +28,7 @@ export default {
components: { components: {
TheAppBar, TheAppBar,
AddRecipeFab, TheRecipeFab,
}, },
mixins: [user], mixins: [user],
@ -40,13 +40,12 @@ export default {
}, },
}, },
created() { async created() {
window.addEventListener("keyup", e => { window.addEventListener("keyup", e => {
if (e.key == "/" && !document.activeElement.id.startsWith("input")) { if (e.key == "/" && !document.activeElement.id.startsWith("input")) {
this.search = !this.search; this.search = !this.search;
} }
}); });
this.$store.dispatch("initLang", { currentVueComponent: this });
}, },
async mounted() { async mounted() {

View File

@ -1,5 +1,6 @@
import { baseURL } from "./api-utils"; import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils"; import { apiReq } from "./api-utils";
import { store } from "@/store";
const settingsBase = baseURL + "site-settings"; const settingsBase = baseURL + "site-settings";
@ -11,7 +12,7 @@ const settingsURLs = {
customPage: id => `${settingsBase}/custom-pages/${id}`, customPage: id => `${settingsBase}/custom-pages/${id}`,
}; };
export const siteSettingsAPI = { export const siteSettingsAPI = {
async get() { async get() {
let response = await apiReq.get(settingsURLs.siteSettings); let response = await apiReq.get(settingsURLs.siteSettings);
return response.data; return response.data;
@ -19,6 +20,7 @@ export const siteSettingsAPI = {
async update(body) { async update(body) {
let response = await apiReq.put(settingsURLs.updateSiteSettings, body); let response = await apiReq.put(settingsURLs.updateSiteSettings, body);
store.dispatch("requestSiteSettings");
return response.data; return response.data;
}, },

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -45,7 +45,7 @@
</template> </template>
<script> <script>
import DataTable from "@/components/Admin/Backup/ImportSummaryDialog/DataTable"; import DataTable from "@/components/ImportSummaryDialog";
export default { export default {
components: { components: {
DataTable, DataTable,

View File

@ -31,11 +31,7 @@
v-on="on" v-on="on"
></v-text-field> ></v-text-field>
</template> </template>
<DatePicker <DatePicker v-model="startDate" no-title @input="menu2 = false" />
v-model="startDate"
no-title
@input="menu2 = false"
/>
</v-menu> </v-menu>
</v-col> </v-col>
<v-col cols="12" lg="6" md="6" sm="12"> <v-col cols="12" lg="6" md="6" sm="12">
@ -59,11 +55,7 @@
v-on="on" v-on="on"
></v-text-field> ></v-text-field>
</template> </template>
<DatePicker <DatePicker v-model="endDate" no-title @input="menu2 = false" />
v-model="endDate"
no-title
@input="menu2 = false"
/>
</v-menu> </v-menu>
</v-col> </v-col>
</v-row> </v-row>
@ -87,7 +79,7 @@
<script> <script>
const CREATE_EVENT = "created"; const CREATE_EVENT = "created";
import DatePicker from "../UI/DatePicker"; import DatePicker from "@/components/FormHelpers/DatePicker";
import { api } from "@/api"; import { api } from "@/api";
import utils from "@/utils"; import utils from "@/utils";
import MealPlanCard from "./MealPlanCard"; import MealPlanCard from "./MealPlanCard";

View File

@ -1,42 +1,51 @@
<template> <template>
<v-toolbar class="card-btn" flat height="0" extension-height="0"> <v-expand-transition>
<template v-slot:extension> <v-toolbar
<v-col></v-col> class="card-btn pt-1"
<div v-if="open"> flat
<v-btn :height="isSticky ? null : '0'"
class="mr-2" :extension-height="isSticky ? '20' : '0'"
fab color="rgb(255, 0, 0, 0.0)"
dark >
small <ConfirmationDialog
color="error" :title="$t('recipe.delete-recipe')"
@click="deleteRecipeConfrim" :message="$t('recipe.delete-ConfirmationDialog')"
> color="error"
<v-icon>mdi-delete</v-icon> icon="mdi-alert-circle"
ref="deleteRecipieConfirm"
v-on:confirm="deleteRecipe()"
/>
<template v-slot:extension>
<v-col></v-col>
<div v-if="open">
<v-btn
class="mr-2"
fab
dark
small
color="error"
@click="deleteRecipeConfrim"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
<v-btn class="mr-2" fab dark small color="success" @click="save">
<v-icon>mdi-content-save</v-icon>
</v-btn>
<v-btn class="mr-5" fab dark small color="secondary" @click="json">
<v-icon>mdi-code-braces</v-icon>
</v-btn>
</div>
<v-btn color="accent" fab dark small @click="editor">
<v-icon>mdi-square-edit-outline</v-icon>
</v-btn> </v-btn>
<Confirmation </template>
:title="$t('recipe.delete-recipe')" </v-toolbar>
:message="$t('recipe.delete-confirmation')" </v-expand-transition>
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>
<v-btn class="mr-5" fab dark small color="secondary" @click="json">
<v-icon>mdi-code-braces</v-icon>
</v-btn>
</div>
<v-btn color="accent" fab dark small @click="editor">
<v-icon>mdi-square-edit-outline</v-icon>
</v-btn>
</template>
</v-toolbar>
</template> </template>
<script> <script>
import Confirmation from "../../components/UI/Confirmation.vue"; import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog.vue";
export default { export default {
props: { props: {
@ -47,7 +56,25 @@ export default {
}, },
components: { 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: { methods: {
@ -57,6 +84,9 @@ export default {
save() { save() {
this.$emit("save"); this.$emit("save");
}, },
updateScroll() {
this.scrollPosition = window.scrollY;
},
deleteRecipeConfrim() { deleteRecipeConfrim() {
this.$refs.deleteRecipieConfirm.open(); this.$refs.deleteRecipieConfirm.open();

View File

@ -1,23 +1,39 @@
<template> <template>
<v-card <v-card
class="mx-auto"
hover hover
:to="`/recipe/${slug}`" :to="`/recipe/${slug}`"
max-height="125"
@click="$emit('selected')" @click="$emit('selected')"
> >
<v-list-item> <v-list-item three-line>
<v-list-item-avatar rounded size="125" class="mt-0 ml-n4"> <v-list-item-avatar
<v-img :src="getImage(slug)"> </v-img> tile
</v-list-item-avatar> size="125"
<v-list-item-content class="align-self-start"> color="grey"
<v-list-item-title> class="v-mobile-img rounded-sm my-0 ml-n4"
{{ name }} >
</v-list-item-title> <v-img :src="getImage(slug)" lazy-src=""></v-img
<v-rating length="5" size="16" dense :value="rating"></v-rating> ></v-list-item-avatar>
<div class="text"> <v-list-item-content>
<v-list-item-action-text> <v-list-item-title class=" mb-1">{{ name }}</v-list-item-title>
{{ description | truncate(115) }} <v-list-item-subtitle> {{ description }} </v-list-item-subtitle>
</v-list-item-action-text> <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> </div>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
@ -25,8 +41,12 @@
</template> </template>
<script> <script>
import RecipeChips from "@/components/Recipe/RecipeViewer/RecipeChips";
import { api } from "@/api"; import { api } from "@/api";
export default { export default {
components: {
RecipeChips,
},
props: { props: {
name: String, name: String,
slug: String, slug: String,
@ -36,6 +56,9 @@ export default {
route: { route: {
default: true, default: true,
}, },
tags: {
default: true,
},
}, },
methods: { methods: {
@ -47,6 +70,11 @@ export default {
</script> </script>
<style> <style>
.v-mobile-img {
padding-top: 0;
padding-bottom: 0;
padding-left: 0;
}
.v-card--reveal { .v-card--reveal {
align-items: center; align-items: center;
bottom: 0; bottom: 0;

View File

@ -11,7 +11,7 @@
<div> <div>
Recipe Image Recipe Image
</div> </div>
<UploadBtn <TheUploadBtn
class="ml-auto" class="ml-auto"
url="none" url="none"
file-name="image" file-name="image"
@ -44,12 +44,12 @@
<script> <script>
const REFRESH_EVENT = "refresh"; const REFRESH_EVENT = "refresh";
const UPLOAD_EVENT = "upload"; const UPLOAD_EVENT = "upload";
import UploadBtn from "@/components/UI/UploadBtn"; import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
import { api } from "@/api"; import { api } from "@/api";
// import axios from "axios"; // import axios from "axios";
export default { export default {
components: { components: {
UploadBtn, TheUploadBtn,
}, },
props: { props: {
slug: String, slug: String,

View File

@ -33,7 +33,7 @@
class="my-3" class="my-3"
:label="$t('recipe.recipe-name')" :label="$t('recipe.recipe-name')"
v-model="value.name" v-model="value.name"
:rules="[rules.required]" :rules="[existsRule]"
> >
</v-text-field> </v-text-field>
<v-textarea <v-textarea
@ -94,7 +94,7 @@
class="mr-n1" class="mr-n1"
slot="prepend" slot="prepend"
color="error" color="error"
@click="removeIngredient(index)" @click="removeByIndex(value.recipeIngredient, index)"
> >
mdi-delete mdi-delete
</v-icon> </v-icon>
@ -107,7 +107,7 @@
<v-btn color="secondary" fab dark small @click="addIngredient"> <v-btn color="secondary" fab dark small @click="addIngredient">
<v-icon>mdi-plus</v-icon> <v-icon>mdi-plus</v-icon>
</v-btn> </v-btn>
<BulkAdd @bulk-data="appendIngredients" /> <BulkAdd @bulk-data="addIngredient" />
<h2 class="mt-6">{{ $t("recipe.categories") }}</h2> <h2 class="mt-6">{{ $t("recipe.categories") }}</h2>
<CategoryTagSelector <CategoryTagSelector
@ -140,7 +140,7 @@
color="white" color="white"
class="mr-2" class="mr-2"
elevation="0" elevation="0"
@click="removeNote(index)" @click="removeByIndex(value.notes, index)"
> >
<v-icon color="error">mdi-delete</v-icon> <v-icon color="error">mdi-delete</v-icon>
</v-btn> </v-btn>
@ -183,7 +183,7 @@
color="white" color="white"
class="mr-2" class="mr-2"
elevation="0" elevation="0"
@click="removeStep(index)" @click="removeByIndex(value.recipeInstructions, index)"
> >
<v-icon size="24" color="error">mdi-delete</v-icon> <v-icon size="24" color="error">mdi-delete</v-icon>
</v-btn> </v-btn>
@ -218,6 +218,7 @@
</template> </template>
<script> <script>
const UPLOAD_EVENT = "upload";
import draggable from "vuedraggable"; import draggable from "vuedraggable";
import utils from "@/utils"; import utils from "@/utils";
import BulkAdd from "./BulkAdd"; import BulkAdd from "./BulkAdd";
@ -225,6 +226,7 @@ import ExtrasEditor from "./ExtrasEditor";
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector"; import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
import NutritionEditor from "./NutritionEditor"; import NutritionEditor from "./NutritionEditor";
import ImageUploadBtn from "./ImageUploadBtn.vue"; import ImageUploadBtn from "./ImageUploadBtn.vue";
import { validators } from "@/mixins/validators";
export default { export default {
components: { components: {
BulkAdd, BulkAdd,
@ -237,26 +239,20 @@ export default {
props: { props: {
value: Object, value: Object,
}, },
mixins: [validators],
data() { data() {
return { return {
drag: false, drag: false,
fileObject: null, 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: { methods: {
uploadImage(fileObject) { uploadImage(fileObject) {
this.$emit("upload", fileObject); this.$emit(UPLOAD_EVENT, fileObject);
}, },
toggleDisabled(stepIndex) { toggleDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex)) { if (this.disabledSteps.includes(stepIndex)) {
let index = this.disabledSteps.indexOf(stepIndex); const index = this.disabledSteps.indexOf(stepIndex);
if (index !== -1) { if (index !== -1) {
this.disabledSteps.splice(index, 1); this.disabledSteps.splice(index, 1);
} }
@ -265,66 +261,40 @@ export default {
} }
}, },
isDisabled(stepIndex) { isDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex)) { return this.disabledSteps.includes(stepIndex) ? "disabled-card" : null;
return "disabled-card";
} else {
return;
}
}, },
generateKey(item, index) { generateKey(item, index) {
return utils.generateUniqueKey(item, index); return utils.generateUniqueKey(item, index);
}, },
addIngredient(ingredients = null) {
appendIngredients(ingredients) { if (ingredients) {
this.value.recipeIngredient.push(...ingredients); this.value.recipeIngredient.push(...ingredients);
}, } else {
addIngredient() { this.value.recipeIngredient.push("");
let list = this.value.recipeIngredient; }
list.push("");
},
removeIngredient(index) {
this.value.recipeIngredient.splice(index, 1);
}, },
appendSteps(steps) { appendSteps(steps) {
let processSteps = []; this.value.recipeInstructions.push(
steps.forEach(element => { ...steps.map(x => ({
processSteps.push({ text: element }); text: x,
}); }))
);
this.value.recipeInstructions.push(...processSteps);
}, },
addStep() { addStep() {
let list = this.value.recipeInstructions; this.value.recipeInstructions.push({ text: "" });
list.push({ text: "" });
}, },
removeStep(index) {
this.value.recipeInstructions.splice(index, 1);
},
addNote() { addNote() {
let list = this.value.notes; this.value.notes.push({ text: "" });
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);
}, },
saveExtras(extras) { saveExtras(extras) {
this.value.extras = extras; this.value.extras = extras;
}, },
removeByIndex(list, index) {
list.splice(index, 1);
},
validateRecipe() { validateRecipe() {
if (this.$refs.form.validate()) { return this.$refs.form.validate();
return true;
} else {
return false;
}
}, },
}, },
}; };

View File

@ -1,57 +1,26 @@
<template> <template>
<v-card <v-card
color="accent" 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 tile
:width="`${timeCardWidth}`" v-if="showCards"
height="55"
v-if="totalTime || prepTime || performTime"
> >
<v-card flat color="rgb(255, 0, 0, 0.0)"> <v-card flat color="rgb(255, 0, 0, 0.0)">
<v-icon large color="white" class="mx-2"> mdi-clock-outline </v-icon> <v-icon large color="white" class="mx-2"> mdi-clock-outline </v-icon>
</v-card> </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-card
v-for="(time, index) in allTimes"
:key="index"
class="d-flex justify-start align-center text-center time-card-flex"
flat flat
color="rgb(255, 0, 0, 0.0)" 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> <div>
<strong> {{ $t("recipe.prep-time") }} </strong> <strong> {{ time.name }} </strong>
</div> </div>
<div>{{ prepTime }}</div> <div>{{ time.value }}</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>
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-card> </v-card>
@ -64,52 +33,52 @@ export default {
totalTime: String, totalTime: String,
performTime: String, performTime: String,
}, },
watch: {
showCards(val) {
console.log(val);
},
},
computed: { computed: {
timeLength() { showCards() {
let times = []; return [this.prepTime, this.totalTime, this.performTime].some(
let timeArray = [this.totalTime, this.prepTime, this.performTime]; x => !this.isEmpty(x)
timeArray.forEach(element => { );
if (element) {
times.push(element);
}
});
return times.length;
}, },
iconColumn() { allTimes() {
switch (this.timeLength) { return [
case 0: this.validateTotalTime,
return null; this.validatePrepTime,
case 1: this.validatePerformTime,
return 4; ].filter(x => x !== null);
case 2:
return 3;
case 3:
return 2;
default:
return 1;
}
}, },
timeCardWidth() { validateTotalTime() {
let timeArray = [this.totalTime, this.prepTime, this.performTime]; return !this.isEmpty(this.totalTime)
let width = 80; ? { name: this.$t("recipe.total-time"), value: this.totalTime }
timeArray.forEach(element => { : null;
if (element) { },
width += 95; validatePrepTime() {
} return !this.isEmpty(this.prepTime)
}); ? { name: this.$t("recipe.prep-time"), value: this.prepTime }
: null;
if (this.$vuetify.breakpoint.name === "xs") { },
return "100%"; validatePerformTime() {
} return !this.isEmpty(this.performTime)
? { name: this.$t("recipe.perform-time"), value: this.performTime }
return `${width}px`; : null;
},
},
methods: {
isEmpty(str) {
return !str || str.length === 0;
}, },
}, },
}; };
</script> </script>
<style scoped> <style scoped>
.time-card-flex {
width: fit-content;
}
.custom-transparent { .custom-transparent {
opacity: 0.7; opacity: 0.7;
} }

View File

@ -73,6 +73,7 @@
:slug="recipe.slug" :slug="recipe.slug"
:rating="recipe.rating" :rating="recipe.rating"
:image="recipe.image" :image="recipe.image"
:tags="recipe.tags"
/> />
</v-col> </v-col>
</v-row> </v-row>

View File

@ -8,7 +8,7 @@
@keydown.esc="cancel" @keydown.esc="cancel"
> >
<v-card> <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-icon v-if="Boolean(icon)" left> {{ icon }}</v-icon>
<v-toolbar-title v-text="title" /> <v-toolbar-title v-text="title" />
</v-app-bar> </v-app-bar>
@ -36,13 +36,13 @@
const CLOSE_EVENT = "close"; const CLOSE_EVENT = "close";
const OPEN_EVENT = "open"; 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 * @version 1.0.1
* @author [zackbcom](https://github.com/zackbcom) * @author [zackbcom](https://github.com/zackbcom)
* @since Version 1.0.0 * @since Version 1.0.0
*/ */
export default { export default {
name: "Confirmation", name: "ConfirmationDialog",
props: { props: {
/** /**
* Message to be in body. * Message to be in body.

View File

@ -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>

View File

@ -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>

View File

@ -35,7 +35,7 @@
<v-icon>mdi-magnify</v-icon> <v-icon>mdi-magnify</v-icon>
</v-btn> </v-btn>
<SiteMenu /> <TheSiteMenu />
</v-app-bar> </v-app-bar>
<v-app-bar <v-app-bar
v-else v-else
@ -67,13 +67,13 @@
<v-icon>mdi-magnify</v-icon> <v-icon>mdi-magnify</v-icon>
</v-btn> </v-btn>
<SiteMenu /> <TheSiteMenu />
</v-app-bar> </v-app-bar>
</div> </div>
</template> </template>
<script> <script>
import SiteMenu from "@/components/UI/SiteMenu"; import TheSiteMenu from "@/components/UI/TheSiteMenu";
import SearchBar from "@/components/UI/Search/SearchBar"; import SearchBar from "@/components/UI/Search/SearchBar";
import SearchDialog from "@/components/UI/Search/SearchDialog"; import SearchDialog from "@/components/UI/Search/SearchDialog";
import { user } from "@/mixins/user"; import { user } from "@/mixins/user";
@ -82,7 +82,7 @@ export default {
mixins: [user], mixins: [user],
components: { components: {
SiteMenu, TheSiteMenu,
SearchBar, SearchBar,
SearchDialog, SearchDialog,
}, },

View File

@ -3,7 +3,7 @@ import App from "./App.vue";
import vuetify from "./plugins/vuetify"; import vuetify from "./plugins/vuetify";
import store from "./store"; import store from "./store";
import VueRouter from "vue-router"; import VueRouter from "vue-router";
import { routes } from "./routes"; import { router } from "./routes";
import i18n from "./i18n"; import i18n from "./i18n";
import FlashMessage from "@smartweb/vue-flash-message"; import FlashMessage from "@smartweb/vue-flash-message";
import "@mdi/font/css/materialdesignicons.css"; import "@mdi/font/css/materialdesignicons.css";
@ -13,25 +13,6 @@ Vue.use(FlashMessage);
Vue.config.productionTip = false; Vue.config.productionTip = false;
Vue.use(VueRouter); 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({ const vueApp = new Vue({
vuetify, vuetify,
store, store,
@ -56,5 +37,4 @@ let titleCase = function(value) {
Vue.filter("truncate", truncate); Vue.filter("truncate", truncate);
Vue.filter("titleCase", titleCase); Vue.filter("titleCase", titleCase);
export { vueApp }; export { router, vueApp };
export { router };

View File

@ -4,12 +4,18 @@ export const validators = {
emailRule: v => emailRule: v =>
!v || !v ||
/^[^@\s]+@[^@\s.]+.[^@.\s]+$/.test(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 => 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"),
}; };
}, },
}; };

View File

@ -38,7 +38,7 @@
<script> <script>
import { api } from "@/api"; import { api } from "@/api";
import TheDownloadBtn from "@/components/UI/TheDownloadBtn"; import TheDownloadBtn from "@/components/UI/Buttons/TheDownloadBtn";
export default { export default {
components: { TheDownloadBtn }, components: { TheDownloadBtn },
data() { data() {

View File

@ -58,8 +58,8 @@
<script> <script>
import ImportOptions from "@/components/Admin/Backup/ImportOptions"; import ImportOptions from "./ImportOptions";
import TheDownloadBtn from "@/components/UI/TheDownloadBtn.vue"; import TheDownloadBtn from "@/components/UI/Buttons/TheDownloadBtn.vue";
import { backupURLs } from "@/api/backup"; import { backupURLs } from "@/api/backup";
export default { export default {
components: { ImportOptions, TheDownloadBtn }, components: { ImportOptions, TheDownloadBtn },

View File

@ -42,7 +42,7 @@
</template> </template>
<script> <script>
import ImportOptions from "@/components/Admin/Backup/ImportOptions"; import ImportOptions from "./ImportOptions";
import { api } from "@/api"; import { api } from "@/api";
export default { export default {
components: { ImportOptions }, components: { ImportOptions },

View File

@ -20,7 +20,7 @@
<v-card-title class="mt-n6"> <v-card-title class="mt-n6">
{{ $t("settings.available-backups") }} {{ $t("settings.available-backups") }}
<span> <span>
<UploadBtn <TheUploadBtn
class="mt-1" class="mt-1"
url="/api/backups/upload" url="/api/backups/upload"
@uploaded="getAvailableBackups" @uploaded="getAvailableBackups"
@ -33,14 +33,7 @@
@finished="processFinished" @finished="processFinished"
:backups="availableBackups" :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" /> <ImportSummaryDialog ref="report" :import-data="importData" />
</v-card-text> </v-card-text>
</v-card> </v-card>
@ -48,16 +41,14 @@
<script> <script>
import { api } from "@/api"; import { api } from "@/api";
import SuccessFailureAlert from "@/components/UI/SuccessFailureAlert"; import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
import ImportSummaryDialog from "@/components/Admin/Backup/ImportSummaryDialog"; import ImportSummaryDialog from "@/components/ImportSummaryDialog";
import UploadBtn from "@/components/UI/UploadBtn"; import AvailableBackupCard from "@/pages/Admin/Backup/AvailableBackupCard";
import AvailableBackupCard from "@/components/Admin/Backup/AvailableBackupCard"; import NewBackupCard from "@/pages/Admin/Backup/NewBackupCard";
import NewBackupCard from "@/components/Admin/Backup/NewBackupCard";
export default { export default {
components: { components: {
SuccessFailureAlert, TheUploadBtn,
UploadBtn,
AvailableBackupCard, AvailableBackupCard,
NewBackupCard, NewBackupCard,
ImportSummaryDialog, ImportSummaryDialog,

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<Confirmation <ConfirmationDialog
ref="deleteGroupConfirm" ref="deleteGroupConfirm"
:title="$t('user.confirm-group-deletion')" :title="$t('user.confirm-group-deletion')"
:message=" :message="
@ -55,10 +55,10 @@
<script> <script>
const RENDER_EVENT = "update"; const RENDER_EVENT = "update";
import Confirmation from "@/components/UI/Confirmation"; import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog";
import { api } from "@/api"; import { api } from "@/api";
export default { export default {
components: { Confirmation }, components: { ConfirmationDialog },
props: { props: {
group: { group: {
default: { default: {

View File

@ -85,7 +85,7 @@
<script> <script>
import { validators } from "@/mixins/validators"; import { validators } from "@/mixins/validators";
import { api } from "@/api"; import { api } from "@/api";
import GroupCard from "@/components/Admin/ManageUsers/GroupCard"; import GroupCard from "./GroupCard";
export default { export default {
components: { GroupCard }, components: { GroupCard },
mixins: [validators], mixins: [validators],

View File

@ -1,6 +1,6 @@
<template> <template>
<v-card outlined class="mt-n1"> <v-card outlined class="mt-n1">
<Confirmation <ConfirmationDialog
ref="deleteUserDialog" ref="deleteUserDialog"
:title="$t('user.confirm-link-deletion')" :title="$t('user.confirm-link-deletion')"
:message=" :message="
@ -107,11 +107,11 @@
</template> </template>
<script> <script>
import Confirmation from "@/components/UI/Confirmation"; import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog";
import { api } from "@/api"; import { api } from "@/api";
import { validators } from "@/mixins/validators"; import { validators } from "@/mixins/validators";
export default { export default {
components: { Confirmation }, components: { ConfirmationDialog },
mixins: [validators], mixins: [validators],
data() { data() {
return { return {

View File

@ -1,6 +1,6 @@
<template> <template>
<v-card outlined class="mt-n1"> <v-card outlined class="mt-n1">
<Confirmation <ConfirmationDialog
ref="deleteUserDialog" ref="deleteUserDialog"
:title="$t('user.confirm-user-deletion')" :title="$t('user.confirm-user-deletion')"
:message=" :message="
@ -144,11 +144,11 @@
</template> </template>
<script> <script>
import Confirmation from "@/components/UI/Confirmation"; import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog";
import { api } from "@/api"; import { api } from "@/api";
import { validators } from "@/mixins/validators"; import { validators } from "@/mixins/validators";
export default { export default {
components: { Confirmation }, components: { ConfirmationDialog },
mixins: [validators], mixins: [validators],
data() { data() {
return { return {

View File

@ -11,17 +11,17 @@
<v-tabs-slider></v-tabs-slider> <v-tabs-slider></v-tabs-slider>
<v-tab> <v-tab>
{{$t('user.users')}} {{ $t("user.users") }}
<v-icon>mdi-account</v-icon> <v-icon>mdi-account</v-icon>
</v-tab> </v-tab>
<v-tab> <v-tab>
{{$t('user.sign-up-links')}} {{ $t("user.sign-up-links") }}
<v-icon>mdi-account-plus-outline</v-icon> <v-icon>mdi-account-plus-outline</v-icon>
</v-tab> </v-tab>
<v-tab> <v-tab>
{{$t('user.groups')}} {{ $t("user.groups") }}
<v-icon>mdi-account-group</v-icon> <v-icon>mdi-account-group</v-icon>
</v-tab> </v-tab>
</v-tabs> </v-tabs>
@ -42,9 +42,9 @@
</template> </template>
<script> <script>
import TheUserTable from "@/components/Admin/ManageUsers/TheUserTable"; import TheUserTable from "./TheUserTable";
import GroupDashboard from "@/components/Admin/ManageUsers/GroupDashboard"; import GroupDashboard from "./GroupDashboard";
import TheSignUpTable from "@/components/Admin/ManageUsers/TheSignUpTable"; import TheSignUpTable from "./TheSignUpTable";
export default { export default {
components: { TheUserTable, GroupDashboard, TheSignUpTable }, components: { TheUserTable, GroupDashboard, TheSignUpTable },
data() { data() {

View File

@ -82,7 +82,7 @@
<script> <script>
import { api } from "@/api"; import { api } from "@/api";
import TimePickerDialog from "@/components/Admin/MealPlanner/TimePickerDialog"; import TimePickerDialog from "@/components/FormHelpers/TimePickerDialog";
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector"; import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
export default { export default {
components: { components: {

View File

@ -5,7 +5,7 @@
{{ title }} {{ title }}
<v-spacer></v-spacer> <v-spacer></v-spacer>
<span> <span>
<UploadBtn <TheUploadBtn
class="mt-1" class="mt-1"
:url="`/api/migrations/${folder}/upload`" :url="`/api/migrations/${folder}/upload`"
fileName="archive" fileName="archive"
@ -66,10 +66,10 @@
</template> </template>
<script> <script>
import UploadBtn from "../../UI/UploadBtn"; import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
import utils from "@/utils"; import utils from "@/utils";
import { api } from "@/api"; import { api } from "@/api";
import MigrationDialog from "@/components/Admin/Migration/MigrationDialog.vue"; import MigrationDialog from "./MigrationDialog";
export default { export default {
props: { props: {
folder: String, folder: String,
@ -78,7 +78,7 @@ export default {
available: Array, available: Array,
}, },
components: { components: {
UploadBtn, TheUploadBtn,
MigrationDialog, MigrationDialog,
}, },
data() { data() {

View File

@ -42,7 +42,7 @@
</template> </template>
<script> <script>
import DataTable from "@/components/Admin/Backup/ImportSummaryDialog/DataTable"; import DataTable from "@/components/ImportSummaryDialog";
export default { export default {
components: { components: {
DataTable, DataTable,

View File

@ -1,13 +1,5 @@
<template> <template>
<div> <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 :loading="loading">
<v-card-title class="headline"> <v-card-title class="headline">
{{ $t("migration.recipe-migration") }} {{ $t("migration.recipe-migration") }}
@ -42,13 +34,11 @@
<script> <script>
import MigrationCard from "@/components/Admin/Migration/MigrationCard"; import MigrationCard from "./MigrationCard";
import SuccessFailureAlert from "@/components/UI/SuccessFailureAlert";
import { api } from "@/api"; import { api } from "@/api";
export default { export default {
components: { components: {
MigrationCard, MigrationCard,
SuccessFailureAlert,
}, },
data() { data() {
return { return {

View File

@ -68,7 +68,7 @@
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<UploadBtn <TheUploadBtn
icon="mdi-image-area" icon="mdi-image-area"
:text="$t('user.upload-photo')" :text="$t('user.upload-photo')"
:url="userProfileImage" :url="userProfileImage"
@ -145,13 +145,13 @@
<script> <script>
// import AvatarPicker from '@/components/AvatarPicker' // import AvatarPicker from '@/components/AvatarPicker'
import UploadBtn from "@/components/UI/UploadBtn"; import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
import { api } from "@/api"; import { api } from "@/api";
import { validators } from "@/mixins/validators"; import { validators } from "@/mixins/validators";
import { initials } from "@/mixins/initials"; import { initials } from "@/mixins/initials";
export default { export default {
components: { components: {
UploadBtn, TheUploadBtn,
}, },
mixins: [validators, initials], mixins: [validators, initials],
data() { data() {

View File

@ -63,7 +63,7 @@
<script> <script>
import draggable from "vuedraggable"; import draggable from "vuedraggable";
import CreatePageDialog from "@/components/Admin/General/CreatePageDialog"; import CreatePageDialog from "./CreatePageDialog";
import { api } from "@/api"; import { api } from "@/api";
export default { export default {
components: { components: {

View File

@ -117,21 +117,21 @@
</v-row> </v-row>
</v-card-text> </v-card-text>
<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-row>
<v-col cols="1"> <v-col cols="12" md="3" sm="12">
<LanguageMenu @select-lang="writeLang" :site-settings="true" /> <LanguageSelector @select-lang="writeLang" :site-settings="true" />
</v-col> </v-col>
<v-col sm="3"> <v-col cols="12" md="3" sm="12">
<v-select <v-select
dense dense
prepend-icon="mdi-calendar-week-begin" prepend-icon="mdi-calendar-week-begin"
v-model="settings.firstDayOfWeek" v-model="settings.firstDayOfWeek"
:items="allDays" :items="allDays"
item-text="name" item-text="name"
item-value="value" item-value="value"
:label="$t('settings.first-day-of-week')" :label="$t('settings.first-day-of-week')"
/> />
</v-col> </v-col>
</v-row> </v-row>
</v-card-text> </v-card-text>
@ -147,14 +147,14 @@
<script> <script>
import { api } from "@/api"; import { api } from "@/api";
import LanguageMenu from "@/components/UI/LanguageMenu"; import LanguageSelector from "@/components/FormHelpers/LanguageSelector";
import draggable from "vuedraggable"; import draggable from "vuedraggable";
import NewCategoryTagDialog from "@/components/UI/Dialogs/NewCategoryTagDialog.vue"; import NewCategoryTagDialog from "@/components/UI/Dialogs/NewCategoryTagDialog.vue";
export default { export default {
components: { components: {
draggable, draggable,
LanguageMenu, LanguageSelector,
NewCategoryTagDialog, NewCategoryTagDialog,
}, },
data() { data() {
@ -178,33 +178,33 @@ export default {
allDays() { allDays() {
return [ return [
{ {
name: this.$t('general.sunday'), name: this.$t("general.sunday"),
value: 0, value: 0,
}, },
{ {
name: this.$t('general.monday'), name: this.$t("general.monday"),
value: 1, value: 1,
}, },
{ {
name: this.$t('general.tuesday'), name: this.$t("general.tuesday"),
value: 2, value: 2,
}, },
{ {
name: this.$t('general.wednesday'), name: this.$t("general.wednesday"),
value: 3, value: 3,
}, },
{ {
name: this.$t('general.thursday'), name: this.$t("general.thursday"),
value: 4, value: 4,
}, },
{ {
name: this.$t('general.friday'), name: this.$t("general.friday"),
value: 5, value: 5,
}, },
{ {
name: this.$t('general.saturday'), name: this.$t("general.saturday"),
value: 6, value: 6,
} },
]; ];
}, },
}, },
@ -223,10 +223,8 @@ export default {
this.settings.categories.splice(index, 1); this.settings.categories.splice(index, 1);
}, },
async saveSettings() { async saveSettings() {
await api.siteSettings.update(this.settings); const newSettings = await api.siteSettings.update(this.settings);
this.$store.dispatch("setLang", { console.log("New Settings", newSettings);
currentVueComponent: this,
language: this.settings.language });
this.getOptions(); this.getOptions();
}, },
}, },

View File

@ -20,8 +20,8 @@
</template> </template>
<script> <script>
import HomePageSettings from "@/components/Admin/General/HomePageSettings"; import HomePageSettings from "./HomePageSettings";
import CustomPageCreator from "@/components/Admin/General/CustomPageCreator"; import CustomPageCreator from "./CustomPageCreator";
export default { export default {
components: { components: {

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<Confirmation <ConfirmationDialog
:title="$t('settings.theme.delete-theme')" :title="$t('settings.theme.delete-theme')"
:message="$t('settings.theme.are-you-sure-you-want-to-delete-this-theme')" :message="$t('settings.theme.are-you-sure-you-want-to-delete-this-theme')"
color="error" color="error"
@ -44,7 +44,7 @@
</template> </template>
<script> <script>
import Confirmation from "@/components/UI/Confirmation"; import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog";
import { api } from "@/api"; import { api } from "@/api";
const DELETE_EVENT = "delete"; const DELETE_EVENT = "delete";
@ -52,7 +52,7 @@ const APPLY_EVENT = "apply";
const EDIT_EVENT = "edit"; const EDIT_EVENT = "edit";
export default { export default {
components: { components: {
Confirmation, ConfirmationDialog,
}, },
props: { props: {
theme: Object, theme: Object,

View File

@ -135,9 +135,9 @@
<script> <script>
import { api } from "@/api"; import { api } from "@/api";
import ColorPickerDialog from "@/components/Admin/Theme/ColorPickerDialog"; import ColorPickerDialog from "@/components/FormHelpers/ColorPickerDialog";
import NewThemeDialog from "@/components/Admin/Theme/NewThemeDialog"; import NewThemeDialog from "./NewThemeDialog";
import ThemeCard from "@/components/Admin/Theme/ThemeCard"; import ThemeCard from "./ThemeCard";
export default { export default {
components: { components: {

View File

@ -10,7 +10,7 @@ import About from "@/pages/Admin/About";
import { store } from "../store"; import { store } from "../store";
import i18n from '@/i18n.js'; import i18n from '@/i18n.js';
export default { export const adminRoutes = {
path: "/admin", path: "/admin",
component: Admin, component: Admin,
beforeEnter: (to, _from, next) => { beforeEnter: (to, _from, next) => {

View 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 },
];

View 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"),
},
},
];

View File

@ -1,87 +1,54 @@
import HomePage from "@/pages/HomePage";
import Page404 from "@/pages/404Page"; import Page404 from "@/pages/404Page";
import SearchPage from "@/pages/SearchPage"; import { adminRoutes } from "./admin";
import ViewRecipe from "@/pages/Recipe/ViewRecipe"; import { authRoutes } from "./auth";
import NewRecipe from "@/pages/Recipe/NewRecipe"; import { recipeRoutes } from "./recipes";
import CustomPage from "@/pages/Recipes/CustomPage"; import { mealRoutes } from "./meal";
import AllRecipes from "@/pages/Recipes/AllRecipes"; import { generalRoutes } from "./general";
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 { store } from "../store"; 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 = [ export const routes = [
{ path: "/", name: "home", component: HomePage }, ...generalRoutes,
{ adminRoutes,
path: "/logout", ...authRoutes,
beforeEnter: (_to, _from, next) => { ...mealRoutes,
store.commit("setToken", ""); ...recipeRoutes,
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'),
}
},
Admin,
{
path: "/meal-plan/today",
beforeEnter: async (_to, _from, next) => {
await todaysMealRoute().then(redirect => {
next(redirect);
});
},
},
{ path: "*", component: Page404 }, { path: "*", component: Page404 },
]; ];
async function todaysMealRoute() { const router = new VueRouter({
const response = await api.mealPlans.today(); routes,
return "/recipe/" + response.data; 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 };

View 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;
}

View 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;
},
},
},
];

View File

@ -12,7 +12,7 @@ Vue.use(Vuex);
const store = new Vuex.Store({ const store = new Vuex.Store({
plugins: [ plugins: [
createPersistedState({ createPersistedState({
paths: ["userSettings", "language.lang", "siteSettings"], paths: ["userSettings", "siteSettings"],
}), }),
], ],
modules: { modules: {

View File

@ -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 = { const state = {
lang: "en-US",
allLangs: [ allLangs: [
{ {
name: "English", 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 = { const getters = {
getActiveLang: state => state.lang,
getAllLangs: state => state.allLangs, getAllLangs: state => state.allLangs,
}; };
export default { export default {
state, state,
mutations,
actions,
getters, getters,
}; };

View File

@ -1,8 +1,10 @@
import { api } from "@/api"; import { api } from "@/api";
import VueI18n from "@/i18n";
import Vuetify from "@/plugins/vuetify";
const state = { const state = {
siteSettings: { siteSettings: {
language: "en", language: "en-US",
firstDayOfWeek: 0, firstDayOfWeek: 0,
showRecent: true, showRecent: true,
cardsPerSection: 9, cardsPerSection: 9,
@ -13,17 +15,20 @@ const state = {
const mutations = { const mutations = {
setSettings(state, payload) { setSettings(state, payload) {
state.siteSettings = payload; state.siteSettings = payload;
VueI18n.locale = payload.language;
Vuetify.framework.lang.current = payload.language;
}, },
}; };
const actions = { const actions = {
async requestSiteSettings() { async requestSiteSettings({ commit }) {
let settings = await api.siteSettings.get(); let settings = await api.siteSettings.get();
this.commit("setSettings", settings); commit("setSettings", settings);
}, },
}; };
const getters = { const getters = {
getActiveLang: state => state.siteSettings.language,
getSiteSettings: state => state.siteSettings, getSiteSettings: state => state.siteSettings,
}; };

View File

@ -1,5 +1,5 @@
import { api } from "@/api"; import { api } from "@/api";
import Vuetify from "../../plugins/vuetify"; import Vuetify from "@/plugins/vuetify";
import axios from "axios"; import axios from "axios";
function inDarkMode(payload) { function inDarkMode(payload) {