mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-24 15:49:42 +02:00
reorganize all frontend items
This commit is contained in:
parent
d67240d449
commit
00a8fdda41
147 changed files with 3845 additions and 743 deletions
|
@ -1,173 +0,0 @@
|
|||
<template>
|
||||
<v-row>
|
||||
<SearchDialog ref="mealselect" @selected="setSlug" />
|
||||
<BaseDialog
|
||||
title="Custom Meal"
|
||||
:title-icon="$globals.icons.primary"
|
||||
:submit-text="$t('general.save')"
|
||||
:top="true"
|
||||
ref="customMealDialog"
|
||||
@submit="pushCustomMeal"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-text-field autofocus v-model="customMeal.name" :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 cols="12" sm="12" md="6" lg="4" xl="3" v-for="(planDay, index) in value" :key="index">
|
||||
<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 SearchDialog from "../UI/Dialogs/SearchDialog";
|
||||
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
|
||||
import { api } from "@/api";
|
||||
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: name, slug: slug, description: 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>
|
|
@ -1,46 +0,0 @@
|
|||
<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) {
|
||||
let 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>
|
|
@ -1,227 +0,0 @@
|
|||
<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 v-slot: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 v-slot: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 edit @click="random" v-if="planDays.length > 0" text>
|
||||
<template v-slot:icon>
|
||||
{{ $globals.icons.diceMultiple }}
|
||||
</template>
|
||||
{{ $t("general.random") }}
|
||||
</TheButton>
|
||||
<TheButton create @click="save" :disabled="planDays.length == 0" />
|
||||
</v-card-actions>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const CREATE_EVENT = "created";
|
||||
import DatePicker from "@/components/FormHelpers/DatePicker";
|
||||
import { api } from "@/api";
|
||||
import { utils } from "@/utils";
|
||||
import MealPlanCard from "./MealPlanCard";
|
||||
export default {
|
||||
components: {
|
||||
MealPlanCard,
|
||||
DatePicker,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
planDays: [],
|
||||
items: [],
|
||||
|
||||
// Dates
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
menu1: false,
|
||||
menu2: false,
|
||||
usedRecipes: [1],
|
||||
};
|
||||
},
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
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;
|
||||
let 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;
|
||||
},
|
||||
},
|
||||
|
||||
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) => {
|
||||
let 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>
|
|
@ -1,185 +0,0 @@
|
|||
<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 }"
|
||||
>
|
||||
<ConfirmationDialog
|
||||
:title="$t('recipe.delete-recipe')"
|
||||
:message="$t('recipe.delete-confirmation')"
|
||||
color="error"
|
||||
:icon="$globals.icons.alertCircle"
|
||||
ref="deleteRecipieConfirm"
|
||||
v-on:confirm="emitDelete()"
|
||||
/>
|
||||
<v-spacer></v-spacer>
|
||||
<div v-if="!value" class="custom-btn-group ma-1">
|
||||
<FavoriteBadge class="mx-1" color="info" button-style v-if="loggedIn" :slug="slug" show-always />
|
||||
<v-tooltip bottom color="info">
|
||||
<template v-slot: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>
|
||||
<ContextMenu
|
||||
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 ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog.vue";
|
||||
import ContextMenu from "@/components/Recipe/ContextMenu.vue";
|
||||
import FavoriteBadge from "@/components/Recipe/FavoriteBadge.vue";
|
||||
|
||||
const SAVE_EVENT = "save";
|
||||
const DELETE_EVENT = "delete";
|
||||
const CLOSE_EVENT = "close";
|
||||
const JSON_EVENT = "json";
|
||||
|
||||
export default {
|
||||
components: { ConfirmationDialog, ContextMenu, FavoriteBadge },
|
||||
props: {
|
||||
slug: {
|
||||
type: String,
|
||||
},
|
||||
name: {
|
||||
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>
|
|
@ -1,167 +0,0 @@
|
|||
<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 :flat="!edit" v-if="value.length > 0">
|
||||
<v-list-item v-for="(item, i) in value" :key="i">
|
||||
<v-list-item-icon class="ma-auto">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-icon v-text="getIconDefinition(item.icon).icon" v-bind="attrs" v-on="on"></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 @click="deleteAsset(i)" top>
|
||||
<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>
|
||||
<base-dialog @submit="addAsset" :title="$t('asset.new-asset')" :title-icon="getIconDefinition(newAsset.icon).icon">
|
||||
<template v-slot:open="{ open }">
|
||||
<v-btn color="secondary" dark @click="open" v-if="edit">
|
||||
<v-icon>{{ $globals.icons.create }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card-text class="pt-2">
|
||||
<v-text-field dense v-model="newAsset.name" :label="$t('general.name')"></v-text-field>
|
||||
<div class="d-flex justify-space-between">
|
||||
<v-select
|
||||
dense
|
||||
:prepend-icon="getIconDefinition(newAsset.icon).icon"
|
||||
v-model="newAsset.icon"
|
||||
:items="iconOptions"
|
||||
item-text="title"
|
||||
item-value="name"
|
||||
class="mr-2"
|
||||
>
|
||||
<template v-slot: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 @uploaded="setFileObject" :post="false" file-name="file" :text-btn="false" />
|
||||
</div>
|
||||
{{ fileObject.name }}
|
||||
</v-card-text>
|
||||
</base-dialog>
|
||||
</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>
|
||||
|
|
@ -4,56 +4,72 @@
|
|||
:class="{ 'on-hover': hover }"
|
||||
:elevation="hover ? 12 : 2"
|
||||
:to="route ? `/recipe/${slug}` : ''"
|
||||
@click="$emit('click')"
|
||||
min-height="275"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<CardImage icon-size="200" :slug="slug" small :image-version="image">
|
||||
<RecipeCardImage icon-size="200" :slug="slug" small :image-version="image">
|
||||
<v-expand-transition v-if="description">
|
||||
<div v-if="hover" class="d-flex transition-fast-in-fast-out secondary v-card--reveal " style="height: 100%;">
|
||||
<div v-if="hover" class="d-flex transition-fast-in-fast-out secondary v-card--reveal" style="height: 100%">
|
||||
<v-card-text class="v-card--text-show white--text">
|
||||
{{ description | truncate(300) }}
|
||||
</v-card-text>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</CardImage>
|
||||
<v-card-title class="my-n3 mb-n6 ">
|
||||
</RecipeCardImage>
|
||||
<v-card-title class="my-n3 mb-n6">
|
||||
<div class="headerClass">
|
||||
{{ name }}
|
||||
</div>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-actions>
|
||||
<FavoriteBadge v-if="loggedIn" :slug="slug" show-always />
|
||||
<Rating :value="rating" :name="name" :slug="slug" :small="true" />
|
||||
<RecipeFavoriteBadge v-if="loggedIn" :slug="slug" show-always />
|
||||
<RecipeRating :value="rating" :name="name" :slug="slug" :small="true" />
|
||||
<v-spacer></v-spacer>
|
||||
<RecipeChips :truncate="true" :items="tags" :title="false" :limit="2" :small="true" :isCategory="false" />
|
||||
<ContextMenu :slug="slug" :name="name" />
|
||||
<RecipeChips :truncate="true" :items="tags" :title="false" :limit="2" :small="true" :is-category="false" />
|
||||
<RecipeContextMenu :slug="slug" :name="name" />
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FavoriteBadge from "@/components/Recipe/FavoriteBadge";
|
||||
import RecipeChips from "@/components/Recipe/RecipeViewer/RecipeChips";
|
||||
import ContextMenu from "@/components/Recipe/ContextMenu";
|
||||
import CardImage from "@/components/Recipe/CardImage";
|
||||
import Rating from "@/components/Recipe/Parts/Rating";
|
||||
import { api } from "@/api";
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge";
|
||||
import RecipeChips from "./RecipeChips";
|
||||
import RecipeContextMenu from "./RecipeContextMenu";
|
||||
import RecipeCardImage from "./RecipeCardImage";
|
||||
import RecipeRating from "./RecipeRating";
|
||||
export default {
|
||||
components: { FavoriteBadge, RecipeChips, ContextMenu, Rating, CardImage },
|
||||
components: { RecipeFavoriteBadge, RecipeChips, RecipeContextMenu, RecipeRating, RecipeCardImage },
|
||||
props: {
|
||||
name: String,
|
||||
slug: String,
|
||||
description: String,
|
||||
rating: Number,
|
||||
image: String,
|
||||
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
rating: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
image: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
route: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
tags: {
|
||||
default: true,
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
<template>
|
||||
<v-img
|
||||
@click="$emit('click')"
|
||||
:height="height"
|
||||
v-if="!fallBackImage"
|
||||
:height="height"
|
||||
:src="getImage(slug)"
|
||||
@click="$emit('click')"
|
||||
@load="fallBackImage = false"
|
||||
@error="fallBackImage = true"
|
||||
>
|
||||
<slot> </slot>
|
||||
</v-img>
|
||||
<div class="icon-slot" v-else @click="$emit('click')">
|
||||
<div v-else class="icon-slot" @click="$emit('click')">
|
||||
<v-icon color="primary" class="icon-position" :size="iconSize">
|
||||
{{ $globals.icons.primary }}
|
||||
</v-icon>
|
||||
<slot> </slot>
|
||||
<slot> </slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
import { useApi } from "~/composables/use-api";
|
||||
export default {
|
||||
props: {
|
||||
tiny: {
|
||||
|
@ -34,18 +34,32 @@ export default {
|
|||
default: null,
|
||||
},
|
||||
iconSize: {
|
||||
type: [Number, String],
|
||||
default: 100,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
imageVersion: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const api = useApi();
|
||||
|
||||
return { api };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fallBackImage: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
imageSize() {
|
||||
if (this.tiny) return "tiny";
|
||||
|
@ -59,20 +73,15 @@ export default {
|
|||
this.fallBackImage = false;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fallBackImage: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getImage(slug) {
|
||||
switch (this.imageSize) {
|
||||
case "tiny":
|
||||
return api.recipes.recipeTinyImage(slug, this.imageVersion);
|
||||
return this.api.recipes.recipeTinyImage(slug, this.imageVersion);
|
||||
case "small":
|
||||
return api.recipes.recipeSmallImage(slug, this.imageVersion);
|
||||
return this.api.recipes.recipeSmallImage(slug, this.imageVersion);
|
||||
case "large":
|
||||
return api.recipes.recipeImage(slug, this.imageVersion);
|
||||
return this.api.recipes.recipeImage(slug, this.imageVersion);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
:ripple="false"
|
||||
class="mx-auto"
|
||||
hover
|
||||
:to="this.$listeners.selected ? undefined : `/recipe/${slug}`"
|
||||
:to="$listeners.selected ? undefined : `/recipe/${slug}`"
|
||||
@click="$emit('selected')"
|
||||
>
|
||||
<v-list-item three-line>
|
||||
|
@ -20,10 +20,10 @@
|
|||
</v-icon>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class=" mb-1">{{ name }} </v-list-item-title>
|
||||
<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">
|
||||
<FavoriteBadge v-if="loggedIn" :slug="slug" show-always />
|
||||
<RecipeFavoriteBadge v-if="loggedIn" :slug="slug" show-always />
|
||||
<v-rating
|
||||
color="secondary"
|
||||
class="ml-auto"
|
||||
|
@ -34,7 +34,7 @@
|
|||
:value="rating"
|
||||
></v-rating>
|
||||
<v-spacer></v-spacer>
|
||||
<ContextMenu :slug="slug" :menu-icon="$globals.icons.dotsHorizontal" :name="name" />
|
||||
<RecipeContextMenu :slug="slug" :menu-icon="$globals.icons.dotsHorizontal" :name="name" />
|
||||
</div>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
@ -43,24 +43,41 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import FavoriteBadge from "@/components/Recipe/FavoriteBadge";
|
||||
import ContextMenu from "@/components/Recipe/ContextMenu";
|
||||
import { api } from "@/api";
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge";
|
||||
import RecipeContextMenu from "./RecipeContextMenu";
|
||||
export default {
|
||||
components: {
|
||||
FavoriteBadge,
|
||||
ContextMenu,
|
||||
RecipeFavoriteBadge,
|
||||
RecipeContextMenu,
|
||||
},
|
||||
props: {
|
||||
name: String,
|
||||
slug: String,
|
||||
description: String,
|
||||
rating: Number,
|
||||
image: String,
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
rating: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
image: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
route: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
tags: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
@ -69,16 +86,16 @@ export default {
|
|||
fallBackImage: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getImage(slug) {
|
||||
return api.recipes.recipeSmallImage(slug, this.image);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
loggedIn() {
|
||||
return this.$store.getters.getIsLoggedIn;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getImage(slug) {
|
||||
return api.recipes.recipeSmallImage(slug, this.image);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
238
frontend/components/Domain/Recipe/RecipeCardSection.vue
Normal file
238
frontend/components/Domain/Recipe/RecipeCardSection.vue
Normal file
|
@ -0,0 +1,238 @@
|
|||
<template>
|
||||
<div v-if="recipes">
|
||||
<v-app-bar v-if="!disableToolbar" color="transparent" flat class="mt-n1 flex-sm-wrap rounded">
|
||||
<v-icon v-if="title" large left>
|
||||
{{ displayTitleIcon }}
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn :icon="$vuetify.breakpoint.xsOnly" text @click="navigateRandom">
|
||||
<v-icon :left="!$vuetify.breakpoint.xsOnly">
|
||||
{{ $globals.icons.diceMultiple }}
|
||||
</v-icon>
|
||||
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.random") }}
|
||||
</v-btn>
|
||||
<v-menu v-if="$listeners.sort" offset-y left>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" :loading="sortLoading" v-on="on">
|
||||
<v-icon :left="!$vuetify.breakpoint.xsOnly">
|
||||
{{ $globals.icons.sort }}
|
||||
</v-icon>
|
||||
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.sort") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="sortRecipes(EVENTS.az)">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.orderAlphabeticalAscending }}
|
||||
</v-icon>
|
||||
<v-list-item-title>{{ $t("general.sort-alphabetically") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="sortRecipes(EVENTS.rating)">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.star }}
|
||||
</v-icon>
|
||||
<v-list-item-title>{{ $t("general.rating") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="sortRecipes(EVENTS.created)">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.newBox }}
|
||||
</v-icon>
|
||||
<v-list-item-title>{{ $t("general.created") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="sortRecipes(EVENTS.updated)">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.update }}
|
||||
</v-icon>
|
||||
<v-list-item-title>{{ $t("general.updated") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="sortRecipes(EVENTS.shuffle)">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.shuffleVariant }}
|
||||
</v-icon>
|
||||
<v-list-item-title>{{ $t("general.shuffle") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-app-bar>
|
||||
<div v-if="recipes" class="mt-2">
|
||||
<v-row v-if="!viewScale">
|
||||
<v-col v-for="recipe in recipes.slice(0, cardLimit)" :key="recipe.name" :sm="6" :md="6" :lg="4" :xl="3">
|
||||
<v-lazy>
|
||||
<RecipeCard
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
:tags="recipe.tags"
|
||||
/>
|
||||
</v-lazy>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else dense>
|
||||
<v-col
|
||||
v-for="recipe in recipes.slice(0, cardLimit)"
|
||||
:key="recipe.name"
|
||||
cols="12"
|
||||
:sm="singleColumn ? '12' : '12'"
|
||||
:md="singleColumn ? '12' : '6'"
|
||||
:lg="singleColumn ? '12' : '4'"
|
||||
:xl="singleColumn ? '12' : '3'"
|
||||
>
|
||||
<v-lazy>
|
||||
<RecipeCardMobile
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
:tags="recipe.tags"
|
||||
/>
|
||||
</v-lazy>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
<div v-intersect="bumpList" class="d-flex mt-5">
|
||||
<v-fade-transition>
|
||||
<AppLoader v-if="loading" :loading="loading" />
|
||||
</v-fade-transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { utils } from "@/utils";
|
||||
import RecipeCard from "./RecipeCard";
|
||||
import RecipeCardMobile from "./RecipeCardMobile";
|
||||
const SORT_EVENT = "sort";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RecipeCard,
|
||||
RecipeCardMobile,
|
||||
},
|
||||
props: {
|
||||
disableToolbar: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
titleIcon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
hardLimit: {
|
||||
type: Number,
|
||||
default: 99999,
|
||||
},
|
||||
mobileCards: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
singleColumn: {
|
||||
type: Boolean,
|
||||
defualt: false,
|
||||
},
|
||||
recipes: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
sortLoading: false,
|
||||
cardLimit: 50,
|
||||
loading: false,
|
||||
EVENTS: {
|
||||
az: "az",
|
||||
rating: "rating",
|
||||
created: "created",
|
||||
updated: "updated",
|
||||
shuffle: "shuffle",
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
viewScale() {
|
||||
if (this.mobileCards) return true;
|
||||
switch (this.$vuetify.breakpoint.name) {
|
||||
case "xs":
|
||||
return true;
|
||||
case "sm":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
effectiveHardLimit() {
|
||||
return Math.min(this.hardLimit, this.recipes.length);
|
||||
},
|
||||
displayTitleIcon() {
|
||||
return this.titleIcon || this.$globals.icons.tags;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
recipes() {
|
||||
this.bumpList();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
bumpList() {
|
||||
const newCardLimit = Math.min(this.cardLimit + 20, this.effectiveHardLimit);
|
||||
|
||||
if (this.loading === false && newCardLimit > this.cardLimit) {
|
||||
this.setLoader();
|
||||
}
|
||||
|
||||
this.cardLimit = newCardLimit;
|
||||
},
|
||||
async setLoader() {
|
||||
this.loading = true;
|
||||
// eslint-disable-next-line promise/param-names
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
this.loading = false;
|
||||
},
|
||||
navigateRandom() {
|
||||
const recipe = utils.recipe.randomRecipe(this.recipes);
|
||||
this.$router.push(`/recipe/${recipe.slug}`);
|
||||
},
|
||||
sortRecipes(sortType) {
|
||||
this.sortLoading = true;
|
||||
const sortTarget = [...this.recipes];
|
||||
switch (sortType) {
|
||||
case this.EVENTS.az:
|
||||
utils.recipe.sortAToZ(sortTarget);
|
||||
break;
|
||||
case this.EVENTS.rating:
|
||||
utils.recipe.sortByRating(sortTarget);
|
||||
break;
|
||||
case this.EVENTS.created:
|
||||
utils.recipe.sortByCreated(sortTarget);
|
||||
break;
|
||||
case this.EVENTS.updated:
|
||||
utils.recipe.sortByUpdated(sortTarget);
|
||||
break;
|
||||
case this.EVENTS.shuffle:
|
||||
utils.recipe.shuffle(sortTarget);
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown Event", sortType);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit(SORT_EVENT, sortTarget);
|
||||
this.sortLoading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.transparent {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
|
@ -2,14 +2,14 @@
|
|||
<div v-if="items.length > 0">
|
||||
<h2 v-if="title" class="mt-4">{{ title }}</h2>
|
||||
<v-chip
|
||||
v-for="category in items.slice(0, limit)"
|
||||
:key="category"
|
||||
label
|
||||
class="ma-1"
|
||||
color="accent"
|
||||
:small="small"
|
||||
dark
|
||||
v-for="category in items.slice(0, limit)"
|
||||
:to="`/recipes/${urlParam}/${getSlug(category)}`"
|
||||
:key="category"
|
||||
>
|
||||
{{ truncateText(category) }}
|
||||
</v-chip>
|
||||
|
@ -20,24 +20,33 @@
|
|||
export default {
|
||||
props: {
|
||||
truncate: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
items: {
|
||||
default: [],
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
title: {
|
||||
default: null,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isCategory: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 999,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxWidth: {},
|
||||
maxWidth: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
allCategories() {
|
||||
|
@ -55,19 +64,19 @@ export default {
|
|||
if (!name) return;
|
||||
|
||||
if (this.isCategory) {
|
||||
const matches = this.allCategories.filter(x => x.name == name);
|
||||
const matches = this.allCategories.filter((x) => x.name === name);
|
||||
if (matches.length > 0) return matches[0].slug;
|
||||
} else {
|
||||
const matches = this.allTags.filter(x => x.name == name);
|
||||
const matches = this.allTags.filter((x) => x.name === name);
|
||||
if (matches.length > 0) return matches[0].slug;
|
||||
}
|
||||
},
|
||||
truncateText(text, length = 20, clamp) {
|
||||
if (!this.truncate) return text;
|
||||
clamp = clamp || "...";
|
||||
var node = document.createElement("div");
|
||||
const node = document.createElement("div");
|
||||
node.innerHTML = text;
|
||||
var content = node.textContent;
|
||||
const content = node.textContent;
|
||||
return content.length > length ? content.slice(0, length) + clamp : content;
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
<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 class="ma-2" v-for="(comment, index) in comments" :key="comment.id">
|
||||
<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
|
||||
small
|
||||
minor
|
||||
v-if="!editKeys[comment.id] && (user.admin || comment.user.id === user.id)"
|
||||
delete
|
||||
@click="deleteComment(comment.id)"
|
||||
/>
|
||||
<TheButton
|
||||
small
|
||||
v-if="!editKeys[comment.id] && comment.user.id === user.id"
|
||||
edit
|
||||
@click="editComment(comment.id)"
|
||||
/>
|
||||
<TheButton small v-else-if="editKeys[comment.id]" 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 auto-grow row-height="1" outlined v-model="newComment"> </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>
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<ConfirmationDialog
|
||||
<BaseDialog
|
||||
ref="deleteRecipieConfirm"
|
||||
:title="$t('recipe.delete-recipe')"
|
||||
:message="$t('recipe.delete-confirmation')"
|
||||
color="error"
|
||||
:icon="$globals.icons.alertCircle"
|
||||
ref="deleteRecipieConfirm"
|
||||
v-on:confirm="deleteRecipe()"
|
||||
@confirm="deleteRecipe()"
|
||||
/>
|
||||
<v-menu
|
||||
offset-y
|
||||
|
@ -20,7 +20,7 @@
|
|||
open-on-hover
|
||||
content-class="d-print-none"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn :fab="fab" :small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent>
|
||||
<v-icon>{{ effMenuIcon }}</v-icon>
|
||||
</v-btn>
|
||||
|
@ -28,7 +28,7 @@
|
|||
<v-list dense>
|
||||
<v-list-item v-for="(item, index) in displayedMenu" :key="index" @click="menuAction(item.action)">
|
||||
<v-list-item-icon>
|
||||
<v-icon v-text="item.icon" :color="item.color"></v-icon>
|
||||
<v-icon :color="item.color" v-text="item.icon"></v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
@ -38,13 +38,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog.vue";
|
||||
import { api } from "@/api";
|
||||
import { utils } from "@/utils";
|
||||
export default {
|
||||
components: {
|
||||
ConfirmationDialog,
|
||||
},
|
||||
props: {
|
||||
menuTop: {
|
||||
type: Boolean,
|
||||
|
@ -76,6 +72,11 @@ export default {
|
|||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
effMenuIcon() {
|
||||
return this.menuIcon ? this.menuIcon : this.$globals.icons.dotsVertical;
|
||||
|
@ -143,11 +144,6 @@ export default {
|
|||
return this.$t("recipe.share-recipe-message", [this.name]);
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async menuAction(action) {
|
||||
this.loading = true;
|
||||
|
@ -165,7 +161,7 @@ export default {
|
|||
url: this.recipeURL,
|
||||
})
|
||||
.then(() => console.log("Successful share"))
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.log("WebShareAPI not supported", error);
|
||||
this.updateClipboard();
|
||||
});
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-dialog v-model="dialog" width="600">
|
||||
<template v-slot: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() {
|
||||
let 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>
|
|
@ -1,85 +0,0 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-dialog v-model="dialog" width="700">
|
||||
<template v-slot: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 align="center" v-for="(value, key, index) in extras" :key="index">
|
||||
<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 :label="$t('recipe.object-value')" v-model="extras[key]"> </v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions>
|
||||
<v-form ref="addKey">
|
||||
<v-text-field
|
||||
:label="$t('recipe.new-key-name')"
|
||||
v-model="newKeyName"
|
||||
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>
|
|
@ -1,14 +1,14 @@
|
|||
<template>
|
||||
<v-tooltip bottom nudge-right="50" :color="buttonStyle ? 'info' : 'secondary'">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn
|
||||
small
|
||||
@click.prevent="toggleFavorite"
|
||||
v-if="isFavorite || showAlways"
|
||||
small
|
||||
:color="buttonStyle ? 'info' : 'secondary'"
|
||||
:icon="!buttonStyle"
|
||||
:fab="buttonStyle"
|
||||
v-bind="attrs"
|
||||
@click.prevent="toggleFavorite"
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon :small="!buttonStyle" :color="buttonStyle ? 'white' : 'secondary'">
|
||||
|
@ -25,6 +25,7 @@ import { api } from "@/api";
|
|||
export default {
|
||||
props: {
|
||||
slug: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
showAlways: {
|
||||
|
@ -41,7 +42,7 @@ export default {
|
|||
return this.$store.getters.getUserData;
|
||||
},
|
||||
isFavorite() {
|
||||
return this.user.favoriteRecipes.indexOf(this.slug) !== -1;
|
||||
return this.user.favoriteRecipes.includes(this.slug);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-menu offset-y top nudge-top="6" :close-on-content-click="false">
|
||||
<template v-slot: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"
|
||||
@uploaded="uploadImage"
|
||||
:post="false"
|
||||
/>
|
||||
</v-card-title>
|
||||
<v-card-text class="mt-n5">
|
||||
<div>
|
||||
<v-text-field :label="$t('general.url')" class="pt-5" clearable v-model="url" :messages="getMessages()">
|
||||
<template v-slot:append-outer>
|
||||
<v-btn class="ml-2" color="primary" @click="getImageFromURL" :loading="loading" :disabled="!slug">
|
||||
{{ $t("general.get") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const REFRESH_EVENT = "refresh";
|
||||
const UPLOAD_EVENT = "upload";
|
||||
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
|
||||
import { api } from "@/api";
|
||||
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>
|
|
@ -1,167 +0,0 @@
|
|||
<template>
|
||||
<div v-if="edit || (value && value.length > 0)">
|
||||
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
|
||||
<div v-if="edit">
|
||||
<draggable :value="value" @input="updateIndex" @start="drag = true" @end="drag = false" handle=".handle">
|
||||
<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]"
|
||||
class="mx-3 mt-3"
|
||||
v-model="value[index].title"
|
||||
dense
|
||||
:label="$t('recipe.section-title')"
|
||||
>
|
||||
</v-text-field>
|
||||
|
||||
<v-textarea
|
||||
class="mr-2"
|
||||
:label="$t('recipe.ingredient')"
|
||||
v-model="value[index].note"
|
||||
auto-grow
|
||||
solo
|
||||
dense
|
||||
rows="1"
|
||||
>
|
||||
<template slot="append">
|
||||
<v-tooltip right nudge-right="10">
|
||||
<template v-slot: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 class="mr-n1" slot="prepend" 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 @bulk-data="addIngredient" class="mr-2" />
|
||||
<v-btn color="secondary" dark @click="addIngredient" class="mr-4">
|
||||
<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 class="mt-2" v-if="showTitleEditor[index]">{{ 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>
|
||||
<vue-markdown class="ma-0 pa-0 text-subtitle-1 dense-markdown" :source="ingredient.note"> </vue-markdown>
|
||||
</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: [],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.checked = this.value.map(() => false);
|
||||
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler() {
|
||||
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>
|
|
@ -1,169 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<h2 class="mb-4">{{ $t("recipe.instructions") }}</h2>
|
||||
<div>
|
||||
<draggable
|
||||
:disabled="!edit"
|
||||
:value="value"
|
||||
@input="updateIndex"
|
||||
@start="drag = true"
|
||||
@end="drag = false"
|
||||
handle=".handle"
|
||||
>
|
||||
<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 class="headline" v-if="!edit">
|
||||
<v-app-bar-title v-text="step.title"> </v-app-bar-title>
|
||||
</v-toolbar-title>
|
||||
<v-text-field
|
||||
v-if="edit"
|
||||
class="headline pa-0 mt-5"
|
||||
v-model="step.title"
|
||||
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
|
||||
auto-grow
|
||||
dense
|
||||
v-model="value[index]['text']"
|
||||
:key="generateKey('instructions', index)"
|
||||
rows="4"
|
||||
>
|
||||
</v-textarea>
|
||||
</v-card-text>
|
||||
<v-expand-transition>
|
||||
<div class="m-0 p-0" v-show="!isChecked(index) && !edit">
|
||||
<v-card-text>
|
||||
<vue-markdown :source="step.text"> </vue-markdown>
|
||||
</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: [],
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
|
||||
},
|
||||
|
||||
watch: {
|
||||
value: {
|
||||
handler() {
|
||||
this.disabledSteps = [];
|
||||
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)) {
|
||||
let 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 {
|
||||
return;
|
||||
}
|
||||
},
|
||||
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>
|
||||
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
<template>
|
||||
<div v-if="value.length > 0 || edit">
|
||||
<h2 class="my-4">{{ $t("recipe.note") }}</h2>
|
||||
<v-card class="mt-1" v-for="(note, index) in value" :key="generateKey('note', index)">
|
||||
<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 :label="$t('recipe.title')" v-model="value[index]['title']"></v-text-field>
|
||||
</v-row>
|
||||
|
||||
<v-textarea auto-grow :placeholder="$t('recipe.note')" v-model="value[index]['text']"> </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>
|
||||
<vue-markdown :source="note.text"> </vue-markdown>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<div class="d-flex justify-end" v-if="edit">
|
||||
<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>
|
|
@ -1,100 +0,0 @@
|
|||
<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 dense v-if="showViewer" 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>
|
|
@ -1,155 +0,0 @@
|
|||
<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 :prepTime="recipe.prepTime" :totalTime="recipe.totalTime" :performTime="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>
|
||||
<vue-markdown :source="recipe.description"> </vue-markdown>
|
||||
<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>
|
||||
<vue-markdown :source="step.text"> </vue-markdown>
|
||||
</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>
|
||||
<vue-markdown :source="note.text"> </vue-markdown>
|
||||
</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>
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div @click.prevent>
|
||||
<v-rating
|
||||
v-model="rating"
|
||||
:readonly="!loggedIn"
|
||||
color="secondary"
|
||||
background-color="secondary lighten-3"
|
||||
|
@ -8,7 +9,6 @@
|
|||
:dense="small ? true : undefined"
|
||||
:size="small ? 15 : undefined"
|
||||
hover
|
||||
v-model="rating"
|
||||
:value="value"
|
||||
@input="updateRating"
|
||||
@click="updateRating"
|
||||
|
@ -21,18 +21,26 @@ import { api } from "@/api";
|
|||
export default {
|
||||
props: {
|
||||
emitOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
name: String,
|
||||
slug: String,
|
||||
value: Number,
|
||||
name: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.rating = this.value;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
rating: 0,
|
||||
|
@ -43,6 +51,9 @@ export default {
|
|||
return this.$store.getters.getIsLoggedIn;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.rating = this.value;
|
||||
},
|
||||
methods: {
|
||||
updateRating(val) {
|
||||
if (this.emitOnly) {
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-menu offset-y top nudge-top="6" :close-on-content-click="false">
|
||||
<template v-slot: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
|
||||
dense
|
||||
v-for="(itemValue, key) in value"
|
||||
:key="key"
|
||||
v-model="value[key]"
|
||||
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>
|
|
@ -1,52 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-chip label color="accent custom-transparent" class="ma-1" v-for="(time, index) in allTimes" :key="index">
|
||||
<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>
|
|
@ -1,61 +0,0 @@
|
|||
<template>
|
||||
<v-list-item two-line to="/admin/profile">
|
||||
<v-list-item-avatar color="accent" class="white--text">
|
||||
<v-img :src="profileImage" v-if="!noImage" />
|
||||
<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>
|
|
@ -1,15 +1,270 @@
|
|||
<template>
|
||||
<div></div>
|
||||
<div class="text-center d-print-none">
|
||||
<BaseDialog
|
||||
ref="domImportFromUrlDialog"
|
||||
:title="$t('new-recipe.from-url')"
|
||||
:icon="$globals.icons.link"
|
||||
:submit-text="$t('general.create')"
|
||||
:loading="processing"
|
||||
width="600px"
|
||||
@submit="uploadZip"
|
||||
>
|
||||
<v-form ref="urlForm" @submit.prevent="createRecipe">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="recipeURL"
|
||||
:label="$t('new-recipe.recipe-url')"
|
||||
validate-on-blur
|
||||
autofocus
|
||||
filled
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
:rules="[isValidWebUrl]"
|
||||
:hint="$t('new-recipe.url-form-hint')"
|
||||
persistent-hint
|
||||
></v-text-field>
|
||||
|
||||
<v-expand-transition>
|
||||
<v-alert v-show="error" color="error" class="mt-6 white--text">
|
||||
<v-card-title class="ma-0 pa-0">
|
||||
<v-icon left color="white" x-large> {{ $globals.icons.robot }} </v-icon>
|
||||
{{ $t("new-recipe.error-title") }}
|
||||
</v-card-title>
|
||||
<v-divider class="my-3 mx-2"></v-divider>
|
||||
|
||||
<p>
|
||||
{{ $t("new-recipe.error-details") }}
|
||||
</p>
|
||||
<div class="d-flex row justify-space-around my-3 force-white">
|
||||
<a
|
||||
class="dark"
|
||||
href="https://developers.google.com/search/docs/data-types/recipe"
|
||||
target="_blank"
|
||||
rel="noreferrer nofollow"
|
||||
>
|
||||
{{ $t("new-recipe.google-ld-json-info") }}
|
||||
</a>
|
||||
<a href="https://github.com/hay-kot/mealie/issues" target="_blank" rel="noreferrer nofollow">
|
||||
{{ $t("new-recipe.github-issues") }}
|
||||
</a>
|
||||
<a href="https://schema.org/Recipe" target="_blank" rel="noreferrer nofollow">
|
||||
{{ $t("new-recipe.recipe-markup-specification") }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="d-flex justify-end">
|
||||
<v-btn
|
||||
white
|
||||
outlined
|
||||
:to="{ path: '/recipes/debugger', query: { test_url: recipeURL } }"
|
||||
@click="addRecipe = false"
|
||||
>
|
||||
<v-icon left> {{ $globals.icons.externalLink }} </v-icon>
|
||||
{{ $t("new-recipe.view-scraped-data") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-alert>
|
||||
</v-expand-transition>
|
||||
</v-card-text>
|
||||
</v-form>
|
||||
</BaseDialog>
|
||||
<BaseDialog
|
||||
ref="domUploadZipDialog"
|
||||
:title="$t('new-recipe.upload-a-recipe')"
|
||||
:icon="$globals.icons.zip"
|
||||
:submit-text="$t('general.import')"
|
||||
:loading="processing"
|
||||
@submit="uploadZip"
|
||||
>
|
||||
<v-card-text class="mt-1 pb-0">
|
||||
{{ $t("new-recipe.upload-individual-zip-file") }}
|
||||
|
||||
<div class="headline mx-auto mb-0 pb-0 text-center">
|
||||
{{ fileName }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<!-- <TheUploadBtn class="mx-auto" :text-btn="false" :post="false" @uploaded="setFile"> </TheUploadBtn> -->
|
||||
</v-card-actions>
|
||||
</BaseDialog>
|
||||
<BaseDialog
|
||||
ref="domCreateDialog"
|
||||
:icon="$globals.icons.primary"
|
||||
title="Create A Recipe"
|
||||
@submit="manualCreateRecipe()"
|
||||
>
|
||||
<v-card-text class="mt-5">
|
||||
<v-form>
|
||||
<AutoForm v-model="createRecipeData.form" :items="createRecipeData.items" />
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
<v-speed-dial v-model="fab" :open-on-hover="absolute" :fixed="absolute" :bottom="absolute" :right="absolute">
|
||||
<template #activator>
|
||||
<v-btn v-model="fab" :color="absolute ? 'accent' : 'white'" dark :icon="!absolute" :fab="absolute">
|
||||
<v-icon> {{ $globals.icons.createAlt }} </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<v-tooltip left dark color="primary">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn fab dark small color="primary" v-bind="attrs" v-on="on" @click="domImportFromUrlDialog.open()">
|
||||
<v-icon>{{ $globals.icons.link }} </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t("new-recipe.from-url") }}</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip left dark color="accent">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn fab dark small color="accent" v-bind="attrs" v-on="on" @click="domCreateDialog.open()">
|
||||
<v-icon>{{ $globals.icons.edit }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t("general.new") }}</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip left dark color="info">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn fab dark small color="info" v-bind="attrs" v-on="on" @click="domUploadZipDialog.open()">
|
||||
<v-icon>{{ $globals.icons.zip }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t("general.upload") }}</span>
|
||||
</v-tooltip>
|
||||
</v-speed-dial>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@nuxtjs/composition-api'
|
||||
// import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn.vue";
|
||||
import { defineComponent, ref } from "@nuxtjs/composition-api";
|
||||
import { fieldTypes } from "~/composables/forms";
|
||||
import { useApi } from "~/composables/use-api";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
absolute: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
const domCreateDialog = ref(null);
|
||||
const domUploadZipDialog = ref(null);
|
||||
const domImportFromUrlDialog = ref(null);
|
||||
|
||||
const api = useApi();
|
||||
|
||||
return { domCreateDialog, domUploadZipDialog, domImportFromUrlDialog, api };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
error: false,
|
||||
fab: false,
|
||||
addRecipe: false,
|
||||
processing: false,
|
||||
uploadData: {
|
||||
fileName: "archive",
|
||||
file: null,
|
||||
},
|
||||
createRecipeData: {
|
||||
items: [
|
||||
{
|
||||
label: "Recipe Name",
|
||||
varName: "name",
|
||||
type: fieldTypes.TEXT,
|
||||
rules: ["required"],
|
||||
},
|
||||
],
|
||||
form: {
|
||||
name: "",
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
recipeURL: {
|
||||
set(recipe_import_url: string) {
|
||||
this.$router.replace({ query: { ...this.$route.query, recipe_import_url } });
|
||||
},
|
||||
get(): string {
|
||||
return this.$route.query.recipe_import_url || "";
|
||||
},
|
||||
},
|
||||
fileName(): string {
|
||||
if (this.uploadData?.file?.name) {
|
||||
return this.uploadData.file.name;
|
||||
}
|
||||
return "";
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.$route.query.recipe_import_url) {
|
||||
this.addRecipe = true;
|
||||
this.createRecipe();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async manualCreateRecipe() {
|
||||
console.log(this.createRecipeData.form);
|
||||
await this.api.recipes.createOne(this.createRecipeData.form.name);
|
||||
},
|
||||
|
||||
resetVars() {
|
||||
this.uploadData = {
|
||||
fileName: "archive",
|
||||
file: null,
|
||||
};
|
||||
},
|
||||
setFile(file) {
|
||||
this.uploadData.file = file;
|
||||
console.log("Uploaded");
|
||||
},
|
||||
openZipUploader() {
|
||||
this.resetVars();
|
||||
this.$refs.uploadZipDialog.open();
|
||||
},
|
||||
async uploadZip() {
|
||||
const formData = new FormData();
|
||||
formData.append(this.uploadData.fileName, this.uploadData.file);
|
||||
|
||||
const response = await api.utils.uploadFile("/api/recipes/create-from-zip", formData);
|
||||
|
||||
this.$router.push(`/recipe/${response.data.slug}`);
|
||||
},
|
||||
async createRecipe() {
|
||||
this.error = false;
|
||||
if (this.$refs.urlForm === undefined || this.$refs.urlForm.validate()) {
|
||||
this.processing = true;
|
||||
const response = await api.recipes.createByURL(this.recipeURL);
|
||||
this.processing = false;
|
||||
if (response) {
|
||||
this.addRecipe = false;
|
||||
this.recipeURL = "";
|
||||
this.$router.push(`/recipe/${response.data}`);
|
||||
} else {
|
||||
this.error = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
this.fab = false;
|
||||
this.error = false;
|
||||
this.addRecipe = false;
|
||||
this.recipeURL = "";
|
||||
this.processing = false;
|
||||
},
|
||||
isValidWebUrl(url: string) {
|
||||
const regEx =
|
||||
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,256}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/gm;
|
||||
return regEx.test(url) ? true : this.$t("new-recipe.must-be-a-valid-url");
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,15 +1,29 @@
|
|||
<template>
|
||||
<div></div>
|
||||
<v-footer color="primary lighten-1" padless app>
|
||||
<v-row justify="center" align="center" dense no-gutters>
|
||||
<v-col class="primary py-2 text-center white--text" cols="12">
|
||||
<v-btn dark icon href="https://github.com/hay-kot/mealie" target="_blank">
|
||||
<v-icon>
|
||||
{{ $globals.icons.github }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
{{ new Date().getFullYear() }} — <strong>Mealie</strong>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-footer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@nuxtjs/composition-api'
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
return {};
|
||||
},
|
||||
data: () => ({
|
||||
links: ["Home", "About Us", "Team", "Services", "Blog", "Contact Us"],
|
||||
}),
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,16 +1,152 @@
|
|||
<template>
|
||||
<div></div>
|
||||
<v-app-bar clipped-left dense app color="primary" dark class="d-print-none">
|
||||
<slot />
|
||||
<router-link to="/">
|
||||
<v-btn icon>
|
||||
<v-icon size="40"> {{ $globals.icons.primary }} </v-icon>
|
||||
</v-btn>
|
||||
</router-link>
|
||||
|
||||
<div btn class="pl-2">
|
||||
<v-toolbar-title style="cursor: pointer" @click="$router.push('/')"> Mealie </v-toolbar-title>
|
||||
</div>
|
||||
|
||||
{{ value }}
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<!-- <v-tooltip bottom>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn icon class="mr-1" small v-bind="attrs" v-on="on">
|
||||
<v-icon v-text="isDark ? $globals.icons.weatherSunny : $globals.icons.weatherNight"> </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ isDark ? $t("settings.theme.switch-to-light-mode") : $t("settings.theme.switch-to-dark-mode") }}</span>
|
||||
</v-tooltip> -->
|
||||
<!-- <div v-if="false" style="width: 350px"></div>
|
||||
<div v-else>
|
||||
<v-btn icon @click="$refs.recipeSearch.open()">
|
||||
<v-icon> {{ $globals.icons.search }} </v-icon>
|
||||
</v-btn>
|
||||
</div> -->
|
||||
|
||||
<!-- Navigation Menu -->
|
||||
<v-menu
|
||||
v-if="menu"
|
||||
transition="slide-x-transition"
|
||||
bottom
|
||||
right
|
||||
offset-y
|
||||
offset-overflow
|
||||
open-on-hover
|
||||
close-delay="200"
|
||||
>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn v-bind="attrs" icon v-on="on">
|
||||
<v-icon>{{ $globals.icons.user }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item-group v-model="itemSelected" color="primary">
|
||||
<v-list-item
|
||||
v-for="(item, i) in filteredItems"
|
||||
:key="i"
|
||||
link
|
||||
:to="item.nav ? item.nav : null"
|
||||
@click="item.logout ? $auth.logout() : null"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ item.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ item.title }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-app-bar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@nuxtjs/composition-api'
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
menu: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
return {};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
itemSelected: null,
|
||||
items: [
|
||||
{
|
||||
icon: this.$globals.icons.user,
|
||||
title: this.$t("user.login"),
|
||||
restricted: false,
|
||||
nav: "/user/login",
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.calendarWeek,
|
||||
title: this.$t("meal-plan.dinner-this-week"),
|
||||
nav: "/meal-plan/this-week",
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.calendarToday,
|
||||
title: this.$t("meal-plan.dinner-today"),
|
||||
nav: "/meal-plan/today",
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.calendarMultiselect,
|
||||
title: this.$t("meal-plan.planner"),
|
||||
nav: "/meal-plan/planner",
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.formatListCheck,
|
||||
title: this.$t("shopping-list.shopping-lists"),
|
||||
nav: "/shopping-list",
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.logout,
|
||||
title: this.$t("user.logout"),
|
||||
restricted: true,
|
||||
logout: true,
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.cog,
|
||||
title: this.$t("general.settings"),
|
||||
nav: "/user/profile",
|
||||
restricted: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredItems(): Array<any> {
|
||||
if (this.loggedIn) {
|
||||
return this.items.filter((x) => x.restricted === true);
|
||||
} else {
|
||||
return this.items.filter((x) => x.restricted === false);
|
||||
}
|
||||
},
|
||||
loggedIn(): Boolean {
|
||||
return this.$auth.loggedIn;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
|
@ -1,16 +1,122 @@
|
|||
<template>
|
||||
<div></div>
|
||||
<v-navigation-drawer :value="value" clipped app width="200px">
|
||||
<!-- User Profile -->
|
||||
<template v-if="$auth.user">
|
||||
<v-list-item two-line to="/user/profile">
|
||||
<v-list-item-avatar color="accent" class="white--text">
|
||||
<v-img :src="require(`~/static/account.png`)" />
|
||||
</v-list-item-avatar>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title> {{ $auth.user.fullName }}</v-list-item-title>
|
||||
<v-list-item-subtitle> {{ $auth.user.admin ? $t("user.admin") : $t("user.user") }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
</template>
|
||||
|
||||
<!-- Primary Links -->
|
||||
<v-list nav dense>
|
||||
<v-list-item-group v-model="topSelected" color="primary">
|
||||
<v-list-item v-for="nav in topLink" :key="nav.title" link :to="nav.to">
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ nav.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>{{ nav.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
|
||||
<!-- Secondary Links -->
|
||||
<template v-if="secondaryLinks">
|
||||
<v-divider></v-divider>
|
||||
<v-list nav dense>
|
||||
<v-list-item-group v-model="secondarySelected" color="primary">
|
||||
<v-list-item v-for="nav in secondaryLinks" :key="nav.title" link :to="nav.to">
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ nav.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>{{ nav.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<!-- Bottom Navigation Links -->
|
||||
<template v-if="bottomLinks">
|
||||
<v-list class="fixedBottom" nav dense>
|
||||
<v-list-item-group v-model="bottomSelected" color="primary">
|
||||
<v-list-item
|
||||
v-for="nav in bottomLinks"
|
||||
:key="nav.title"
|
||||
link
|
||||
:to="nav.to || null"
|
||||
:href="nav.href || null"
|
||||
:target="nav.href ? '_blank' : null"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ nav.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>{{ nav.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@nuxtjs/composition-api'
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { SidebarLinks } from "~/types/application-types";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
user: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
topLink: {
|
||||
type: Array as () => SidebarLinks,
|
||||
required: true,
|
||||
},
|
||||
secondaryLinks: {
|
||||
type: Array as () => SidebarLinks,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
bottomLinks: {
|
||||
type: Array as () => SidebarLinks,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
return {};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
topSelected: null,
|
||||
secondarySelected: null,
|
||||
bottomSelected: null,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style>
|
||||
.fixedBottom {
|
||||
position: fixed !important;
|
||||
bottom: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,153 +0,0 @@
|
|||
<template>
|
||||
<v-form ref="form">
|
||||
<v-card-text>
|
||||
<v-row dense>
|
||||
<ImageUploadBtn class="my-1" @upload="uploadImage" :slug="value.slug" @refresh="$emit('upload')" />
|
||||
<SettingsMenu class="my-1 mx-1" @upload="uploadImage" :value="value.settings" />
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<v-col>
|
||||
<v-text-field :label="$t('recipe.total-time')" v-model="value.totalTime"></v-text-field>
|
||||
</v-col>
|
||||
<v-col><v-text-field :label="$t('recipe.prep-time')" v-model="value.prepTime"></v-text-field></v-col>
|
||||
<v-col><v-text-field :label="$t('recipe.perform-time')" v-model="value.performTime"></v-text-field></v-col>
|
||||
</v-row>
|
||||
<v-text-field class="my-3" :label="$t('recipe.recipe-name')" v-model="value.name" :rules="[existsRule]">
|
||||
</v-text-field>
|
||||
<v-textarea auto-grow min-height="100" :label="$t('recipe.description')" v-model="value.description">
|
||||
</v-textarea>
|
||||
<div class="my-2"></div>
|
||||
<v-row dense disabled>
|
||||
<v-col sm="4">
|
||||
<v-text-field :label="$t('recipe.servings')" v-model="value.recipeYield" 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 :edit="true" v-model="value.recipeIngredient" />
|
||||
<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
|
||||
:return-object="false"
|
||||
v-model="value.recipeCategory"
|
||||
: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
|
||||
:return-object="false"
|
||||
v-model="value.tags"
|
||||
: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 @bulk-data="appendSteps" class="mr-2" />
|
||||
<v-btn color="secondary" dark @click="addStep" class="mr-4">
|
||||
<v-icon>{{ $globals.icons.create }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<Notes :edit="true" v-model="value.notes" />
|
||||
|
||||
<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>
|
||||
const UPLOAD_EVENT = "upload";
|
||||
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";
|
||||
export default {
|
||||
components: {
|
||||
BulkAdd,
|
||||
ExtrasEditor,
|
||||
CategoryTagSelector,
|
||||
Nutrition,
|
||||
ImageUploadBtn,
|
||||
Instructions,
|
||||
Ingredients,
|
||||
Assets,
|
||||
Notes,
|
||||
SettingsMenu,
|
||||
Rating,
|
||||
},
|
||||
props: {
|
||||
value: Object,
|
||||
},
|
||||
mixins: [validators],
|
||||
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>
|
|
@ -1,137 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-card-title class="headline">
|
||||
{{ recipe.name }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<vue-markdown :source="recipe.description"> </vue-markdown>
|
||||
<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 :value="recipe.rating" :name="recipe.name" :slug="recipe.slug" :key="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 class="mt-2" v-if="recipe.recipeCategory.length > 0">
|
||||
<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 class="mt-2" v-if="recipe.tags.length > 0">
|
||||
<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" :isCategory="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 RecipeChips from "./RecipeChips";
|
||||
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";
|
||||
export default {
|
||||
components: {
|
||||
VueMarkdown,
|
||||
RecipeChips,
|
||||
Notes,
|
||||
Ingredients,
|
||||
Nutrition,
|
||||
Instructions,
|
||||
Assets,
|
||||
Rating,
|
||||
},
|
||||
props: {
|
||||
recipe: Object,
|
||||
},
|
||||
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>
|
68
frontend/components/global/AppLoader.vue
Normal file
68
frontend/components/global/AppLoader.vue
Normal file
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<div class="mx-auto">
|
||||
<v-progress-circular :width="size.width" :size="size.size" color="primary lighten-2" indeterminate>
|
||||
<div class="text-center">
|
||||
<v-icon :size="size.icon" color="primary lighten-2">
|
||||
{{ $globals.icons.primary }}
|
||||
</v-icon>
|
||||
<div v-if="large" class="text-small">
|
||||
<slot>
|
||||
{{ small ? "" : waitingText }}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</v-progress-circular>
|
||||
<div v-if="!large" class="text-small">
|
||||
<slot>
|
||||
{{ small ? "" : waitingText }}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
loading: {
|
||||
default: true,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
medium: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
large: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
size() {
|
||||
if (this.small) {
|
||||
return {
|
||||
width: 2,
|
||||
icon: 30,
|
||||
size: 50,
|
||||
};
|
||||
} else if (this.large) {
|
||||
return {
|
||||
width: 4,
|
||||
icon: 120,
|
||||
size: 200,
|
||||
};
|
||||
}
|
||||
return {
|
||||
width: 3,
|
||||
icon: 75,
|
||||
size: 125,
|
||||
};
|
||||
},
|
||||
waitingText() {
|
||||
return this.$t("general.loading-recipes");
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
264
frontend/components/global/AutoForm.vue
Normal file
264
frontend/components/global/AutoForm.vue
Normal file
|
@ -0,0 +1,264 @@
|
|||
<template>
|
||||
<v-card :color="color" :dark="dark" flat :width="width" class="my-2">
|
||||
<v-row>
|
||||
<v-col v-for="(inputField, index) in items" :key="index" class="py-0" cols="12" sm="12">
|
||||
<v-divider v-if="inputField.section" class="my-2" />
|
||||
<v-card-title v-if="inputField.section" class="pl-0">
|
||||
{{ inputField.section }}
|
||||
</v-card-title>
|
||||
<v-card-text v-if="inputField.sectionDetails" class="pl-0 mt-0 pt-0">
|
||||
{{ inputField.sectionDetails }}
|
||||
</v-card-text>
|
||||
|
||||
<!-- Check Box -->
|
||||
<v-checkbox
|
||||
v-if="inputField.type === fieldTypes.BOOLEAN"
|
||||
v-model="value[inputField.varName]"
|
||||
class="my-0 py-0"
|
||||
:label="inputField.label"
|
||||
:name="inputField.varName"
|
||||
:hint="inputField.hint || ''"
|
||||
@change="emitBlur"
|
||||
/>
|
||||
|
||||
<!-- Text Field -->
|
||||
<v-text-field
|
||||
v-else-if="inputField.type === fieldTypes.TEXT"
|
||||
v-model="value[inputField.varName]"
|
||||
:readonly="inputField.fixed && updateMode"
|
||||
filled
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
dense
|
||||
:label="inputField.label"
|
||||
:name="inputField.varName"
|
||||
:hint="inputField.hint || ''"
|
||||
:rules="[...rulesByKey(inputField.rules), ...defaultRules]"
|
||||
lazy-validation
|
||||
@blur="emitBlur"
|
||||
/>
|
||||
|
||||
<!-- Text Area -->
|
||||
<v-textarea
|
||||
v-else-if="inputField.type === fieldTypes.TEXT_AREA"
|
||||
v-model="value[inputField.varName]"
|
||||
:readonly="inputField.fixed && updateMode"
|
||||
filled
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
rows="3"
|
||||
auto-grow
|
||||
dense
|
||||
:label="inputField.label"
|
||||
:name="inputField.varName"
|
||||
:hint="inputField.hint || ''"
|
||||
:rules="[...rulesByKey(inputField.rules), ...defaultRules]"
|
||||
lazy-validation
|
||||
@blur="emitBlur"
|
||||
/>
|
||||
|
||||
<!-- Option Select -->
|
||||
<v-select
|
||||
v-else-if="inputField.type === fieldTypes.SELECT"
|
||||
v-model="value[inputField.varName]"
|
||||
:readonly="inputField.fixed && updateMode"
|
||||
filled
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
:prepend-icon="inputField.icons ? value[inputField.varName] : null"
|
||||
:label="inputField.label"
|
||||
:name="inputField.varName"
|
||||
:items="inputField.options"
|
||||
:return-object="false"
|
||||
lazy-validation
|
||||
@blur="emitBlur"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ item.text }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ item.description }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</template>
|
||||
</v-select>
|
||||
|
||||
<!-- Color Picker -->
|
||||
<div v-else-if="inputField.type === fieldTypes.COLOR" class="d-flex" style="width: 100%">
|
||||
<v-menu offset-y>
|
||||
<template #activator="{ on }">
|
||||
<v-btn class="my-2 ml-auto" style="min-width: 200px" :color="value[inputField.varName]" dark v-on="on">
|
||||
{{ inputField.label }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-color-picker
|
||||
v-model="value[inputField.varName]"
|
||||
value="#7417BE"
|
||||
hide-canvas
|
||||
hide-inputs
|
||||
show-swatches
|
||||
class="mx-auto"
|
||||
@input="emitBlur"
|
||||
/>
|
||||
</v-menu>
|
||||
</div>
|
||||
|
||||
<div v-else-if="inputField.type === fieldTypes.OBJECT">
|
||||
<auto-form v-model="value[inputField.varName]" :color="color" :items="inputField.items" @blur="emitBlur" />
|
||||
</div>
|
||||
|
||||
<!-- List Type -->
|
||||
<div v-else-if="inputField.type === fieldTypes.LIST">
|
||||
<div v-for="(item, idx) in value[inputField.varName]" :key="idx">
|
||||
<p>
|
||||
{{ inputField.label }} {{ idx + 1 }}
|
||||
<span>
|
||||
<BaseButton class="ml-5" x-small delete @click="removeByIndex(value[inputField.varName], idx)" />
|
||||
</span>
|
||||
</p>
|
||||
<v-divider class="mb-5 mx-2" />
|
||||
<auto-form
|
||||
v-model="value[inputField.varName][idx]"
|
||||
:color="color"
|
||||
:items="inputField.items"
|
||||
@blur="emitBlur"
|
||||
/>
|
||||
</div>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<BaseButton small @click="value[inputField.varName].push(getTemplate(inputField.items))"> New </BaseButton>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { validators } from "@/composables/use-validators";
|
||||
import { fieldTypes } from "@/composables/forms";
|
||||
import { ref } from "@vue/composition-api";
|
||||
|
||||
const BLUR_EVENT = "blur";
|
||||
|
||||
export default {
|
||||
name: "AutoForm",
|
||||
props: {
|
||||
value: {
|
||||
default: null,
|
||||
type: [Object, Array],
|
||||
},
|
||||
updateMode: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
items: {
|
||||
default: null,
|
||||
type: Array,
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: "max",
|
||||
},
|
||||
globalRules: {
|
||||
default: null,
|
||||
type: Array,
|
||||
},
|
||||
color: {
|
||||
default: null,
|
||||
type: String,
|
||||
},
|
||||
dark: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const menu = ref({});
|
||||
|
||||
return {
|
||||
menu,
|
||||
fieldTypes,
|
||||
validators,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
defaultRules() {
|
||||
return this.rulesByKey(this.globalRules);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
items: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
// Initialize Value Object to Obtain all keys
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < val.length; i++) {
|
||||
try {
|
||||
if (this.value[val[i].varName]) {
|
||||
continue;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (val[i].type === "text" || val[i].type === "textarea") {
|
||||
this.$set(this.value, val[i].varName, "");
|
||||
} else if (val[i].type === "select") {
|
||||
if (!val[i].options[0]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.$set(this.value, val[i].varName, val[i].options[0].value);
|
||||
} else if (val[i].type === "list") {
|
||||
this.$set(this.value, val[i].varName, []);
|
||||
} else if (val[i].type === "object") {
|
||||
this.$set(this.value, val[i].varName, {});
|
||||
} else if (val[i].type === "color") {
|
||||
this.$set(this.value, val[i].varName, "");
|
||||
this.$set(this.menu, val[i].varName, false);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
removeByIndex(list, index) {
|
||||
// Removes the item at the index
|
||||
list.splice(index, 1);
|
||||
},
|
||||
getTemplate(item) {
|
||||
const obj = {};
|
||||
|
||||
item.forEach((field) => {
|
||||
obj[field.varName] = "";
|
||||
});
|
||||
|
||||
return obj;
|
||||
},
|
||||
rulesByKey(keys) {
|
||||
const list = [];
|
||||
|
||||
if (keys === undefined) {
|
||||
return list;
|
||||
}
|
||||
if (keys === null) {
|
||||
return list;
|
||||
}
|
||||
if (keys === list) {
|
||||
return list;
|
||||
}
|
||||
|
||||
keys.forEach((key) => {
|
||||
if (key in this.validators) {
|
||||
list.push(this.validators[key]);
|
||||
}
|
||||
});
|
||||
return list;
|
||||
},
|
||||
emitBlur() {
|
||||
this.$emit(BLUR_EVENT, this.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
133
frontend/components/global/BaseDialog.vue
Normal file
133
frontend/components/global/BaseDialog.vue
Normal file
|
@ -0,0 +1,133 @@
|
|||
<template>
|
||||
<div>
|
||||
<slot name="activator" v-bind="{ open }" />
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
absolute
|
||||
:width="width"
|
||||
:content-class="top ? 'top-dialog' : undefined"
|
||||
:fullscreen="$vuetify.breakpoint.xsOnly"
|
||||
>
|
||||
<v-card height="100%">
|
||||
<v-app-bar dark :color="color" class="mt-n1">
|
||||
<v-icon large left>
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-progress-linear v-if="loading" class="mt-1" indeterminate color="primary"></v-progress-linear>
|
||||
|
||||
<div>
|
||||
<slot v-bind="{ submitEvent }" />
|
||||
</div>
|
||||
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
|
||||
<v-card-actions>
|
||||
<slot name="card-actions">
|
||||
<v-btn text color="grey" @click="dialog = false">
|
||||
{{ $t("general.cancel") }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<BaseButton v-if="$listeners.delete" delete secondary @click="deleteEvent" />
|
||||
<BaseButton v-if="$listeners.confirm" :color="color" type="submit" @click="submitEvent">
|
||||
{{ $t("general.confirm") }}
|
||||
</BaseButton>
|
||||
<BaseButton v-else-if="$listeners.submit" type="submit" @click="submitEvent">
|
||||
{{ submitText }}
|
||||
</BaseButton>
|
||||
</slot>
|
||||
</v-card-actions>
|
||||
|
||||
<div v-if="$slots['below-actions']" class="pb-4">
|
||||
<slot name="below-actions"> </slot>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
export default defineComponent({
|
||||
name: "BaseDialog",
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: "primary",
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "Modal Title",
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: "500",
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
top: {
|
||||
default: null,
|
||||
type: Boolean,
|
||||
},
|
||||
submitText: {
|
||||
type: String,
|
||||
default: () => "Create",
|
||||
},
|
||||
keepOpen: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
submitted: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
determineClose() {
|
||||
return this.submitted && !this.loading && !this.keepOpen;
|
||||
},
|
||||
displayicon() {
|
||||
return this.icon || this.$globals.icons.user;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
determineClose() {
|
||||
this.submitted = false;
|
||||
this.dialog = false;
|
||||
},
|
||||
dialog(val) {
|
||||
if (val) this.submitted = false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
submitEvent() {
|
||||
this.$emit("submit");
|
||||
this.submitted = true;
|
||||
},
|
||||
deleteEvent() {
|
||||
this.$emit("delete");
|
||||
this.submitted = true;
|
||||
},
|
||||
open() {
|
||||
console.log("Open Dialog");
|
||||
this.dialog = true;
|
||||
},
|
||||
close() {
|
||||
this.dialog = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style></style>
|
Loading…
Add table
Add a link
Reference in a new issue