reorganize all frontend items

This commit is contained in:
hay-kot
2021-08-01 19:24:47 -08:00
parent d67240d449
commit 00a8fdda41
147 changed files with 3845 additions and 743 deletions

View File

@@ -0,0 +1,173 @@
<template>
<v-row>
<SearchDialog ref="mealselect" @selected="setSlug" />
<BaseDialog
ref="customMealDialog"
title="Custom Meal"
:title-icon="$globals.icons.primary"
:submit-text="$t('general.save')"
:top="true"
@submit="pushCustomMeal"
>
<v-card-text>
<v-text-field v-model="customMeal.name" autofocus :label="$t('general.name')"> </v-text-field>
<v-textarea v-model="customMeal.description" :label="$t('recipe.description')"> </v-textarea>
</v-card-text>
</BaseDialog>
<v-col v-for="(planDay, index) in value" :key="index" cols="12" sm="12" md="6" lg="4" xl="3">
<v-hover v-slot="{ hover }" :open-delay="50">
<v-card :class="{ 'on-hover': hover }" :elevation="hover ? 12 : 2">
<CardImage large :slug="planDay.meals[0].slug" icon-size="200" @click="openSearch(index, modes.primary)">
<div>
<v-fade-transition>
<v-btn v-if="hover" small color="info" class="ma-1" @click.stop="addCustomItem(index, modes.primary)">
<v-icon left>
{{ $globals.icons.edit }}
</v-icon>
{{ $t("reicpe.no-recipe") }}
</v-btn>
</v-fade-transition>
</div>
</CardImage>
<v-card-title class="my-n3 mb-n6">
{{ $d(new Date(planDay.date.replaceAll("-", "/")), "short") }}
</v-card-title>
<v-card-subtitle class="mb-0 pb-0"> {{ planDay.meals[0].name }}</v-card-subtitle>
<v-hover v-slot="{ hover }">
<v-card-actions>
<v-spacer></v-spacer>
<v-fade-transition>
<v-btn v-if="hover" small color="info" text @click.stop="addCustomItem(index, modes.sides)">
<v-icon left>
{{ $globals.icons.edit }}
</v-icon>
{{ $t("reicpe.no-recipe") }}
</v-btn>
</v-fade-transition>
<v-btn color="info" outlined small @click="openSearch(index, modes.sides)">
<v-icon small class="mr-1">
{{ $globals.icons.create }}
</v-icon>
{{ $t("meal-plan.side") }}
</v-btn>
</v-card-actions>
</v-hover>
<v-divider class="mx-2"></v-divider>
<v-list dense>
<v-list-item v-for="(recipe, i) in planDay.meals.slice(1)" :key="i">
<v-list-item-avatar color="accent">
<v-img :alt="recipe.slug" :src="getImage(recipe.slug)"></v-img>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title v-text="recipe.name"></v-list-item-title>
</v-list-item-content>
<v-list-item-icon>
<v-btn icon @click="removeSide(index, i + 1)">
<v-icon color="error">
{{ $globals.icons.delete }}
</v-icon>
</v-btn>
</v-list-item-icon>
</v-list-item>
</v-list>
</v-card>
</v-hover>
</v-col>
</v-row>
</template>
<script>
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
import { api } from "@/api";
import SearchDialog from "../UI/Dialogs/SearchDialog";
import CardImage from "../Recipe/CardImage.vue";
export default {
components: {
SearchDialog,
CardImage,
BaseDialog,
},
props: {
value: Array,
},
data() {
return {
activeIndex: 0,
mode: "PRIMARY",
modes: {
primary: "PRIMARY",
sides: "SIDES",
},
customMeal: {
slug: null,
name: "",
description: "",
},
};
},
methods: {
getImage(slug) {
if (slug) {
return api.recipes.recipeSmallImage(slug);
}
},
setSide(name, slug = null, description = "") {
const meal = { name, slug, description };
this.value[this.activeIndex].meals.push(meal);
},
setPrimary(name, slug, description = "") {
this.value[this.activeIndex].meals[0].slug = slug;
this.value[this.activeIndex].meals[0].name = name;
this.value[this.activeIndex].meals[0].description = description;
},
setSlug(recipe) {
switch (this.mode) {
case this.modes.primary:
this.setPrimary(recipe.name, recipe.slug);
break;
default:
this.setSide(recipe.name, recipe.slug);
break;
}
},
openSearch(index, mode) {
this.mode = mode;
this.activeIndex = index;
this.$refs.mealselect.open();
},
removeSide(dayIndex, sideIndex) {
this.value[dayIndex].meals.splice(sideIndex, 1);
},
addCustomItem(index, mode) {
this.mode = mode;
this.activeIndex = index;
this.$refs.customMealDialog.open();
},
pushCustomMeal() {
switch (this.mode) {
case this.modes.primary:
this.setPrimary(this.customMeal.name, this.customMeal.slug, this.customMeal.description);
break;
default:
this.setSide(this.customMeal.name, this.customMeal.slug, this.customMeal.description);
break;
}
this.customMeal = { name: "", slug: null, description: "" };
},
},
};
</script>
<style>
.relative-card {
position: relative;
}
.custom-button {
z-index: -1;
}
</style>

View File

