mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-11-27 22:24:43 -05: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:
@@ -1,42 +1,51 @@
|
||||
<template>
|
||||
<v-toolbar class="card-btn" flat height="0" extension-height="0">
|
||||
<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-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">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</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) {
|
||||
this.value.recipeIngredient.push(...ingredients);
|
||||
},
|
||||
addIngredient() {
|
||||
let list = this.value.recipeIngredient;
|
||||
list.push("");
|
||||
},
|
||||
|
||||
removeIngredient(index) {
|
||||
this.value.recipeIngredient.splice(index, 1);
|
||||
addIngredient(ingredients = null) {
|
||||
if (ingredients) {
|
||||
this.value.recipeIngredient.push(...ingredients);
|
||||
} 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user