@@ -0,0 +1,46 @@
<template>
<v-card>
<v-card-title class="headline">
{{ $t("meal-plan.edit-meal-plan") }}
</v-card-title>
<v-divider></v-divider>
<v-card-text>
<MealPlanCard v-model="mealPlan.planDays" />
<v-row align="center" justify="end">
<v-card-actions>
<TheButton update @click="update" />
<v-spacer></v-spacer>
</v-card-actions>
</v-row>
</v-card-text>
</v-card>
</template>
<script>
import { api } from "@/api";
import { utils } from "@/utils";
import MealPlanCard from "./MealPlanCard";
export default {
components: {
MealPlanCard,
},
props: {
mealPlan: Object,
},
methods: {
formatDate(timestamp) {
const dateObject = new Date(timestamp);
return utils.getDateAsPythonDate(dateObject);
},
async update() {
if (await api.mealPlans.update(this.mealPlan.uid, this.mealPlan)) {
this.$emit("updated");
}
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,227 @@
<template>
<v-card>
<v-card-title class=" headline">
{{ $t("meal-plan.create-a-new-meal-plan") }}
<v-btn color="info" class="ml-auto" @click="setQuickWeek()">
<v-icon left> {{ $globals.icons.calendarMinus }} </v-icon>
{{ $t("meal-plan.quick-week") }}
</v-btn>
</v-card-title>
<v-divider></v-divider>
<v-card-text>
<v-row dense>
<v-col cols="12" lg="6" md="6" sm="12">
<v-menu
ref="menu1"
v-model="menu1"
:close-on-content-click="true"
transition="scale-transition"
offset-y
max-width="290px"
min-width="290px"
>
<template #activator="{ on, attrs }">
<v-text-field
v-model="startComputedDateFormatted"
:label="$t('meal-plan.start-date')"
persistent-hint
:prepend-icon="$globals.icons.calendarMinus"
readonly
v-bind="attrs"
v-on="on"
></v-text-field>
</template>
<DatePicker v-model="startDate" no-title @input="menu2 = false" />
</v-menu>
</v-col>
<v-col cols="12" lg="6" md="6" sm="12">
<v-menu
ref="menu2"
v-model="menu2"
:close-on-content-click="true"
transition="scale-transition"
offset-y
max-width="290px"
min-width="290px"
>
<template #activator="{ on, attrs }">
<v-text-field
v-model="endComputedDateFormatted"
:label="$t('meal-plan.end-date')"
persistent-hint
:prepend-icon="$globals.icons.calendarMinus"
readonly
v-bind="attrs"
v-on="on"
></v-text-field>
</template>
<DatePicker v-model="endDate" no-title @input="menu2 = false" />
</v-menu>
</v-col>
</v-row>
</v-card-text>
<v-card-text v-if="startDate">
<MealPlanCard v-model="planDays" />
</v-card-text>
<v-row align="center" justify="end">
<v-card-actions class="mr-5">
<TheButton v-if="planDays.length > 0" edit text @click="random">
<template #icon>
{{ $globals.icons.diceMultiple }}
</template>
{{ $t("general.random") }}
</TheButton>
<TheButton create :disabled="planDays.length == 0" @click="save" />
</v-card-actions>
</v-row>
</v-card>
</template>
<script>
import DatePicker from "@/components/FormHelpers/DatePicker";
import { api } from "@/api";
import { utils } from "@/utils";
import MealPlanCard from "./MealPlanCard";
const CREATE_EVENT = "created";
export default {
components: {
MealPlanCard,
DatePicker,
},
data() {
return {
isLoading: false,
planDays: [],
items: [],
// Dates
startDate: null,
endDate: null,
menu1: false,
menu2: false,
usedRecipes: [1],
};
},
computed: {
groupSettings() {
return this.$store.getters.getCurrentGroup;
},
actualStartDate() {
if (!this.startDate) return null;
return Date.parse(this.startDate.replaceAll("-", "/"));
},
actualEndDate() {
if (!this.endDate) return null;
return Date.parse(this.endDate.replaceAll("-", "/"));
},
dateDif() {
if (!this.actualEndDate || !this.actualStartDate) return null;
const dateDif = (this.actualEndDate - this.actualStartDate) / (1000 * 3600 * 24) + 1;
if (dateDif < 1) {
return null;
}
return dateDif;
},
startComputedDateFormatted() {
return this.formatDate(this.actualStartDate);
},
endComputedDateFormatted() {
return this.formatDate(this.actualEndDate);
},
filteredRecipes() {
const recipes = this.items.filter(x => !this.usedRecipes.includes(x));
return recipes.length > 0 ? recipes : this.items;
},
allRecipes() {
return this.$store.getters.getAllRecipes;
},
},
watch: {
dateDif() {
this.planDays = [];
for (let i = 0; i < this.dateDif; i++) {
this.planDays.push({
date: this.getDate(i),
meals: [
{
name: "",
slug: "empty",
description: "empty",
},
],
});
}
},
},
async created() {
await this.$store.dispatch("requestCurrentGroup");
await this.$store.dispatch("requestAllRecipes");
await this.buildMealStore();
},
methods: {
async buildMealStore() {
const categories = Array.from(this.groupSettings.categories, x => x.name);
this.items = await api.recipes.getAllByCategory(categories);
if (this.items.length === 0) {
this.items = this.allRecipes;
}
},
getRandom(list) {
return list[Math.floor(Math.random() * list.length)];
},
random() {
this.usedRecipes = [1];
this.planDays.forEach((_, index) => {
const recipe = this.getRandom(this.filteredRecipes);
this.planDays[index].meals[0].slug = recipe.slug;
this.planDays[index].meals[0].name = recipe.name;
this.usedRecipes.push(recipe);
});
},
getDate(index) {
const dateObj = new Date(this.actualStartDate.valueOf() + 1000 * 3600 * 24 * index);
return utils.getDateAsPythonDate(dateObj);
},
async save() {
const mealBody = {
group: this.groupSettings.name,
startDate: this.startDate,
endDate: this.endDate,
planDays: this.planDays,
};
if (await api.mealPlans.create(mealBody)) {
this.$emit(CREATE_EVENT);
this.planDays = [];
this.startDate = null;
this.endDate = null;
}
},
formatDate(date) {
if (!date) return null;
return this.$d(date);
},
getNextDayOfTheWeek(dayName, excludeToday = true, refDate = new Date()) {
const dayOfWeek = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"].indexOf(dayName.slice(0, 3).toLowerCase());
if (dayOfWeek < 0) return;
refDate.setUTCHours(0, 0, 0, 0);
refDate.setDate(refDate.getDate() + +!!excludeToday + ((dayOfWeek + 7 - refDate.getDay() - +!!excludeToday) % 7));
return refDate;
},
setQuickWeek() {
const nextMonday = this.getNextDayOfTheWeek("Monday", false);
const nextEndDate = new Date(nextMonday);
nextEndDate.setDate(nextEndDate.getDate() + 4);
this.startDate = utils.getDateAsPythonDate(nextMonday);
this.endDate = utils.getDateAsPythonDate(nextEndDate);
},
},
};
</script>

View File

@@ -0,0 +1,188 @@
<template>
<v-toolbar
rounded
height="0"
class="fixed-bar mt-0"
color="rgb(255, 0, 0, 0.0)"
flat
style="z-index: 2; position: sticky"
:class="{ 'fixed-bar-mobile': $vuetify.breakpoint.xs }"
>
<BaseDialog
ref="deleteRecipieConfirm"
:title="$t('recipe.delete-recipe')"
color="error"
:icon="$globals.icons.alertCircle"
@confirm="emitDelete()"
>
{{ $t("recipe.delete-confirmation") }}
</BaseDialog>
<v-spacer></v-spacer>
<div v-if="!value" class="custom-btn-group ma-1">
<RecipeFavoriteBadge v-if="loggedIn" class="mx-1" color="info" button-style :slug="slug" show-always />
<v-tooltip bottom color="info">
<template #activator="{ on, attrs }">
<v-btn
v-if="loggedIn"
fab
small
class="mx-1"
color="info"
v-bind="attrs"
v-on="on"
@click="$emit('input', true)"
>
<v-icon> {{ $globals.icons.edit }} </v-icon>
</v-btn>
</template>
<span>{{ $t("general.edit") }}</span>
</v-tooltip>
<RecipeContextMenu
show-print
:menu-top="false"
:slug="slug"
:name="name"
:menu-icon="$globals.icons.mdiDotsHorizontal"
fab
color="info"
:card-menu="false"
/>
</div>
<div v-if="value" class="custom-btn-group mb-">
<v-btn
v-for="(btn, index) in editorButtons"
:key="index"
:fab="$vuetify.breakpoint.xs"
:small="$vuetify.breakpoint.xs"
class="mx-1"
:color="btn.color"
@click="emitHandler(btn.event)"
>
<v-icon :left="!$vuetify.breakpoint.xs">{{ btn.icon }}</v-icon>
{{ $vuetify.breakpoint.xs ? "" : btn.text }}
</v-btn>
</div>
</v-toolbar>
</template>
<script>
import RecipeContextMenu from "./RecipeContextMenu.vue";
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
const SAVE_EVENT = "save";
const DELETE_EVENT = "delete";
const CLOSE_EVENT = "close";
const JSON_EVENT = "json";
export default {
components: { RecipeContextMenu, RecipeFavoriteBadge },
props: {
slug: {
required: true,
type: String,
},
name: {
required: true,
type: String,
},
value: {
type: Boolean,
default: false,
},
loggedIn: {
type: Boolean,
default: false,
},
},
data() {
return {
edit: false,
};
},
computed: {
editorButtons() {
return [
{
text: this.$t("general.delete"),
icon: this.$globals.icons.delete,
event: DELETE_EVENT,
color: "error",
},
{
text: this.$t("general.json"),
icon: this.$globals.icons.codeBraces,
event: JSON_EVENT,
color: "accent",
},
{
text: this.$t("general.close"),
icon: this.$globals.icons.close,
event: CLOSE_EVENT,
color: "",
},
{
text: this.$t("general.save"),
icon: this.$globals.icons.save,
event: SAVE_EVENT,
color: "success",
},
];
},
},
methods: {
emitHandler(event) {
switch (event) {
case CLOSE_EVENT:
this.$emit(CLOSE_EVENT);
this.$emit("input", false);
break;
case SAVE_EVENT:
this.$emit(SAVE_EVENT);
break;
case JSON_EVENT:
this.$emit(JSON_EVENT);
break;
case DELETE_EVENT:
this.$refs.deleteRecipieConfirm.open();
break;
default:
break;
}
},
emitDelete() {
this.$emit(DELETE_EVENT);
this.$emit("input", false);
},
},
};
</script>
<style scoped>
.custom-btn-group {
flex: 0, 1, auto;
display: inline-flex;
}
.vertical {
flex-direction: column !important;
}
.sticky {
margin-left: auto;
position: fixed !important;
margin-top: 4.25rem;
}
.fixed-bar {
position: sticky;
position: -webkit-sticky; /* for Safari */
top: 4.5em;
z-index: 2;
}
.fixed-bar-mobile {
top: 1.5em !important;
}
</style>

View File

@@ -0,0 +1,167 @@
<template>
<div v-if="value.length > 0 || edit">
<v-card class="mt-2">
<v-card-title class="py-2">
{{ $t("asset.assets") }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-list v-if="value.length > 0" :flat="!edit">
<v-list-item v-for="(item, i) in value" :key="i">
<v-list-item-icon class="ma-auto">
<v-tooltip bottom>
<template #activator="{ on, attrs }">
<v-icon v-bind="attrs" v-on="on" v-text="getIconDefinition(item.icon).icon"></v-icon>
</template>
<span>{{ getIconDefinition(item.icon).title }}</span>
</v-tooltip>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title class="pl-2" v-text="item.name"></v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-btn v-if="!edit" color="primary" icon :href="assetURL(item.fileName)" target="_blank" top>
<v-icon> {{ $globals.icons.download }} </v-icon>
</v-btn>
<div v-else>
<v-btn color="error" icon top @click="deleteAsset(i)">
<v-icon>{{ $globals.icons.delete }}</v-icon>
</v-btn>
<TheCopyButton :copy-text="copyLink(item.fileName)" />
</div>
</v-list-item-action>
</v-list-item>
</v-list>
</v-card>
<div class="d-flex ml-auto mt-2">
<v-spacer></v-spacer>
<BaseDialog :title="$t('asset.new-asset')" :title-icon="getIconDefinition(newAsset.icon).icon" @submit="addAsset">
<template #open="{ open }">
<v-btn v-if="edit" color="secondary" dark @click="open">
<v-icon>{{ $globals.icons.create }}</v-icon>
</v-btn>
</template>
<v-card-text class="pt-2">
<v-text-field v-model="newAsset.name" dense :label="$t('general.name')"></v-text-field>
<div class="d-flex justify-space-between">
<v-select
v-model="newAsset.icon"
dense
:prepend-icon="getIconDefinition(newAsset.icon).icon"
:items="iconOptions"
item-text="title"
item-value="name"
class="mr-2"
>
<template #item="{ item }">
<v-list-item-avatar>
<v-icon class="mr-auto">
{{ item.icon }}
</v-icon>
</v-list-item-avatar>
{{ item.title }}
</template>
</v-select>
<TheUploadBtn :post="false" file-name="file" :text-btn="false" @uploaded="setFileObject" />
</div>
{{ fileObject.name }}
</v-card-text>
</BaseDialog>
</div>
</div>
</template>
<script>
import TheCopyButton from "@/components/UI/Buttons/TheCopyButton";
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
import { api } from "@/api";
export default {
components: {
BaseDialog,
TheUploadBtn,
TheCopyButton,
},
props: {
slug: String,
value: {
type: Array,
},
edit: {
type: Boolean,
default: true,
},
},
data() {
return {
fileObject: {},
newAsset: {
name: "",
icon: "mdi-file",
},
};
},
computed: {
baseURL() {
return window.location.origin;
},
iconOptions() {
return [
{
name: "mdi-file",
title: this.$i18n.t("asset.file"),
icon: this.$globals.icons.file
},
{
name: "mdi-file-pdf-box",
title: this.$i18n.t("asset.pdf"),
icon: this.$globals.icons.filePDF
},
{
name: "mdi-file-image",
title: this.$i18n.t("asset.image"),
icon: this.$globals.icons.fileImage
},
{
name: "mdi-code-json",
title: this.$i18n.t("asset.code"),
icon: this.$globals.icons.codeJson
},
{
name: "mdi-silverware-fork-knife",
title: this.$i18n.t("asset.recipe"),
icon: this.$globals.icons.primary
},
];
},
},
methods: {
getIconDefinition(val) {
return this.iconOptions.find(({ name }) => name === val );
},
assetURL(assetName) {
return api.recipes.recipeAssetPath(this.slug, assetName);
},
setFileObject(obj) {
this.fileObject = obj;
},
async addAsset() {
const serverAsset = await api.recipes.createAsset(
this.slug,
this.fileObject,
this.newAsset.name,
this.newAsset.icon
);
this.value.push(serverAsset.data);
this.newAsset = { name: "", icon: "mdi-file" };
},
deleteAsset(index) {
this.value.splice(index, 1);
},
copyLink(fileName) {
const assetLink = api.recipes.recipeAssetPath(this.slug, fileName);
return `<img src="${this.baseURL}${assetLink}" height="100%" width="100%"> </img>`;
},
},
};
</script>

View File

@@ -0,0 +1,117 @@
<template>
<v-card>
<v-card-title class="headline">
<v-icon large class="mr-2">
{{ $globals.icons.commentTextMultipleOutline }}
</v-icon>
{{ $t("recipe.comments") }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card v-for="(comment, index) in comments" :key="comment.id" class="ma-2">
<v-list-item two-line>
<v-list-item-avatar color="accent" class="white--text">
<img :src="getProfileImage(comment.user.id)" />
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title> {{ comment.user.username }}</v-list-item-title>
<v-list-item-subtitle> {{ $d(new Date(comment.dateAdded), "short") }} </v-list-item-subtitle>
</v-list-item-content>
<v-card-actions v-if="loggedIn">
<TheButton
v-if="!editKeys[comment.id] && (user.admin || comment.user.id === user.id)"
small
minor
delete
@click="deleteComment(comment.id)"
/>
<TheButton
v-if="!editKeys[comment.id] && comment.user.id === user.id"
small
edit
@click="editComment(comment.id)"
/>
<TheButton v-else-if="editKeys[comment.id]" small update @click="updateComment(comment.id, index)" />
</v-card-actions>
</v-list-item>
<div>
<v-card-text>
{{ !editKeys[comment.id] ? comment.text : null }}
<v-textarea v-if="editKeys[comment.id]" v-model="comment.text"> </v-textarea>
</v-card-text>
</div>
</v-card>
<v-card-text v-if="loggedIn">
<v-textarea v-model="newComment" auto-grow row-height="1" outlined> </v-textarea>
<div class="d-flex">
<TheButton class="ml-auto" create @click="createNewComment"> {{ $t("recipe.comment-action") }} </TheButton>
</div>
</v-card-text>
</v-card>
</template>
<script>
import { api } from "@/api";
const NEW_COMMENT_EVENT = "new-comment";
const UPDATE_COMMENT_EVENT = "update-comment";
export default {
props: {
comments: {
type: Array,
},
slug: {
type: String,
},
},
data() {
return {
newComment: "",
editKeys: {},
};
},
computed: {
user() {
return this.$store.getters.getUserData;
},
loggedIn() {
return this.$store.getters.getIsLoggedIn;
},
},
watch: {
comments() {
for (const comment of this.comments) {
this.$set(this.editKeys, comment.id, false);
}
},
},
methods: {
resetImage() {
this.hideImage == false;
},
getProfileImage(id) {
return api.users.userProfileImage(id);
},
editComment(id) {
this.$set(this.editKeys, id, true);
},
async updateComment(id, index) {
this.$set(this.editKeys, id, false);
await api.recipes.updateComment(this.slug, id, this.comments[index]);
this.$emit(UPDATE_COMMENT_EVENT);
},
async createNewComment() {
await api.recipes.createComment(this.slug, { text: this.newComment });
this.$emit(NEW_COMMENT_EVENT);
this.newComment = "";
},
async deleteComment(id) {
await api.recipes.deleteComment(this.slug, id);
this.$emit(UPDATE_COMMENT_EVENT);
},
},
};
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,57 @@
<template>
<div class="text-center">
<v-dialog v-model="dialog" width="600">
<template #activator="{ on, attrs }">
<v-btn color="secondary lighten-2" dark v-bind="attrs" v-on="on" @click="inputText = ''">
{{ $t("new-recipe.bulk-add") }}
</v-btn>
</template>
<v-card>
<v-card-title class="headline"> {{ $t("new-recipe.bulk-add") }} </v-card-title>
<v-card-text>
<p>
{{ $t("new-recipe.paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list") }}
</p>
<v-textarea v-model="inputText"> </v-textarea>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="success" text @click="save"> {{ $t("general.save") }} </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
data() {
return {
dialog: false,
inputText: "",
};
},
methods: {
splitText() {
const split = this.inputText.split("\n");
split.forEach((element, index) => {
if ((element === "\n") | (element == false)) {
split.splice(index, 1);
}
});
return split;
},
save() {
this.$emit("bulk-data", this.splitText());
this.dialog = false;
},
},
};
</script>

View File

@@ -0,0 +1,85 @@
<template>
<div class="text-center">
<v-dialog v-model="dialog" width="700">
<template #activator="{ on, attrs }">
<v-btn color="accent" dark v-bind="attrs" v-on="on"> {{ $t("recipe.api-extras") }} </v-btn>
</template>
<v-card>
<v-card-title> {{ $t("recipe.api-extras") }} </v-card-title>
<v-card-text :key="formKey">
<v-row v-for="(value, key, index) in extras" :key="index" align="center">
<v-col cols="12" sm="1">
<v-btn fab text x-small color="white" elevation="0" @click="removeExtra(key)">
<v-icon color="error">{{ $globals.icons.delete }}</v-icon>
</v-btn>
</v-col>
<v-col cols="12" md="3" sm="6">
<v-text-field :label="$t('recipe.object-key')" :value="key" @input="updateKey(index)"> </v-text-field>
</v-col>
<v-col cols="12" md="8" sm="6">
<v-text-field v-model="extras[key]" :label="$t('recipe.object-value')"> </v-text-field>
</v-col>
</v-row>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-form ref="addKey">
<v-text-field
v-model="newKeyName"
:label="$t('recipe.new-key-name')"
class="pr-4"
:rules="[rules.required, rules.whiteSpace]"
></v-text-field>
</v-form>
<v-btn color="info" text @click="append"> {{ $t("recipe.add-key") }} </v-btn>
<v-spacer></v-spacer>
<v-btn color="success" text @click="save"> {{ $t("general.save") }} </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
props: {
extras: Object,
},
data() {
return {
newKeyName: null,
dialog: false,
formKey: 1,
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: {
save() {
this.$emit("save", this.extras);
this.dialog = false;
},
append() {
if (this.$refs.addKey.validate()) {
this.extras[this.newKeyName] = "value";
this.formKey += 1;
}
},
removeExtra(key) {
delete this.extras[key];
this.formKey += 1;
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,76 @@
<template>
<div class="text-center">
<v-menu offset-y top nudge-top="6" :close-on-content-click="false">
<template #activator="{ on, attrs }">
<v-btn color="accent" dark v-bind="attrs" v-on="on">
<v-icon left>
{{ $globals.icons.fileImage }}
</v-icon>
{{ $t("general.image") }}
</v-btn>
</template>
<v-card width="400">
<v-card-title class="headline flex mb-0">
<div>
{{ $t("recipe.recipe-image") }}
</div>
<TheUploadBtn
class="ml-auto"
url="none"
file-name="image"
:text-btn="false"
:post="false"
@uploaded="uploadImage"
/>
</v-card-title>
<v-card-text class="mt-n5">
<div>
<v-text-field v-model="url" :label="$t('general.url')" class="pt-5" clearable :messages="getMessages()">
<template #append-outer>
<v-btn class="ml-2" color="primary" :loading="loading" :disabled="!slug" @click="getImageFromURL">
{{ $t("general.get") }}
</v-btn>
</template>
</v-text-field>
</div>
</v-card-text>
</v-card>
</v-menu>
</div>
</template>
<script>
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
import { api } from "@/api";
const REFRESH_EVENT = "refresh";
const UPLOAD_EVENT = "upload";
export default {
components: {
TheUploadBtn,
},
props: {
slug: String,
},
data: () => ({
url: "",
loading: false,
}),
methods: {
uploadImage(fileObject) {
this.$emit(UPLOAD_EVENT, fileObject);
},
async getImageFromURL() {
this.loading = true;
if (await api.recipes.updateImagebyURL(this.slug, this.url)) {
this.$emit(REFRESH_EVENT);
}
this.loading = false;
},
getMessages() {
return this.slug ? [""] : [this.$i18n.t("recipe.save-recipe-before-use")];
},
},
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,167 @@
<template>
<div v-if="edit || (value && value.length > 0)">
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
<div v-if="edit">
<draggable :value="value" handle=".handle" @input="updateIndex" @start="drag = true" @end="drag = false">
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
<div v-for="(ingredient, index) in value" :key="generateKey('ingredient', index)">
<v-row align="center">
<v-text-field
v-if="edit && showTitleEditor[index]"
v-model="value[index].title"
class="mx-3 mt-3"
dense
:label="$t('recipe.section-title')"
>
</v-text-field>
<v-textarea
v-model="value[index].note"
class="mr-2"
:label="$t('recipe.ingredient')"
auto-grow
solo
dense
rows="1"
>
<template slot="append">
<v-tooltip right nudge-right="10">
<template #activator="{ on, attrs }">
<v-btn icon small class="mt-n1" v-bind="attrs" v-on="on" @click="toggleShowTitle(index)">
<v-icon>{{ showTitleEditor[index] ? $globals.icons.minus : $globals.icons.createAlt }}</v-icon>
</v-btn>
</template>
<span>{{
showTitleEditor[index] ? $t("recipe.remove-section") : $t("recipe.insert-section")
}}</span>
</v-tooltip>
</template>
<template slot="append-outer">
<v-icon class="handle">{{ $globals.icons.arrowUpDown }}</v-icon>
</template>
<v-icon slot="prepend" class="mr-n1" color="error" @click="removeByIndex(value, index)">
{{ $globals.icons.delete }}
</v-icon>
</v-textarea>
</v-row>
</div>
</transition-group>
</draggable>
<div class="d-flex row justify-end">
<BulkAdd class="mr-2" @bulk-data="addIngredient" />
<v-btn color="secondary" dark class="mr-4" @click="addIngredient">
<v-icon>{{ $globals.icons.create }}</v-icon>
</v-btn>
</div>
</div>
<div v-else>
<div v-for="(ingredient, index) in value" :key="generateKey('ingredient', index)">
<h3 v-if="showTitleEditor[index]" class="mt-2">{{ ingredient.title }}</h3>
<v-divider v-if="showTitleEditor[index]"></v-divider>
<v-list-item dense @click="toggleChecked(index)">
<v-checkbox hide-details :value="checked[index]" class="pt-0 my-auto py-auto" color="secondary"> </v-checkbox>
<v-list-item-content>
<VueMarkdown class="ma-0 pa-0 text-subtitle-1 dense-markdown" :source="ingredient.note"> </VueMarkdown>
</v-list-item-content>
</v-list-item>
</div>
</div>
</div>
</template>
<script>
import BulkAdd from "@/components/Recipe/Parts/Helpers/BulkAdd";
import VueMarkdown from "@adapttive/vue-markdown";
import draggable from "vuedraggable";
import { utils } from "@/utils";
export default {
components: {
BulkAdd,
draggable,
VueMarkdown,
},
props: {
value: {
type: Array,
},
edit: {
type: Boolean,
default: true,
},
},
data() {
return {
drag: false,
checked: [],
showTitleEditor: [],
};
},
watch: {
value: {
handler() {
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
},
},
},
mounted() {
this.checked = this.value.map(() => false);
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
},
methods: {
addIngredient(ingredients = null) {
if (ingredients.length) {
const newIngredients = ingredients.map(x => {
return {
title: null,
note: x,
unit: null,
food: null,
disableAmount: true,
quantity: 1,
};
});
this.value.push(...newIngredients);
} else {
this.value.push({
title: null,
note: "",
unit: null,
food: null,
disableAmount: true,
quantity: 1,
});
}
},
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
updateIndex(data) {
this.$emit("input", data);
},
toggleChecked(index) {
this.$set(this.checked, index, !this.checked[index]);
},
removeByIndex(list, index) {
list.splice(index, 1);
},
validateTitle(title) {
return !(title === null || title === "");
},
toggleShowTitle(index) {
const newVal = !this.showTitleEditor[index];
if (!newVal) {
this.value[index].title = "";
}
this.$set(this.showTitleEditor, index, newVal);
},
},
};
</script>
<style>
.dense-markdown p {
margin: auto !important;
}
</style>

View File

@@ -0,0 +1,169 @@
<template>
<div>
<h2 class="mb-4">{{ $t("recipe.instructions") }}</h2>
<div>
<draggable
:disabled="!edit"
:value="value"
handle=".handle"
@input="updateIndex"
@start="drag = true"
@end="drag = false"
>
<div v-for="(step, index) in value" :key="index">
<v-app-bar v-if="showTitleEditor[index]" class="primary mx-1 mt-6" dark dense rounded>
<v-toolbar-title v-if="!edit" class="headline">
<v-app-bar-title v-text="step.title"> </v-app-bar-title>
</v-toolbar-title>
<v-text-field
v-if="edit"
v-model="step.title"
class="headline pa-0 mt-5"
dense
solo
flat
:placeholder="$t('recipe.section-title')"
background-color="primary"
>
</v-text-field>
</v-app-bar>
<v-hover v-slot="{ hover }">
<v-card
class="ma-1"
:class="[{ 'on-hover': hover }, isChecked(index)]"
:elevation="hover ? 12 : 2"
:ripple="!edit"
@click="toggleDisabled(index)"
>
<v-card-title :class="{ 'pb-0': !isChecked(index) }">
<v-btn
v-if="edit"
fab
x-small
color="white"
class="mr-2"
elevation="0"
@click="removeByIndex(value, index)"
>
<v-icon size="24" color="error">{{ $globals.icons.delete }}</v-icon>
</v-btn>
{{ $t("recipe.step-index", { step: index + 1 }) }}
<v-btn v-if="edit" text color="primary" class="ml-auto" @click="toggleShowTitle(index)">
{{ !showTitleEditor[index] ? $t("recipe.insert-section") : $t("recipe.remove-section") }}
</v-btn>
<v-icon v-if="edit" class="handle">{{ $globals.icons.arrowUpDown }}</v-icon>
<v-fade-transition>
<v-icon v-show="isChecked(index)" size="24" class="ml-auto" color="success">
{{ $globals.icons.checkboxMarkedCircle }}
</v-icon>
</v-fade-transition>
</v-card-title>
<v-card-text v-if="edit">
<v-textarea
:key="generateKey('instructions', index)"
v-model="value[index]['text']"
auto-grow
dense
rows="4"
>
</v-textarea>
</v-card-text>
<v-expand-transition>
<div v-show="!isChecked(index) && !edit" class="m-0 p-0">
<v-card-text>
<VueMarkdown :source="step.text"> </VueMarkdown>
</v-card-text>
</div>
</v-expand-transition>
</v-card>
</v-hover>
</div>
</draggable>
</div>
</div>
</template>
<script>
import draggable from "vuedraggable";
import VueMarkdown from "@adapttive/vue-markdown";
import { utils } from "@/utils";
export default {
components: {
VueMarkdown,
draggable,
},
props: {
value: {
type: Array,
},
edit: {
type: Boolean,
default: true,
},
},
data() {
return {
disabledSteps: [],
showTitleEditor: [],
};
},
watch: {
value: {
handler() {
this.disabledSteps = [];
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
},
},
},
mounted() {
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
},
methods: {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
removeByIndex(list, index) {
list.splice(index, 1);
},
validateTitle(title) {
return !(title === null || title === "");
},
toggleDisabled(stepIndex) {
if (this.edit) return;
if (this.disabledSteps.includes(stepIndex)) {
const index = this.disabledSteps.indexOf(stepIndex);
if (index !== -1) {
this.disabledSteps.splice(index, 1);
}
} else {
this.disabledSteps.push(stepIndex);
}
},
isChecked(stepIndex) {
if (this.disabledSteps.includes(stepIndex) && !this.edit) {
return "disabled-card";
} else {
}
},
toggleShowTitle(index) {
const newVal = !this.showTitleEditor[index];
if (!newVal) {
this.value[index].title = "";
}
this.$set(this.showTitleEditor, index, newVal);
},
updateIndex(data) {
this.$emit("input", data);
},
},
};
</script>

View File

@@ -0,0 +1,67 @@
<template>
<div v-if="value.length > 0 || edit">
<h2 class="my-4">{{ $t("recipe.note") }}</h2>
<v-card v-for="(note, index) in value" :key="generateKey('note', index)" class="mt-1">
<div v-if="edit">
<v-card-text>
<v-row align="center">
<v-btn fab x-small color="white" class="mr-2" elevation="0" @click="removeByIndex(value, index)">
<v-icon color="error">{{ $globals.icons.delete }}</v-icon>
</v-btn>
<v-text-field v-model="value[index]['title']" :label="$t('recipe.title')"></v-text-field>
</v-row>
<v-textarea v-model="value[index]['text']" auto-grow :placeholder="$t('recipe.note')"> </v-textarea>
</v-card-text>
</div>
<div v-else>
<v-card-title class="py-2">
{{ note.title }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text>
<VueMarkdown :source="note.text"> </VueMarkdown>
</v-card-text>
</div>
</v-card>
<div v-if="edit" class="d-flex justify-end">
<v-btn class="mt-1" color="secondary" dark @click="addNote">
<v-icon>{{ $globals.icons.create }}</v-icon>
</v-btn>
</div>
</div>
</template>
<script>
import VueMarkdown from "@adapttive/vue-markdown";
import { utils } from "@/utils";
export default {
components: {
VueMarkdown,
},
props: {
value: {
type: Array,
},
edit: {
type: Boolean,
default: true,
},
},
methods: {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
addNote() {
this.value.push({ title: "", text: "" });
},
removeByIndex(list, index) {
list.splice(index, 1);
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,100 @@
<template>
<div v-if="valueNotNull || edit">
<v-card class="mt-2">
<v-card-title class="py-2">
{{ $t("recipe.nutrition") }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text v-if="edit">
<div v-for="(item, key, index) in value" :key="index">
<v-text-field
dense
:value="value[key]"
:label="labels[key].label"
:suffix="labels[key].suffix"
type="number"
autocomplete="off"
@input="updateValue(key, $event)"
></v-text-field>
</div>
</v-card-text>
<v-list v-if="showViewer" dense class="mt-0 pt-0">
<v-list-item v-for="(item, key, index) in labels" :key="index">
<v-list-item-content>
<v-list-item-title class="pl-4 text-subtitle-1 flex row ">
<div>{{ item.label }}</div>
<div class="ml-auto mr-1">{{ value[key] }}</div>
<div>{{ item.suffix }}</div>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-card>
</div>
</template>
<script>
export default {
props: {
value: {},
edit: {
type: Boolean,
default: true,
},
},
data() {
return {
labels: {
calories: {
label: this.$t("recipe.calories"),
suffix: this.$t("recipe.calories-suffix"),
},
fatContent: {
label: this.$t("recipe.fat-content"),
suffix: this.$t("recipe.grams"),
},
fiberContent: {
label: this.$t("recipe.fiber-content"),
suffix: this.$t("recipe.grams"),
},
proteinContent: {
label: this.$t("recipe.protein-content"),
suffix: this.$t("recipe.grams"),
},
sodiumContent: {
label: this.$t("recipe.sodium-content"),
suffix: this.$t("recipe.milligrams"),
},
sugarContent: {
label: this.$t("recipe.sugar-content"),
suffix: this.$t("recipe.grams"),
},
carbohydrateContent: {
label: this.$t("recipe.carbohydrate-content"),
suffix: this.$t("recipe.grams"),
},
},
};
},
computed: {
showViewer() {
return !this.edit && this.valueNotNull;
},
valueNotNull() {
for (const property in this.value) {
const valueProperty = this.value[property];
if (valueProperty && valueProperty !== "") return true;
}
return false;
},
},
methods: {
updateValue(key, value) {
this.$emit("input", { ...this.value, [key]: value });
},
},
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,155 @@
<template>
<div class="container print">
<div>
<h1>
<svg class="icon" viewBox="0 0 24 24">
<path
fill="#E58325"
d="M8.1,13.34L3.91,9.16C2.35,7.59 2.35,5.06 3.91,3.5L10.93,10.5L8.1,13.34M13.41,13L20.29,19.88L18.88,21.29L12,14.41L5.12,21.29L3.71,19.88L13.36,10.22L13.16,10C12.38,9.23 12.38,7.97 13.16,7.19L17.5,2.82L18.43,3.74L15.19,7L16.15,7.94L19.39,4.69L20.31,5.61L17.06,8.85L18,9.81L21.26,6.56L22.18,7.5L17.81,11.84C17.03,12.62 15.77,12.62 15,11.84L14.78,11.64L13.41,13Z"
/>
</svg>
{{ recipe.name }}
</h1>
</div>
<div class="time-container">
<RecipeTimeCard :prep-time="recipe.prepTime" :total-time="recipe.totalTime" :perform-time="recipe.performTime" />
</div>
<v-btn
v-if="recipe.recipeYield"
dense
small
:hover="false"
type="label"
:ripple="false"
elevation="0"
color="secondary darken-1"
class="rounded-sm static"
>
{{ recipe.recipeYield }}
</v-btn>
<div>
<VueMarkdown :source="recipe.description"> </VueMarkdown>
<h2>{{ $t("recipe.ingredients") }}</h2>
<ul>
<li v-for="(ingredient, index) in recipe.recipeIngredient" :key="index">
<v-icon>
{{ $globals.icons.checkboxBlankOutline }}
</v-icon>
<p>{{ ingredient.note }}</p>
</li>
</ul>
</div>
<div>
<h2>{{ $t("recipe.instructions") }}</h2>
<div v-for="(step, index) in recipe.recipeInstructions" :key="index">
<h2 v-if="step.title">{{ step.title }}</h2>
<div class="ml-5">
<h3>{{ $t("recipe.step-index", { step: index + 1 }) }}</h3>
<VueMarkdown :source="step.text"> </VueMarkdown>
</div>
</div>
<br />
<v-divider v-if="recipe.notes.length > 0" class="mb-5 mt-0"></v-divider>
<div v-for="(note, index) in recipe.notes" :key="index + 'note'">
<h3>{{ note.title }}</h3>
<VueMarkdown :source="note.text"> </VueMarkdown>
</div>
</div>
</div>
</template>
<script>
import RecipeTimeCard from "@/components/Recipe/RecipeTimeCard.vue";
import VueMarkdown from "@adapttive/vue-markdown";
export default {
components: {
RecipeTimeCard,
VueMarkdown,
},
props: {
recipe: Object,
},
};
</script>
<style>
@media print {
body,
html {
margin-top: -40px !important;
}
}
h1 {
margin-top: 0 !important;
display: -webkit-box;
display: flex;
font-size: 2rem;
letter-spacing: -0.015625em;
font-weight: 300;
padding: 0;
}
h2 {
margin-bottom: 0.25rem;
}
h3 {
margin-bottom: 0.25rem;
}
ul {
padding-left: 1rem;
}
li {
display: -webkit-box;
display: -webkit-flex;
margin-left: 0;
margin-bottom: 0.5rem;
}
li p {
margin-left: 0.25rem;
margin-bottom: 0 !important;
}
p {
margin: 0;
font-size: 1rem;
letter-spacing: 0.03125em;
font-weight: 400;
}
.icon {
margin-top: auto;
margin-bottom: auto;
margin-right: 0.5rem;
height: 3rem;
width: 3rem;
}
.time-container {
display: flex;
justify-content: left;
}
.time-chip {
border-radius: 0.25rem;
border-color: black;
border: 1px;
border-top: 1px;
}
.print {
display: none;
}
@media print {
.print {
display: initial;
}
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<div class="text-center">
<v-menu offset-y top nudge-top="6" :close-on-content-click="false">
<template #activator="{ on, attrs }">
<v-btn color="accent" dark v-bind="attrs" v-on="on">
<v-icon left>
{{ $globals.icons.cog }}
</v-icon>
{{ $t("general.settings") }}
</v-btn>
</template>
<v-card>
<v-card-title class="py-2">
<div>
{{ $t("recipe.recipe-settings") }}
</div>
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text class="mt-n5">
<v-switch
v-for="(itemValue, key) in value"
:key="key"
v-model="value[key]"
dense
flat
inset
:label="labels[key]"
hide-details
></v-switch>
</v-card-text>
</v-card>
</v-menu>
</div>
</template>
<script>
export default {
components: {},
props: {
value: Object,
},
computed: {
labels() {
return {
public: this.$t("recipe.public-recipe"),
showNutrition: this.$t("recipe.show-nutrition-values"),
showAssets: this.$t("asset.show-assets"),
landscapeView: this.$t("recipe.landscape-view-coming-soon"),
disableComments: this.$t("recipe.disable-comments"),
disableAmount: this.$t("recipe.disable-amount"),
};
},
},
methods: {},
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,52 @@
<template>
<div>
<v-chip v-for="(time, index) in allTimes" :key="index" label color="accent custom-transparent" class="ma-1">
<v-icon left>
{{ $globals.icons.clockOutline }}
</v-icon>
{{ time.name }} |
{{ time.value }}
</v-chip>
</div>
</template>
<script>
export default {
props: {
prepTime: String,
totalTime: String,
performTime: String,
},
computed: {
showCards() {
return [this.prepTime, this.totalTime, this.performTime].some(x => !this.isEmpty(x));
},
allTimes() {
return [this.validateTotalTime, this.validatePrepTime, this.validatePerformTime].filter(x => x !== null);
},
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;
}
</style>

View File

@@ -0,0 +1,61 @@
<template>
<v-list-item two-line to="/admin/profile">
<v-list-item-avatar color="accent" class="white--text">
<v-img v-if="!noImage" :src="profileImage" />
<div v-else>
{{ initials }}
</div>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title> {{ user.fullName }}</v-list-item-title>
<v-list-item-subtitle> {{ user.admin ? $t("user.admin") : $t("user.user") }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</template>
<script>
import { initials } from "@/mixins/initials";
import axios from "axios";
import { api } from "@/api";
export default {
mixins: [initials],
props: {
user: {
type: Object,
},
},
data() {
return {
noImage: false,
profileImage: "",
};
},
watch: {
async user() {
this.setImage();
},
},
methods: {
async setImage() {
const userImageURL = api.users.userProfileImage(this.user.id);
if (await this.imageExists(userImageURL)) {
this.noImage = false;
this.profileImage = userImageURL;
} else {
this.noImage = true;
}
},
async imageExists(url) {
const response = await axios.get(url).catch(() => {
this.noImage = true;
return { status: 404 };
});
return response.status !== 404;
},
},
};
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,153 @@
<template>
<v-form ref="form">
<v-card-text>
<v-row dense>
<ImageUploadBtn class="my-1" :slug="value.slug" @upload="uploadImage" @refresh="$emit('upload')" />
<SettingsMenu class="my-1 mx-1" :value="value.settings" @upload="uploadImage" />
</v-row>
<v-row dense>
<v-col>
<v-text-field v-model="value.totalTime" :label="$t('recipe.total-time')"></v-text-field>
</v-col>
<v-col><v-text-field v-model="value.prepTime" :label="$t('recipe.prep-time')"></v-text-field></v-col>
<v-col><v-text-field v-model="value.performTime" :label="$t('recipe.perform-time')"></v-text-field></v-col>
</v-row>
<v-text-field v-model="value.name" class="my-3" :label="$t('recipe.recipe-name')" :rules="[existsRule]">
</v-text-field>
<v-textarea v-model="value.description" auto-grow min-height="100" :label="$t('recipe.description')">
</v-textarea>
<div class="my-2"></div>
<v-row dense disabled>
<v-col sm="4">
<v-text-field v-model="value.recipeYield" :label="$t('recipe.servings')" class="rounded-sm"> </v-text-field>
</v-col>
<v-spacer></v-spacer>
<Rating v-model="value.rating" :emit-only="true" />
</v-row>
<v-row>
<v-col cols="12" sm="12" md="4" lg="4">
<Ingredients v-model="value.recipeIngredient" :edit="true" />
<v-card class="mt-6">
<v-card-title class="py-2">
{{ $t("recipe.categories") }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text>
<CategoryTagSelector
v-model="value.recipeCategory"
:return-object="false"
:show-add="true"
:show-label="false"
/>
</v-card-text>
</v-card>
<v-card class="mt-2">
<v-card-title class="py-2">
{{ $t("tag.tags") }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text>
<CategoryTagSelector
v-model="value.tags"
:return-object="false"
:show-add="true"
:tag-selector="true"
:show-label="false"
/>
</v-card-text>
</v-card>
<Nutrition v-model="value.nutrition" :edit="true" />
<Assets v-model="value.assets" :edit="true" :slug="value.slug" />
<ExtrasEditor :extras="value.extras" @save="saveExtras" />
</v-col>
<v-divider class="my-divider" :vertical="true"></v-divider>
<v-col cols="12" sm="12" md="8" lg="8">
<Instructions v-model="value.recipeInstructions" :edit="true" />
<div class="d-flex row justify-end mt-2">
<BulkAdd class="mr-2" @bulk-data="appendSteps" />
<v-btn color="secondary" dark class="mr-4" @click="addStep">
<v-icon>{{ $globals.icons.create }}</v-icon>
</v-btn>
</div>
<Notes v-model="value.notes" :edit="true" />
<v-text-field v-model="value.orgURL" class="mt-10" :label="$t('recipe.original-url')"></v-text-field>
</v-col>
</v-row>
</v-card-text>
</v-form>
</template>
<script>
import BulkAdd from "@/components/Recipe/Parts/Helpers/BulkAdd";
import ExtrasEditor from "@/components/Recipe/Parts/Helpers/ExtrasEditor";
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
import ImageUploadBtn from "@/components/Recipe/Parts/Helpers/ImageUploadBtn";
import { validators } from "@/mixins/validators";
import Nutrition from "@/components/Recipe/Parts/Nutrition";
import Instructions from "@/components/Recipe/Parts/Instructions";
import Ingredients from "@/components/Recipe/Parts/Ingredients";
import Assets from "@/components/Recipe/Parts/Assets.vue";
import Notes from "@/components/Recipe/Parts/Notes.vue";
import SettingsMenu from "@/components/Recipe/Parts/Helpers/SettingsMenu.vue";
import Rating from "@/components/Recipe/Parts/Rating";
const UPLOAD_EVENT = "upload";
export default {
components: {
BulkAdd,
ExtrasEditor,
CategoryTagSelector,
Nutrition,
ImageUploadBtn,
Instructions,
Ingredients,
Assets,
Notes,
SettingsMenu,
Rating,
},
mixins: [validators],
props: {
value: Object,
},
data() {
return {
fileObject: null,
};
},
methods: {
uploadImage(fileObject) {
this.$emit(UPLOAD_EVENT, fileObject);
},
appendSteps(steps) {
this.value.recipeInstructions.push(
...steps.map(x => ({
title: "",
text: x,
}))
);
},
addStep() {
this.value.recipeInstructions.push({ title: "", text: "" });
},
saveExtras(extras) {
this.value.extras = extras;
},
validateRecipe() {
return this.$refs.form.validate();
},
},
};
</script>
<style>
.disabled-card {
opacity: 0.5;
}
.my-divider {
margin: 0 -1px;
}
</style>

View File

@@ -0,0 +1,142 @@
<template>
<div>
<v-card-title class="headline">
{{ recipe.name }}
</v-card-title>
<v-card-text>
<VueMarkdown :source="recipe.description"> </VueMarkdown>
<v-row dense disabled>
<v-col>
<v-btn
v-if="recipe.recipeYield"
dense
small
:hover="false"
type="label"
:ripple="false"
elevation="0"
color="secondary darken-1"
class="rounded-sm static"
>
{{ recipe.recipeYield }}
</v-btn>
</v-col>
<Rating :key="recipe.slug" :value="recipe.rating" :name="recipe.name" :slug="recipe.slug" />
</v-row>
<v-row>
<v-col cols="12" sm="12" md="4" lg="4">
<Ingredients :value="recipe.recipeIngredient" :edit="false" />
<div v-if="medium">
<v-card v-if="recipe.recipeCategory.length > 0" class="mt-2">
<v-card-title class="py-2">
{{ $t("recipe.categories") }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text>
<RecipeChips :items="recipe.recipeCategory" />
</v-card-text>
</v-card>
<v-card v-if="recipe.tags.length > 0" class="mt-2">
<v-card-title class="py-2">
{{ $t("tag.tags") }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text>
<RecipeChips :items="recipe.tags" :is-category="false" />
</v-card-text>
</v-card>
<Nutrition v-if="recipe.settings.showNutrition" :value="recipe.nutrition" :edit="false" />
<Assets v-if="recipe.settings.showAssets" :value="recipe.assets" :edit="false" :slug="recipe.slug" />
</div>
</v-col>
<v-divider v-if="medium" class="my-divider" :vertical="true"></v-divider>
<v-col cols="12" sm="12" md="8" lg="8">
<Instructions :value="recipe.recipeInstructions" :edit="false" />
<Notes :value="recipe.notes" :edit="false" />
</v-col>
</v-row>
<div v-if="!medium">
<RecipeChips :title="$t('recipe.categories')" :items="recipe.recipeCategory" />
<RecipeChips :title="$t('tag.tags')" :items="recipe.tags" />
<Nutrition v-if="recipe.settings.showNutrition" :value="recipe.nutrition" :edit="false" />
<Assets v-if="recipe.settings.showAssets" :value="recipe.assets" :edit="false" :slug="recipe.slug" />
</div>
<v-row class="mt-2 mb-1">
<v-col></v-col>
<v-btn
v-if="recipe.orgURL"
dense
small
:hover="false"
type="label"
:ripple="false"
elevation="0"
:href="recipe.orgURL"
color="secondary darken-1"
target="_blank"
class="rounded-sm mr-4"
>
{{ $t("recipe.original-url") }}
</v-btn>
</v-row>
</v-card-text>
</div>
</template>
<script>
import Nutrition from "@/components/Recipe/Parts/Nutrition";
import VueMarkdown from "@adapttive/vue-markdown";
import { utils } from "@/utils";
import Rating from "@/components/Recipe/Parts/Rating";
import Notes from "@/components/Recipe/Parts/Notes";
import Ingredients from "@/components/Recipe/Parts/Ingredients";
import Instructions from "@/components/Recipe/Parts/Instructions.vue";
import Assets from "../../../../frontend.old/src/components/Recipe/Parts/Assets.vue";
import RecipeChips from "./RecipeChips";
export default {
components: {
VueMarkdown,
RecipeChips,
Notes,
Ingredients,
Nutrition,
Instructions,
Assets,
Rating,
},
props: {
recipe: {
type: Object,
required: true,
},
},
data() {
return {
disabledSteps: [],
};
},
computed: {
medium() {
return this.$vuetify.breakpoint.mdAndUp;
},
},
methods: {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
},
};
</script>
<style>
.static {
pointer-events: none;
}
.my-divider {
margin: 0 -1px;
}
</style>