mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-24 01:12:54 -04:00
reorganize all frontend items
This commit is contained in:
parent
d67240d449
commit
00a8fdda41
@ -1,137 +0,0 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
:max-width="width"
|
||||
:style="{ zIndex: zIndex }"
|
||||
@click:outside="cancel"
|
||||
@keydown.esc="cancel"
|
||||
@keydown.enter="confirm"
|
||||
>
|
||||
<template v-slot:activator="{}">
|
||||
<slot v-bind="{ open }"> </slot>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-app-bar v-if="Boolean(title)" :color="color" dense dark>
|
||||
<v-icon v-if="Boolean(icon)" left> {{ icon }}</v-icon>
|
||||
<v-toolbar-title v-text="title" />
|
||||
</v-app-bar>
|
||||
|
||||
<v-card-text v-show="!!message" class="pa-4 text--primary" v-html="message" />
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="cancel">
|
||||
{{ $t("general.cancel") }}
|
||||
</v-btn>
|
||||
<v-btn :color="color" text @click="confirm">
|
||||
{{ $t("general.confirm") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const CLOSE_EVENT = "close";
|
||||
const OPEN_EVENT = "open";
|
||||
const CONFIRM_EVENT = "confirm";
|
||||
/**
|
||||
* ConfirmationDialog Component used to add a second validaion step to an action.
|
||||
* @version 1.0.1
|
||||
* @author [zackbcom](https://github.com/zackbcom)
|
||||
* @since Version 1.0.0
|
||||
*/
|
||||
export default {
|
||||
name: "ConfirmationDialog",
|
||||
props: {
|
||||
/**
|
||||
* Message to be in body.
|
||||
*/
|
||||
message: String,
|
||||
/**
|
||||
* Optional Title message to be used in title.
|
||||
*/
|
||||
title: String,
|
||||
/**
|
||||
* Optional Icon to be used in title.
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: "mid-alert-circle",
|
||||
},
|
||||
/**
|
||||
* Color theme of the component. Chose one of the defined theme colors.
|
||||
* @values primary, secondary, accent, success, info, warning, error
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: "error",
|
||||
},
|
||||
/**
|
||||
* Define the max width of the component.
|
||||
*/
|
||||
width: {
|
||||
type: Number,
|
||||
default: 400,
|
||||
},
|
||||
/**
|
||||
* zIndex of the component.
|
||||
*/
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
dialog() {
|
||||
if (this.dialog === false) {
|
||||
this.$emit(CLOSE_EVENT);
|
||||
} else this.$emit(OPEN_EVENT);
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
/**
|
||||
* Keep state of open or closed
|
||||
*/
|
||||
dialog: false,
|
||||
}),
|
||||
methods: {
|
||||
open() {
|
||||
this.dialog = true;
|
||||
},
|
||||
/**
|
||||
* Cancel button handler.
|
||||
*/
|
||||
cancel() {
|
||||
/**
|
||||
* Cancel event.
|
||||
*
|
||||
* @event Cancel
|
||||
* @property {string} content content of the first prop passed to the event
|
||||
*/
|
||||
this.$emit("cancel");
|
||||
|
||||
//Hide Modal
|
||||
this.dialog = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* confirm button handler.
|
||||
*/
|
||||
confirm() {
|
||||
/**
|
||||
* confirm event.
|
||||
*
|
||||
* @event confirm
|
||||
* @property {string} content content of the first prop passed to the event
|
||||
*/
|
||||
this.$emit(CONFIRM_EVENT);
|
||||
|
||||
//Hide Modal
|
||||
this.dialog = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
10
frontend/api/class-interfaces/_base.ts
Normal file
10
frontend/api/class-interfaces/_base.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { ApiRequestInstance } from "~/types/api";
|
||||
|
||||
export class BaseAPIClass {
|
||||
requests: ApiRequestInstance
|
||||
|
||||
constructor(requests: ApiRequestInstance) {
|
||||
this.requests = requests;
|
||||
}
|
||||
}
|
||||
|
55
frontend/api/class-interfaces/recipes.ts
Normal file
55
frontend/api/class-interfaces/recipes.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { BaseAPIClass } from "./_base";
|
||||
import { Recipe } from "~/types/api-types/admin";
|
||||
|
||||
const prefix = "/api";
|
||||
|
||||
const routes = {
|
||||
recipesCreate: `${prefix}/recipes/create`,
|
||||
recipesBase: `${prefix}/recipes`,
|
||||
recipesSummary: `${prefix}/recipes/summary`,
|
||||
recipesTestScrapeUrl: `${prefix}/recipes/test-scrape-url`,
|
||||
recipesCreateUrl: `${prefix}/recipes/create-url`,
|
||||
recipesCreateFromZip: `${prefix}/recipes/create-from-zip`,
|
||||
|
||||
recipesRecipeSlug: (recipe_slug: string) => `${prefix}/recipes/${recipe_slug}`,
|
||||
recipesRecipeSlugZip: (recipe_slug: string) => `${prefix}/recipes/${recipe_slug}/zip`,
|
||||
recipesRecipeSlugImage: (recipe_slug: string) => `${prefix}/recipes/${recipe_slug}/image`,
|
||||
recipesRecipeSlugAssets: (recipe_slug: string) => `${prefix}/recipes/${recipe_slug}/assets`,
|
||||
};
|
||||
|
||||
class RecipeAPI extends BaseAPIClass {
|
||||
async getAll(start = 0, limit = 9999) {
|
||||
return await this.requests.get<Recipe[]>(routes.recipesSummary, {
|
||||
params: { start, limit },
|
||||
});
|
||||
}
|
||||
|
||||
async getOne(slug: string) {
|
||||
return await this.requests.get<Recipe>(routes.recipesRecipeSlug(slug));
|
||||
}
|
||||
|
||||
async createOne(name: string) {
|
||||
return await this.requests.post(routes.recipesBase, { name });
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
recipeImage(recipeSlug: string, version = null, key = null) {
|
||||
return `/api/media/recipes/${recipeSlug}/images/original.webp?&rnd=${key}&version=${version}`;
|
||||
}
|
||||
|
||||
recipeSmallImage(recipeSlug: string, version = null, key = null) {
|
||||
return `/api/media/recipes/${recipeSlug}/images/min-original.webp?&rnd=${key}&version=${version}`;
|
||||
}
|
||||
|
||||
recipeTinyImage(recipeSlug: string, version = null, key = null) {
|
||||
return `/api/media/recipes/${recipeSlug}/images/tiny-original.webp?&rnd=${key}&version=${version}`;
|
||||
}
|
||||
|
||||
recipeAssetPath(recipeSlug: string, assetName: string) {
|
||||
return `/api/media/recipes/${recipeSlug}/assets/${assetName}`;
|
||||
}
|
||||
}
|
||||
|
||||
export { RecipeAPI };
|
@ -1,5 +1,20 @@
|
||||
import { RecipeAPI } from "./class-interfaces/recipes";
|
||||
import { ApiRequestInstance } from "~/types/api";
|
||||
|
||||
class Api {
|
||||
private static instance: Api;
|
||||
public recipes: RecipeAPI;
|
||||
|
||||
constructor(requests: ApiRequestInstance) {
|
||||
if (Api.instance instanceof Api) {
|
||||
return Api.instance;
|
||||
}
|
||||
|
||||
export const api = {}
|
||||
this.recipes = new RecipeAPI(requests);
|
||||
|
||||
Object.freeze(this);
|
||||
Api.instance = this;
|
||||
}
|
||||
}
|
||||
|
||||
export { Api };
|
||||
|
@ -1,5 +1,6 @@
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
|
||||
|
||||
interface RequestResponse<T> {
|
||||
response: AxiosResponse<T> | null;
|
||||
data: T | null;
|
||||
|
8
frontend/assets/main.css
Normal file
8
frontend/assets/main.css
Normal file
@ -0,0 +1,8 @@
|
||||
.layout-enter-active,
|
||||
.layout-leave-active {
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.layout-enter,
|
||||
.layout-leave-active {
|
||||
opacity: 0;
|
||||
}
|
@ -2,19 +2,19 @@
|
||||
<v-row>
|
||||
<SearchDialog ref="mealselect" @selected="setSlug" />
|
||||
<BaseDialog
|
||||
ref="customMealDialog"
|
||||
title="Custom Meal"
|
||||
:title-icon="$globals.icons.primary"
|
||||
:submit-text="$t('general.save')"
|
||||
:top="true"
|
||||
ref="customMealDialog"
|
||||
@submit="pushCustomMeal"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-text-field autofocus v-model="customMeal.name" :label="$t('general.name')"> </v-text-field>
|
||||
<v-text-field v-model="customMeal.name" autofocus :label="$t('general.name')"> </v-text-field>
|
||||
<v-textarea v-model="customMeal.description" :label="$t('recipe.description')"> </v-textarea>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
<v-col cols="12" sm="12" md="6" lg="4" xl="3" v-for="(planDay, index) in value" :key="index">
|
||||
<v-col v-for="(planDay, index) in value" :key="index" cols="12" sm="12" md="6" lg="4" xl="3">
|
||||
<v-hover v-slot="{ hover }" :open-delay="50">
|
||||
<v-card :class="{ 'on-hover': hover }" :elevation="hover ? 12 : 2">
|
||||
<CardImage large :slug="planDay.meals[0].slug" icon-size="200" @click="openSearch(index, modes.primary)">
|
||||
@ -80,9 +80,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SearchDialog from "../UI/Dialogs/SearchDialog";
|
||||
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
|
||||
import { api } from "@/api";
|
||||
import SearchDialog from "../UI/Dialogs/SearchDialog";
|
||||
import CardImage from "../Recipe/CardImage.vue";
|
||||
export default {
|
||||
components: {
|
||||
@ -116,13 +116,13 @@ export default {
|
||||
}
|
||||
},
|
||||
setSide(name, slug = null, description = "") {
|
||||
const meal = { name: name, slug: slug, description: description };
|
||||
this.value[this.activeIndex]["meals"].push(meal);
|
||||
const meal = { name, slug, description };
|
||||
this.value[this.activeIndex].meals.push(meal);
|
||||
},
|
||||
setPrimary(name, slug, description = "") {
|
||||
this.value[this.activeIndex]["meals"][0]["slug"] = slug;
|
||||
this.value[this.activeIndex]["meals"][0]["name"] = name;
|
||||
this.value[this.activeIndex]["meals"][0]["description"] = description;
|
||||
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) {
|
||||
@ -140,7 +140,7 @@ export default {
|
||||
this.$refs.mealselect.open();
|
||||
},
|
||||
removeSide(dayIndex, sideIndex) {
|
||||
this.value[dayIndex]["meals"].splice(sideIndex, 1);
|
||||
this.value[dayIndex].meals.splice(sideIndex, 1);
|
||||
},
|
||||
addCustomItem(index, mode) {
|
||||
this.mode = mode;
|
@ -31,7 +31,7 @@ export default {
|
||||
|
||||
methods: {
|
||||
formatDate(timestamp) {
|
||||
let dateObject = new Date(timestamp);
|
||||
const dateObject = new Date(timestamp);
|
||||
return utils.getDateAsPythonDate(dateObject);
|
||||
},
|
||||
async update() {
|
@ -21,7 +21,7 @@
|
||||
max-width="290px"
|
||||
min-width="290px"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-text-field
|
||||
v-model="startComputedDateFormatted"
|
||||
:label="$t('meal-plan.start-date')"
|
||||
@ -45,7 +45,7 @@
|
||||
max-width="290px"
|
||||
min-width="290px"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-text-field
|
||||
v-model="endComputedDateFormatted"
|
||||
:label="$t('meal-plan.end-date')"
|
||||
@ -67,24 +67,24 @@
|
||||
</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>
|
||||
<TheButton v-if="planDays.length > 0" edit text @click="random">
|
||||
<template #icon>
|
||||
{{ $globals.icons.diceMultiple }}
|
||||
</template>
|
||||
{{ $t("general.random") }}
|
||||
</TheButton>
|
||||
<TheButton create @click="save" :disabled="planDays.length == 0" />
|
||||
<TheButton create :disabled="planDays.length == 0" @click="save" />
|
||||
</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";
|
||||
const CREATE_EVENT = "created";
|
||||
export default {
|
||||
components: {
|
||||
MealPlanCard,
|
||||
@ -105,6 +105,41 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
groupSettings() {
|
||||
return this.$store.getters.getCurrentGroup;
|
||||
},
|
||||
actualStartDate() {
|
||||
if (!this.startDate) return null;
|
||||
return Date.parse(this.startDate.replaceAll("-", "/"));
|
||||
},
|
||||
actualEndDate() {
|
||||
if (!this.endDate) return null;
|
||||
return Date.parse(this.endDate.replaceAll("-", "/"));
|
||||
},
|
||||
dateDif() {
|
||||
if (!this.actualEndDate || !this.actualStartDate) return null;
|
||||
const dateDif = (this.actualEndDate - this.actualStartDate) / (1000 * 3600 * 24) + 1;
|
||||
if (dateDif < 1) {
|
||||
return null;
|
||||
}
|
||||
return dateDif;
|
||||
},
|
||||
startComputedDateFormatted() {
|
||||
return this.formatDate(this.actualStartDate);
|
||||
},
|
||||
endComputedDateFormatted() {
|
||||
return this.formatDate(this.actualEndDate);
|
||||
},
|
||||
filteredRecipes() {
|
||||
const recipes = this.items.filter(x => !this.usedRecipes.includes(x));
|
||||
return recipes.length > 0 ? recipes : this.items;
|
||||
},
|
||||
allRecipes() {
|
||||
return this.$store.getters.getAllRecipes;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
dateDif() {
|
||||
this.planDays = [];
|
||||
@ -128,41 +163,6 @@ export default {
|
||||
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);
|
||||
@ -178,9 +178,9 @@ export default {
|
||||
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;
|
||||
const recipe = this.getRandom(this.filteredRecipes);
|
||||
this.planDays[index].meals[0].slug = recipe.slug;
|
||||
this.planDays[index].meals[0].name = recipe.name;
|
||||
this.usedRecipes.push(recipe);
|
||||
});
|
||||
},
|
@ -8,19 +8,21 @@
|
||||
style="z-index: 2; position: sticky"
|
||||
:class="{ 'fixed-bar-mobile': $vuetify.breakpoint.xs }"
|
||||
>
|
||||
<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="emitDelete()"
|
||||
/>
|
||||
@confirm="emitDelete()"
|
||||
>
|
||||
{{ $t("recipe.delete-confirmation") }}
|
||||
</BaseDialog>
|
||||
|
||||
<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 />
|
||||
<RecipeFavoriteBadge v-if="loggedIn" class="mx-1" color="info" button-style :slug="slug" show-always />
|
||||
<v-tooltip bottom color="info">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn
|
||||
v-if="loggedIn"
|
||||
fab
|
||||
@ -36,7 +38,7 @@
|
||||
</template>
|
||||
<span>{{ $t("general.edit") }}</span>
|
||||
</v-tooltip>
|
||||
<ContextMenu
|
||||
<RecipeContextMenu
|
||||
show-print
|
||||
:menu-top="false"
|
||||
:slug="slug"
|
||||
@ -65,9 +67,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog.vue";
|
||||
import ContextMenu from "@/components/Recipe/ContextMenu.vue";
|
||||
import FavoriteBadge from "@/components/Recipe/FavoriteBadge.vue";
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||
|
||||
const SAVE_EVENT = "save";
|
||||
const DELETE_EVENT = "delete";
|
||||
@ -75,12 +76,14 @@ const CLOSE_EVENT = "close";
|
||||
const JSON_EVENT = "json";
|
||||
|
||||
export default {
|
||||
components: { ConfirmationDialog, ContextMenu, FavoriteBadge },
|
||||
components: { RecipeContextMenu, RecipeFavoriteBadge },
|
||||
props: {
|
||||
slug: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
name: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
value: {
|
||||
@ -182,4 +185,4 @@ export default {
|
||||
.fixed-bar-mobile {
|
||||
top: 1.5em !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
@ -5,12 +5,12 @@
|
||||
{{ $t("asset.assets") }}
|
||||
</v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-list :flat="!edit" v-if="value.length > 0">
|
||||
<v-list v-if="value.length > 0" :flat="!edit">
|
||||
<v-list-item v-for="(item, i) in value" :key="i">
|
||||
<v-list-item-icon class="ma-auto">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-icon v-text="getIconDefinition(item.icon).icon" v-bind="attrs" v-on="on"></v-icon>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-icon v-bind="attrs" v-on="on" v-text="getIconDefinition(item.icon).icon"></v-icon>
|
||||
</template>
|
||||
<span>{{ getIconDefinition(item.icon).title }}</span>
|
||||
</v-tooltip>
|
||||
@ -23,7 +23,7 @@
|
||||
<v-icon> {{ $globals.icons.download }} </v-icon>
|
||||
</v-btn>
|
||||
<div v-else>
|
||||
<v-btn color="error" icon @click="deleteAsset(i)" top>
|
||||
<v-btn color="error" icon top @click="deleteAsset(i)">
|
||||
<v-icon>{{ $globals.icons.delete }}</v-icon>
|
||||
</v-btn>
|
||||
<TheCopyButton :copy-text="copyLink(item.fileName)" />
|
||||
@ -34,25 +34,25 @@
|
||||
</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">
|
||||
<BaseDialog :title="$t('asset.new-asset')" :title-icon="getIconDefinition(newAsset.icon).icon" @submit="addAsset">
|
||||
<template #open="{ open }">
|
||||
<v-btn v-if="edit" color="secondary" dark @click="open">
|
||||
<v-icon>{{ $globals.icons.create }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card-text class="pt-2">
|
||||
<v-text-field dense v-model="newAsset.name" :label="$t('general.name')"></v-text-field>
|
||||
<v-text-field v-model="newAsset.name" dense :label="$t('general.name')"></v-text-field>
|
||||
<div class="d-flex justify-space-between">
|
||||
<v-select
|
||||
v-model="newAsset.icon"
|
||||
dense
|
||||
:prepend-icon="getIconDefinition(newAsset.icon).icon"
|
||||
v-model="newAsset.icon"
|
||||
:items="iconOptions"
|
||||
item-text="title"
|
||||
item-value="name"
|
||||
class="mr-2"
|
||||
>
|
||||
<template v-slot:item="{ item }">
|
||||
<template #item="{ item }">
|
||||
<v-list-item-avatar>
|
||||
<v-icon class="mr-auto">
|
||||
{{ item.icon }}
|
||||
@ -61,11 +61,11 @@
|
||||
{{ item.title }}
|
||||
</template>
|
||||
</v-select>
|
||||
<TheUploadBtn @uploaded="setFileObject" :post="false" file-name="file" :text-btn="false" />
|
||||
<TheUploadBtn :post="false" file-name="file" :text-btn="false" @uploaded="setFileObject" />
|
||||
</div>
|
||||
{{ fileObject.name }}
|
||||
</v-card-text>
|
||||
</base-dialog>
|
||||
</BaseDialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -108,27 +108,27 @@ export default {
|
||||
return [
|
||||
{
|
||||
name: "mdi-file",
|
||||
title: this.$i18n.t('asset.file'),
|
||||
title: this.$i18n.t("asset.file"),
|
||||
icon: this.$globals.icons.file
|
||||
},
|
||||
{
|
||||
name: "mdi-file-pdf-box",
|
||||
title: this.$i18n.t('asset.pdf'),
|
||||
title: this.$i18n.t("asset.pdf"),
|
||||
icon: this.$globals.icons.filePDF
|
||||
},
|
||||
{
|
||||
name: "mdi-file-image",
|
||||
title: this.$i18n.t('asset.image'),
|
||||
title: this.$i18n.t("asset.image"),
|
||||
icon: this.$globals.icons.fileImage
|
||||
},
|
||||
{
|
||||
name: "mdi-code-json",
|
||||
title: this.$i18n.t('asset.code'),
|
||||
title: this.$i18n.t("asset.code"),
|
||||
icon: this.$globals.icons.codeJson
|
||||
},
|
||||
{
|
||||
name: "mdi-silverware-fork-knife",
|
||||
title: this.$i18n.t('asset.recipe'),
|
||||
title: this.$i18n.t("asset.recipe"),
|
||||
icon: this.$globals.icons.primary
|
||||
},
|
||||
];
|
@ -7,7 +7,7 @@
|
||||
{{ $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-card v-for="(comment, index) in comments" :key="comment.id" class="ma-2">
|
||||
<v-list-item two-line>
|
||||
<v-list-item-avatar color="accent" class="white--text">
|
||||
<img :src="getProfileImage(comment.user.id)" />
|
||||
@ -18,19 +18,19 @@
|
||||
</v-list-item-content>
|
||||
<v-card-actions v-if="loggedIn">
|
||||
<TheButton
|
||||
v-if="!editKeys[comment.id] && (user.admin || comment.user.id === user.id)"
|
||||
small
|
||||
minor
|
||||
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"
|
||||
small
|
||||
edit
|
||||
@click="editComment(comment.id)"
|
||||
/>
|
||||
<TheButton small v-else-if="editKeys[comment.id]" update @click="updateComment(comment.id, index)" />
|
||||
<TheButton v-else-if="editKeys[comment.id]" small update @click="updateComment(comment.id, index)" />
|
||||
</v-card-actions>
|
||||
</v-list-item>
|
||||
<div>
|
||||
@ -41,7 +41,7 @@
|
||||
</div>
|
||||
</v-card>
|
||||
<v-card-text v-if="loggedIn">
|
||||
<v-textarea auto-grow row-height="1" outlined v-model="newComment"> </v-textarea>
|
||||
<v-textarea v-model="newComment" auto-grow row-height="1" outlined> </v-textarea>
|
||||
<div class="d-flex">
|
||||
<TheButton class="ml-auto" create @click="createNewComment"> {{ $t("recipe.comment-action") }} </TheButton>
|
||||
</div>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="text-center">
|
||||
<v-dialog v-model="dialog" width="600">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn color="secondary lighten-2" dark v-bind="attrs" v-on="on" @click="inputText = ''">
|
||||
{{ $t("new-recipe.bulk-add") }}
|
||||
</v-btn>
|
||||
@ -38,7 +38,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
splitText() {
|
||||
let split = this.inputText.split("\n");
|
||||
const split = this.inputText.split("\n");
|
||||
|
||||
split.forEach((element, index) => {
|
||||
if ((element === "\n") | (element == false)) {
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="text-center">
|
||||
<v-dialog v-model="dialog" width="700">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn color="accent" dark v-bind="attrs" v-on="on"> {{ $t("recipe.api-extras") }} </v-btn>
|
||||
</template>
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
<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-row v-for="(value, key, index) in extras" :key="index" align="center">
|
||||
<v-col cols="12" sm="1">
|
||||
<v-btn fab text x-small color="white" elevation="0" @click="removeExtra(key)">
|
||||
<v-icon color="error">{{ $globals.icons.delete }}</v-icon>
|
||||
@ -19,7 +19,7 @@
|
||||
<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-text-field v-model="extras[key]" :label="$t('recipe.object-value')"> </v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
@ -29,8 +29,8 @@
|
||||
<v-card-actions>
|
||||
<v-form ref="addKey">
|
||||
<v-text-field
|
||||
:label="$t('recipe.new-key-name')"
|
||||
v-model="newKeyName"
|
||||
:label="$t('recipe.new-key-name')"
|
||||
class="pr-4"
|
||||
:rules="[rules.required, rules.whiteSpace]"
|
||||
></v-text-field>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="text-center">
|
||||
<v-menu offset-y top nudge-top="6" :close-on-content-click="false">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn color="accent" dark v-bind="attrs" v-on="on">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.fileImage }}
|
||||
@ -19,15 +19,15 @@
|
||||
url="none"
|
||||
file-name="image"
|
||||
:text-btn="false"
|
||||
@uploaded="uploadImage"
|
||||
:post="false"
|
||||
@uploaded="uploadImage"
|
||||
/>
|
||||
</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">
|
||||
<v-text-field v-model="url" :label="$t('general.url')" class="pt-5" clearable :messages="getMessages()">
|
||||
<template #append-outer>
|
||||
<v-btn class="ml-2" color="primary" :loading="loading" :disabled="!slug" @click="getImageFromURL">
|
||||
{{ $t("general.get") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
@ -40,10 +40,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const REFRESH_EVENT = "refresh";
|
||||
const UPLOAD_EVENT = "upload";
|
||||
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
|
||||
import { api } from "@/api";
|
||||
const REFRESH_EVENT = "refresh";
|
||||
const UPLOAD_EVENT = "upload";
|
||||
export default {
|
||||
components: {
|
||||
TheUploadBtn,
|
@ -2,23 +2,23 @@
|
||||
<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">
|
||||
<draggable :value="value" handle=".handle" @input="updateIndex" @start="drag = true" @end="drag = false">
|
||||
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
||||
<div v-for="(ingredient, index) in value" :key="generateKey('ingredient', index)">
|
||||
<v-row align="center">
|
||||
<v-text-field
|
||||
v-if="edit && showTitleEditor[index]"
|
||||
class="mx-3 mt-3"
|
||||
v-model="value[index].title"
|
||||
class="mx-3 mt-3"
|
||||
dense
|
||||
:label="$t('recipe.section-title')"
|
||||
>
|
||||
</v-text-field>
|
||||
|
||||
<v-textarea
|
||||
v-model="value[index].note"
|
||||
class="mr-2"
|
||||
:label="$t('recipe.ingredient')"
|
||||
v-model="value[index].note"
|
||||
auto-grow
|
||||
solo
|
||||
dense
|
||||
@ -26,7 +26,7 @@
|
||||
>
|
||||
<template slot="append">
|
||||
<v-tooltip right nudge-right="10">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn icon small class="mt-n1" v-bind="attrs" v-on="on" @click="toggleShowTitle(index)">
|
||||
<v-icon>{{ showTitleEditor[index] ? $globals.icons.minus : $globals.icons.createAlt }}</v-icon>
|
||||
</v-btn>
|
||||
@ -39,7 +39,7 @@
|
||||
<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)">
|
||||
<v-icon slot="prepend" class="mr-n1" color="error" @click="removeByIndex(value, index)">
|
||||
{{ $globals.icons.delete }}
|
||||
</v-icon>
|
||||
</v-textarea>
|
||||
@ -49,20 +49,20 @@
|
||||
</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">
|
||||
<BulkAdd class="mr-2" @bulk-data="addIngredient" />
|
||||
<v-btn color="secondary" dark class="mr-4" @click="addIngredient">
|
||||
<v-icon>{{ $globals.icons.create }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-for="(ingredient, index) in value" :key="generateKey('ingredient', index)">
|
||||
<h3 class="mt-2" v-if="showTitleEditor[index]">{{ ingredient.title }}</h3>
|
||||
<h3 v-if="showTitleEditor[index]" class="mt-2">{{ ingredient.title }}</h3>
|
||||
<v-divider v-if="showTitleEditor[index]"></v-divider>
|
||||
<v-list-item dense @click="toggleChecked(index)">
|
||||
<v-checkbox hide-details :value="checked[index]" class="pt-0 my-auto py-auto" color="secondary"> </v-checkbox>
|
||||
<v-list-item-content>
|
||||
<vue-markdown class="ma-0 pa-0 text-subtitle-1 dense-markdown" :source="ingredient.note"> </vue-markdown>
|
||||
<VueMarkdown class="ma-0 pa-0 text-subtitle-1 dense-markdown" :source="ingredient.note"> </VueMarkdown>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</div>
|
||||
@ -98,10 +98,6 @@ export default {
|
||||
showTitleEditor: [],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.checked = this.value.map(() => false);
|
||||
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler() {
|
||||
@ -109,6 +105,10 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.checked = this.value.map(() => false);
|
||||
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
|
||||
},
|
||||
methods: {
|
||||
addIngredient(ingredients = null) {
|
||||
if (ingredients.length) {
|
@ -5,20 +5,20 @@
|
||||
<draggable
|
||||
:disabled="!edit"
|
||||
:value="value"
|
||||
handle=".handle"
|
||||
@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-toolbar-title v-if="!edit" class="headline">
|
||||
<v-app-bar-title v-text="step.title"> </v-app-bar-title>
|
||||
</v-toolbar-title>
|
||||
<v-text-field
|
||||
v-if="edit"
|
||||
class="headline pa-0 mt-5"
|
||||
v-model="step.title"
|
||||
class="headline pa-0 mt-5"
|
||||
dense
|
||||
solo
|
||||
flat
|
||||
@ -62,18 +62,18 @@
|
||||
</v-card-title>
|
||||
<v-card-text v-if="edit">
|
||||
<v-textarea
|
||||
:key="generateKey('instructions', index)"
|
||||
v-model="value[index]['text']"
|
||||
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">
|
||||
<div v-show="!isChecked(index) && !edit" class="m-0 p-0">
|
||||
<v-card-text>
|
||||
<vue-markdown :source="step.text"> </vue-markdown>
|
||||
<VueMarkdown :source="step.text"> </VueMarkdown>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
@ -111,10 +111,6 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
|
||||
},
|
||||
|
||||
watch: {
|
||||
value: {
|
||||
handler() {
|
||||
@ -124,6 +120,10 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
|
||||
},
|
||||
|
||||
methods: {
|
||||
generateKey(item, index) {
|
||||
return utils.generateUniqueKey(item, index);
|
||||
@ -137,7 +137,7 @@ export default {
|
||||
toggleDisabled(stepIndex) {
|
||||
if (this.edit) return;
|
||||
if (this.disabledSteps.includes(stepIndex)) {
|
||||
let index = this.disabledSteps.indexOf(stepIndex);
|
||||
const index = this.disabledSteps.indexOf(stepIndex);
|
||||
if (index !== -1) {
|
||||
this.disabledSteps.splice(index, 1);
|
||||
}
|
||||
@ -149,7 +149,7 @@ export default {
|
||||
if (this.disabledSteps.includes(stepIndex) && !this.edit) {
|
||||
return "disabled-card";
|
||||
} else {
|
||||
return;
|
||||
|
||||
}
|
||||
},
|
||||
toggleShowTitle(index) {
|
@ -1,17 +1,17 @@
|
||||
<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)">
|
||||
<v-card v-for="(note, index) in value" :key="generateKey('note', index)" class="mt-1">
|
||||
<div v-if="edit">
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-btn fab x-small color="white" class="mr-2" elevation="0" @click="removeByIndex(value, index)">
|
||||
<v-icon color="error">{{ $globals.icons.delete }}</v-icon>
|
||||
</v-btn>
|
||||
<v-text-field :label="$t('recipe.title')" v-model="value[index]['title']"></v-text-field>
|
||||
<v-text-field v-model="value[index]['title']" :label="$t('recipe.title')"></v-text-field>
|
||||
</v-row>
|
||||
|
||||
<v-textarea auto-grow :placeholder="$t('recipe.note')" v-model="value[index]['text']"> </v-textarea>
|
||||
<v-textarea v-model="value[index]['text']" auto-grow :placeholder="$t('recipe.note')"> </v-textarea>
|
||||
</v-card-text>
|
||||
</div>
|
||||
<div v-else>
|
||||
@ -20,12 +20,12 @@
|
||||
</v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text>
|
||||
<vue-markdown :source="note.text"> </vue-markdown>
|
||||
<VueMarkdown :source="note.text"> </VueMarkdown>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<div class="d-flex justify-end" v-if="edit">
|
||||
<div v-if="edit" class="d-flex justify-end">
|
||||
<v-btn class="mt-1" color="secondary" dark @click="addNote">
|
||||
<v-icon>{{ $globals.icons.create }}</v-icon>
|
||||
</v-btn>
|
@ -18,7 +18,7 @@
|
||||
></v-text-field>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-list dense v-if="showViewer" class="mt-0 pt-0">
|
||||
<v-list v-if="showViewer" dense class="mt-0 pt-0">
|
||||
<v-list-item v-for="(item, key, index) in labels" :key="index">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="pl-4 text-subtitle-1 flex row ">
|
@ -12,7 +12,7 @@
|
||||
</h1>
|
||||
</div>
|
||||
<div class="time-container">
|
||||
<RecipeTimeCard :prepTime="recipe.prepTime" :totalTime="recipe.totalTime" :performTime="recipe.performTime" />
|
||||
<RecipeTimeCard :prep-time="recipe.prepTime" :total-time="recipe.totalTime" :perform-time="recipe.performTime" />
|
||||
</div>
|
||||
<v-btn
|
||||
v-if="recipe.recipeYield"
|
||||
@ -28,7 +28,7 @@
|
||||
{{ recipe.recipeYield }}
|
||||
</v-btn>
|
||||
<div>
|
||||
<vue-markdown :source="recipe.description"> </vue-markdown>
|
||||
<VueMarkdown :source="recipe.description"> </VueMarkdown>
|
||||
<h2>{{ $t("recipe.ingredients") }}</h2>
|
||||
<ul>
|
||||
<li v-for="(ingredient, index) in recipe.recipeIngredient" :key="index">
|
||||
@ -45,7 +45,7 @@
|
||||
<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>
|
||||
<VueMarkdown :source="step.text"> </VueMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -54,7 +54,7 @@
|
||||
|
||||
<div v-for="(note, index) in recipe.notes" :key="index + 'note'">
|
||||
<h3>{{ note.title }}</h3>
|
||||
<vue-markdown :source="note.text"> </vue-markdown>
|
||||
<VueMarkdown :source="note.text"> </VueMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="text-center">
|
||||
<v-menu offset-y top nudge-top="6" :close-on-content-click="false">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn color="accent" dark v-bind="attrs" v-on="on">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.cog }}
|
||||
@ -18,10 +18,10 @@
|
||||
<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]"
|
||||
dense
|
||||
flat
|
||||
inset
|
||||
:label="labels[key]"
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-chip label color="accent custom-transparent" class="ma-1" v-for="(time, index) in allTimes" :key="index">
|
||||
<v-chip v-for="(time, index) in allTimes" :key="index" label color="accent custom-transparent" class="ma-1">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.clockOutline }}
|
||||
</v-icon>
|
@ -1,7 +1,7 @@
|
||||
<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" />
|
||||
<v-img v-if="!noImage" :src="profileImage" />
|
||||
<div v-else>
|
||||
{{ initials }}
|
||||
</div>
|
@ -2,31 +2,31 @@
|
||||
<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" />
|
||||
<ImageUploadBtn class="my-1" :slug="value.slug" @upload="uploadImage" @refresh="$emit('upload')" />
|
||||
<SettingsMenu class="my-1 mx-1" :value="value.settings" @upload="uploadImage" />
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<v-col>
|
||||
<v-text-field :label="$t('recipe.total-time')" v-model="value.totalTime"></v-text-field>
|
||||
<v-text-field v-model="value.totalTime" :label="$t('recipe.total-time')"></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-col><v-text-field v-model="value.prepTime" :label="$t('recipe.prep-time')"></v-text-field></v-col>
|
||||
<v-col><v-text-field v-model="value.performTime" :label="$t('recipe.perform-time')"></v-text-field></v-col>
|
||||
</v-row>
|
||||
<v-text-field class="my-3" :label="$t('recipe.recipe-name')" v-model="value.name" :rules="[existsRule]">
|
||||
<v-text-field v-model="value.name" class="my-3" :label="$t('recipe.recipe-name')" :rules="[existsRule]">
|
||||
</v-text-field>
|
||||
<v-textarea auto-grow min-height="100" :label="$t('recipe.description')" v-model="value.description">
|
||||
<v-textarea v-model="value.description" auto-grow min-height="100" :label="$t('recipe.description')">
|
||||
</v-textarea>
|
||||
<div class="my-2"></div>
|
||||
<v-row dense disabled>
|
||||
<v-col sm="4">
|
||||
<v-text-field :label="$t('recipe.servings')" v-model="value.recipeYield" class="rounded-sm"> </v-text-field>
|
||||
<v-text-field v-model="value.recipeYield" :label="$t('recipe.servings')" class="rounded-sm"> </v-text-field>
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<Rating v-model="value.rating" :emit-only="true" />
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="4" lg="4">
|
||||
<Ingredients :edit="true" v-model="value.recipeIngredient" />
|
||||
<Ingredients v-model="value.recipeIngredient" :edit="true" />
|
||||
<v-card class="mt-6">
|
||||
<v-card-title class="py-2">
|
||||
{{ $t("recipe.categories") }}
|
||||
@ -34,8 +34,8 @@
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text>
|
||||
<CategoryTagSelector
|
||||
:return-object="false"
|
||||
v-model="value.recipeCategory"
|
||||
:return-object="false"
|
||||
:show-add="true"
|
||||
:show-label="false"
|
||||
/>
|
||||
@ -49,8 +49,8 @@
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text>
|
||||
<CategoryTagSelector
|
||||
:return-object="false"
|
||||
v-model="value.tags"
|
||||
:return-object="false"
|
||||
:show-add="true"
|
||||
:tag-selector="true"
|
||||
:show-label="false"
|
||||
@ -67,12 +67,12 @@
|
||||
<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">
|
||||
<BulkAdd class="mr-2" @bulk-data="appendSteps" />
|
||||
<v-btn color="secondary" dark class="mr-4" @click="addStep">
|
||||
<v-icon>{{ $globals.icons.create }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<Notes :edit="true" v-model="value.notes" />
|
||||
<Notes v-model="value.notes" :edit="true" />
|
||||
|
||||
<v-text-field v-model="value.orgURL" class="mt-10" :label="$t('recipe.original-url')"></v-text-field>
|
||||
</v-col>
|
||||
@ -82,7 +82,6 @@
|
||||
</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";
|
||||
@ -95,6 +94,7 @@ import Assets from "@/components/Recipe/Parts/Assets.vue";
|
||||
import Notes from "@/components/Recipe/Parts/Notes.vue";
|
||||
import SettingsMenu from "@/components/Recipe/Parts/Helpers/SettingsMenu.vue";
|
||||
import Rating from "@/components/Recipe/Parts/Rating";
|
||||
const UPLOAD_EVENT = "upload";
|
||||
export default {
|
||||
components: {
|
||||
BulkAdd,
|
||||
@ -109,10 +109,10 @@ export default {
|
||||
SettingsMenu,
|
||||
Rating,
|
||||
},
|
||||
mixins: [validators],
|
||||
props: {
|
||||
value: Object,
|
||||
},
|
||||
mixins: [validators],
|
||||
data() {
|
||||
return {
|
||||
fileObject: null,
|
@ -4,7 +4,7 @@
|
||||
{{ recipe.name }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<vue-markdown :source="recipe.description"> </vue-markdown>
|
||||
<VueMarkdown :source="recipe.description"> </VueMarkdown>
|
||||
<v-row dense disabled>
|
||||
<v-col>
|
||||
<v-btn
|
||||
@ -21,13 +21,13 @@
|
||||
{{ recipe.recipeYield }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<Rating :value="recipe.rating" :name="recipe.name" :slug="recipe.slug" :key="recipe.slug" />
|
||||
<Rating :key="recipe.slug" :value="recipe.rating" :name="recipe.name" :slug="recipe.slug" />
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="4" lg="4">
|
||||
<Ingredients :value="recipe.recipeIngredient" :edit="false" />
|
||||
<div v-if="medium">
|
||||
<v-card class="mt-2" v-if="recipe.recipeCategory.length > 0">
|
||||
<v-card v-if="recipe.recipeCategory.length > 0" class="mt-2">
|
||||
<v-card-title class="py-2">
|
||||
{{ $t("recipe.categories") }}
|
||||
</v-card-title>
|
||||
@ -36,13 +36,13 @@
|
||||
<RecipeChips :items="recipe.recipeCategory" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card class="mt-2" v-if="recipe.tags.length > 0">
|
||||
<v-card v-if="recipe.tags.length > 0" class="mt-2">
|
||||
<v-card-title class="py-2">
|
||||
{{ $t("tag.tags") }}
|
||||
</v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text>
|
||||
<RecipeChips :items="recipe.tags" :isCategory="false" />
|
||||
<RecipeChips :items="recipe.tags" :is-category="false" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
@ -89,12 +89,14 @@
|
||||
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";
|
||||
import RecipeChips from "./RecipeChips";
|
||||
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VueMarkdown,
|
||||
@ -107,7 +109,10 @@ export default {
|
||||
Rating,
|
||||
},
|
||||
props: {
|
||||
recipe: Object,
|
||||
recipe: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
@ -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>
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="recipes">
|
||||
<v-app-bar color="transparent" flat class="mt-n1 flex-sm-wrap rounded " v-if="!disableToolbar">
|
||||
<v-icon large left v-if="title">
|
||||
<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>
|
||||
@ -12,9 +12,9 @@
|
||||
</v-icon>
|
||||
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.random") }}
|
||||
</v-btn>
|
||||
<v-menu offset-y left v-if="$listeners.sort">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" v-on="on" :loading="sortLoading">
|
||||
<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>
|
||||
@ -57,7 +57,7 @@
|
||||
</v-app-bar>
|
||||
<div v-if="recipes" class="mt-2">
|
||||
<v-row v-if="!viewScale">
|
||||
<v-col :sm="6" :md="6" :lg="4" :xl="3" v-for="recipe in recipes.slice(0, cardLimit)" :key="recipe.name">
|
||||
<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"
|
||||
@ -72,16 +72,16 @@
|
||||
</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-for="recipe in recipes.slice(0, cardLimit)"
|
||||
:key="recipe.name"
|
||||
>
|
||||
<v-lazy>
|
||||
<MobileRecipeCard
|
||||
<RecipeCardMobile
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:slug="recipe.slug"
|
||||
@ -93,47 +93,54 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
<div v-intersect="bumpList" class="d-flex">
|
||||
<v-expand-x-transition>
|
||||
<SiteLoader v-if="loading" :loading="loading" />
|
||||
</v-expand-x-transition>
|
||||
<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 SiteLoader from "@/components/UI/SiteLoader";
|
||||
import RecipeCard from "../Recipe/RecipeCard";
|
||||
import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard";
|
||||
import { utils } from "@/utils";
|
||||
import RecipeCard from "./RecipeCard";
|
||||
import RecipeCardMobile from "./RecipeCardMobile";
|
||||
const SORT_EVENT = "sort";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RecipeCard,
|
||||
MobileRecipeCard,
|
||||
SiteLoader,
|
||||
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: Array,
|
||||
recipes: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -149,11 +156,6 @@ export default {
|
||||
},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
recipes() {
|
||||
this.bumpList();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
viewScale() {
|
||||
if (this.mobileCards) return true;
|
||||
@ -173,6 +175,11 @@ export default {
|
||||
return this.titleIcon || this.$globals.icons.tags;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
recipes() {
|
||||
this.bumpList();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
bumpList() {
|
||||
const newCardLimit = Math.min(this.cardLimit + 20, this.effectiveHardLimit);
|
||||
@ -185,7 +192,8 @@ export default {
|
||||
},
|
||||
async setLoader() {
|
||||
this.loading = true;
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
// eslint-disable-next-line promise/param-names
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
this.loading = false;
|
||||
},
|
||||
navigateRandom() {
|
||||
@ -194,7 +202,7 @@ export default {
|
||||
},
|
||||
sortRecipes(sortType) {
|
||||
this.sortLoading = true;
|
||||
let sortTarget = [...this.recipes];
|
||||
const sortTarget = [...this.recipes];
|
||||
switch (sortType) {
|
||||
case this.EVENTS.az:
|
||||
utils.recipe.sortAToZ(sortTarget);
|
@ -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,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,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,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,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>
|
||||
|
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>
|
@ -1,23 +1,28 @@
|
||||
<template>
|
||||
<div>
|
||||
<slot name="open" v-bind="{ open }"> </slot>
|
||||
<slot name="activator" v-bind="{ open }" />
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
:width="modalWidth + 'px'"
|
||||
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 mb-0">
|
||||
<v-app-bar dark :color="color" class="mt-n1">
|
||||
<v-icon large left>
|
||||
{{ displayTitleIcon }}
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-progress-linear class="mt-1" v-if="loading" indeterminate color="primary"></v-progress-linear>
|
||||
<v-progress-linear v-if="loading" class="mt-1" indeterminate color="primary"></v-progress-linear>
|
||||
|
||||
<slot v-bind="{ submitEvent }"> </slot>
|
||||
<div>
|
||||
<slot v-bind="{ submitEvent }" />
|
||||
</div>
|
||||
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
|
||||
<v-card-actions>
|
||||
<slot name="card-actions">
|
||||
@ -26,17 +31,17 @@
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn color="error" text @click="deleteEvent" v-if="$listeners.delete">
|
||||
{{ $t("general.delete") }}
|
||||
</v-btn>
|
||||
<slot name="extra-buttons"> </slot>
|
||||
<v-btn color="success" type="submit" @click="submitEvent">
|
||||
<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 }}
|
||||
</v-btn>
|
||||
</BaseButton>
|
||||
</slot>
|
||||
</v-card-actions>
|
||||
|
||||
<div class="pb-4" v-if="$slots['below-actions']">
|
||||
<div v-if="$slots['below-actions']" class="pb-4">
|
||||
<slot name="below-actions"> </slot>
|
||||
</div>
|
||||
</v-card>
|
||||
@ -44,23 +49,29 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import i18n from "@/i18n.js";
|
||||
export default {
|
||||
<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",
|
||||
},
|
||||
titleIcon: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
modalWidth: {
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: "500",
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
top: {
|
||||
@ -68,7 +79,8 @@ export default {
|
||||
type: Boolean,
|
||||
},
|
||||
submitText: {
|
||||
default: () => i18n.t("general.create"),
|
||||
type: String,
|
||||
default: () => "Create",
|
||||
},
|
||||
keepOpen: {
|
||||
default: false,
|
||||
@ -85,8 +97,8 @@ export default {
|
||||
determineClose() {
|
||||
return this.submitted && !this.loading && !this.keepOpen;
|
||||
},
|
||||
displayTitleIcon() {
|
||||
return this.titleIcon || this.$globals.icons.user;
|
||||
displayicon() {
|
||||
return this.icon || this.$globals.icons.user;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@ -115,8 +127,7 @@ export default {
|
||||
this.dialog = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
64
frontend/composables/use-api.ts
Normal file
64
frontend/composables/use-api.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useContext } from "@nuxtjs/composition-api";
|
||||
import { NuxtAxiosInstance } from "@nuxtjs/axios";
|
||||
import { Api } from "~/api";
|
||||
import { ApiRequestInstance } from "~/types/api";
|
||||
|
||||
interface RequestResponse<T> {
|
||||
response: AxiosResponse<T> | null;
|
||||
data: T | null;
|
||||
error: any;
|
||||
}
|
||||
|
||||
const request = {
|
||||
async safe<T>(funcCall: any, url: string, data: object = {}): Promise<RequestResponse<T>> {
|
||||
const response = await funcCall(url, data).catch(function (error: object) {
|
||||
console.log(error);
|
||||
// Insert Generic Error Handling Here
|
||||
return { response: null, error, data: null };
|
||||
});
|
||||
return { response, error: null, data: response.data };
|
||||
},
|
||||
};
|
||||
|
||||
function getRequests(axoisInstance: NuxtAxiosInstance): ApiRequestInstance {
|
||||
const requests = {
|
||||
async get<T>(url: string, queryParams = {}): Promise<RequestResponse<T>> {
|
||||
let error = null;
|
||||
const response = await axoisInstance.get<T>(url, { params: { queryParams } }).catch((e) => {
|
||||
error = e;
|
||||
});
|
||||
if (response != null) {
|
||||
return { response, error, data: response?.data };
|
||||
}
|
||||
return { response: null, error, data: null };
|
||||
},
|
||||
|
||||
async post<T>(url: string, data: object) {
|
||||
return await request.safe<T>(axoisInstance.post, url, data);
|
||||
},
|
||||
|
||||
async put<T>(url: string, data: object) {
|
||||
return await request.safe<T>(axoisInstance.put, url, data);
|
||||
},
|
||||
|
||||
async patch<T>(url: string, data: object) {
|
||||
return await request.safe<T>(axoisInstance.patch, url, data);
|
||||
},
|
||||
|
||||
async delete<T>(url: string) {
|
||||
return await request.safe<T>(axoisInstance.delete, url);
|
||||
},
|
||||
};
|
||||
return requests;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const useApi = function (): Api {
|
||||
const { $axios } = useContext();
|
||||
const requests = getRequests($axios);
|
||||
|
||||
return new Api(requests)
|
||||
};
|
497
frontend/lang/messages/en-US.js
Normal file
497
frontend/lang/messages/en-US.js
Normal file
@ -0,0 +1,497 @@
|
||||
export default {
|
||||
about: {
|
||||
about: "About",
|
||||
"about-mealie": "About Mealie",
|
||||
"api-docs": "API Docs",
|
||||
"api-port": "API Port",
|
||||
"application-mode": "Application Mode",
|
||||
"database-type": "Database Type",
|
||||
"database-url": "Database URL",
|
||||
"default-group": "Default Group",
|
||||
demo: "Demo",
|
||||
"demo-status": "Demo Status",
|
||||
development: "Development",
|
||||
docs: "Docs",
|
||||
"download-log": "Download Log",
|
||||
"download-recipe-json": "Last Scraped JSON",
|
||||
github: "Github",
|
||||
"log-lines": "Log Lines",
|
||||
"not-demo": "Not Demo",
|
||||
portfolio: "Portfolio",
|
||||
production: "Production",
|
||||
support: "Support",
|
||||
version: "Version",
|
||||
},
|
||||
asset: {
|
||||
assets: "Assets",
|
||||
code: "Code",
|
||||
file: "File",
|
||||
image: "Image",
|
||||
"new-asset": "New Asset",
|
||||
pdf: "PDF",
|
||||
recipe: "Recipe",
|
||||
"show-assets": "Show Assets",
|
||||
},
|
||||
category: {
|
||||
"category-created": "Category created",
|
||||
"category-creation-failed": "Category creation failed",
|
||||
"category-deleted": "Category Deleted",
|
||||
"category-deletion-failed": "Category deletion failed",
|
||||
"category-filter": "Category Filter",
|
||||
"category-update-failed": "Category update failed",
|
||||
"category-updated": "Category updated",
|
||||
"uncategorized-count": "Uncategorized {count}",
|
||||
},
|
||||
events: {
|
||||
"apprise-url": "Apprise URL",
|
||||
database: "Database",
|
||||
"delete-event": "Delete Event",
|
||||
"new-notification-form-description":
|
||||
"Mealie uses the Apprise library to generate notifications. They offer many options for services to use for notifications. Refer to their wiki for a comprehensive guide on how to create the URL for your service. If available, selecting the type of your notification may include extra features.",
|
||||
"new-version": "New version available!",
|
||||
notification: "Notification",
|
||||
refresh: "Refresh",
|
||||
scheduled: "Scheduled",
|
||||
"something-went-wrong": "Something Went Wrong!",
|
||||
"subscribed-events": "Subscribed Events",
|
||||
"test-message-sent": "Test Message Sent",
|
||||
},
|
||||
general: {
|
||||
cancel: "Cancel",
|
||||
clear: "Clear",
|
||||
close: "Close",
|
||||
confirm: "Confirm",
|
||||
"confirm-delete-generic": "Are you sure you want to delete this?",
|
||||
copied: "Copied",
|
||||
create: "Create",
|
||||
created: "Created",
|
||||
custom: "Custom",
|
||||
dashboard: "Dashboard",
|
||||
delete: "Delete",
|
||||
disabled: "Disabled",
|
||||
download: "Download",
|
||||
edit: "Edit",
|
||||
enabled: "Enabled",
|
||||
exception: "Exception",
|
||||
"failed-count": "Failed: {count}",
|
||||
"failure-uploading-file": "Failure uploading file",
|
||||
favorites: "Favorites",
|
||||
"field-required": "Field Required",
|
||||
"file-folder-not-found": "File/folder not found",
|
||||
"file-uploaded": "File uploaded",
|
||||
filter: "Filter",
|
||||
friday: "Friday",
|
||||
general: "General",
|
||||
get: "Get",
|
||||
home: "Home",
|
||||
image: "Image",
|
||||
"image-upload-failed": "Image upload failed",
|
||||
import: "Import",
|
||||
json: "JSON",
|
||||
keyword: "Keyword",
|
||||
"link-copied": "Link Copied",
|
||||
"loading-recipes": "Loading Recipes",
|
||||
monday: "Monday",
|
||||
name: "Name",
|
||||
new: "New",
|
||||
no: "No",
|
||||
"no-recipe-found": "No Recipe Found",
|
||||
ok: "OK",
|
||||
options: "Options:",
|
||||
print: "Print",
|
||||
random: "Random",
|
||||
rating: "Rating",
|
||||
recent: "Recent",
|
||||
recipe: "Recipe",
|
||||
recipes: "Recipes",
|
||||
"rename-object": "Rename {0}",
|
||||
reset: "Reset",
|
||||
saturday: "Saturday",
|
||||
save: "Save",
|
||||
settings: "Settings",
|
||||
share: "Share",
|
||||
shuffle: "Shuffle",
|
||||
sort: "Sort",
|
||||
"sort-alphabetically": "Alphabetical",
|
||||
status: "Status",
|
||||
submit: "Submit",
|
||||
"success-count": "Success: {count}",
|
||||
sunday: "Sunday",
|
||||
templates: "Templates:",
|
||||
test: "Test",
|
||||
themes: "Themes",
|
||||
thursday: "Thursday",
|
||||
token: "Token",
|
||||
tuesday: "Tuesday",
|
||||
type: "Type",
|
||||
update: "Update",
|
||||
updated: "Updated",
|
||||
upload: "Upload",
|
||||
url: "URL",
|
||||
view: "View",
|
||||
wednesday: "Wednesday",
|
||||
yes: "Yes",
|
||||
},
|
||||
group: {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Are you sure you want to delete <b>{groupName}<b/>?",
|
||||
"cannot-delete-default-group": "Cannot delete default group",
|
||||
"cannot-delete-group-with-users": "Cannot delete group with users",
|
||||
"confirm-group-deletion": "Confirm Group Deletion",
|
||||
"create-group": "Create Group",
|
||||
"error-updating-group": "Error updating group",
|
||||
group: "Group",
|
||||
"group-deleted": "Group deleted",
|
||||
"group-deletion-failed": "Group deletion failed",
|
||||
"group-id-with-value": "Group ID: {groupID}",
|
||||
"group-name": "Group Name",
|
||||
"group-not-found": "Group not found",
|
||||
"group-with-value": "Group: {groupID}",
|
||||
groups: "Groups",
|
||||
"manage-groups": "Manage Groups",
|
||||
"user-group": "User Group",
|
||||
"user-group-created": "User Group Created",
|
||||
"user-group-creation-failed": "User Group Creation Failed",
|
||||
},
|
||||
"meal-plan": {
|
||||
"create-a-new-meal-plan": "Create a New Meal Plan",
|
||||
"dinner-this-week": "Dinner This Week",
|
||||
"dinner-today": "Dinner Today",
|
||||
"dinner-tonight": "DINNER TONIGHT",
|
||||
"edit-meal-plan": "Edit Meal Plan",
|
||||
"end-date": "End Date",
|
||||
group: "Group (Beta)",
|
||||
main: "Main",
|
||||
"meal-planner": "Meal Planner",
|
||||
"meal-plans": "Meal Plans",
|
||||
"mealplan-categories": "MEALPLAN CATEGORIES",
|
||||
"mealplan-created": "Mealplan created",
|
||||
"mealplan-creation-failed": "Mealplan creation failed",
|
||||
"mealplan-deleted": "Mealplan Deleted",
|
||||
"mealplan-deletion-failed": "Mealplan deletion failed",
|
||||
"mealplan-settings": "Mealplan Settings",
|
||||
"mealplan-update-failed": "Mealplan update failed",
|
||||
"mealplan-updated": "Mealplan Updated",
|
||||
"no-meal-plan-defined-yet": "No meal plan defined yet",
|
||||
"no-meal-planned-for-today": "No meal planned for today",
|
||||
"only-recipes-with-these-categories-will-be-used-in-meal-plans":
|
||||
"Only recipes with these categories will be used in Meal Plans",
|
||||
planner: "Planner",
|
||||
"quick-week": "Quick Week",
|
||||
side: "Side",
|
||||
sides: "Sides",
|
||||
"start-date": "Start Date",
|
||||
},
|
||||
migration: {
|
||||
chowdown: {
|
||||
description: "Migrate data from Chowdown",
|
||||
title: "Chowdown",
|
||||
},
|
||||
"migration-data-removed": "Migration data removed",
|
||||
nextcloud: {
|
||||
description: "Migrate data from a Nextcloud Cookbook instance",
|
||||
title: "Nextcloud Cookbook",
|
||||
},
|
||||
"no-migration-data-available": "No Migration Data Available",
|
||||
"recipe-migration": "Recipe Migration",
|
||||
},
|
||||
"new-recipe": {
|
||||
"bulk-add": "Bulk Add",
|
||||
"error-details":
|
||||
"Only websites containing ld+json or microdata can be imported by Mealie. Most major recipe websites support this data structure. If your site cannot be imported but there is json data in the log, please submit a github issue with the URL and data.",
|
||||
"error-title": "Looks Like We Couldn't Find Anything",
|
||||
"from-url": "Import a Recipe",
|
||||
"github-issues": "GitHub Issues",
|
||||
"google-ld-json-info": "Google ld+json Info",
|
||||
"must-be-a-valid-url": "Must be a Valid URL",
|
||||
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list":
|
||||
"Paste in your recipe data. Each line will be treated as an item in a list",
|
||||
"recipe-markup-specification": "Recipe Markup Specification",
|
||||
"recipe-url": "Recipe URL",
|
||||
"upload-a-recipe": "Upload a Recipe",
|
||||
"upload-individual-zip-file": "Upload an individual .zip file exported from another Mealie instance.",
|
||||
"url-form-hint": "Copy and paste a link from your favorite recipe website",
|
||||
"view-scraped-data": "View Scraped Data",
|
||||
},
|
||||
page: {
|
||||
"404-page-not-found": "404 Page not found",
|
||||
"all-recipes": "All Recipes",
|
||||
"new-page-created": "New page created",
|
||||
page: "Page",
|
||||
"page-creation-failed": "Page creation failed",
|
||||
"page-deleted": "Page deleted",
|
||||
"page-deletion-failed": "Page deletion failed",
|
||||
"page-update-failed": "Page update failed",
|
||||
"page-updated": "Page updated",
|
||||
"pages-update-failed": "Pages update failed",
|
||||
"pages-updated": "Pages updated",
|
||||
},
|
||||
recipe: {
|
||||
"add-key": "Add Key",
|
||||
"add-to-favorites": "Add to Favorites",
|
||||
"api-extras": "API Extras",
|
||||
calories: "Calories",
|
||||
"calories-suffix": "calories",
|
||||
"carbohydrate-content": "Carbohydrate",
|
||||
categories: "Categories",
|
||||
"comment-action": "Comment",
|
||||
comments: "Comments",
|
||||
"delete-confirmation": "Are you sure you want to delete this recipe?",
|
||||
"delete-recipe": "Delete Recipe",
|
||||
description: "Description",
|
||||
"disable-amount": "Disable Ingredient Amounts",
|
||||
"disable-comments": "Disable Comments",
|
||||
"fat-content": "Fat",
|
||||
"fiber-content": "Fiber",
|
||||
grams: "grams",
|
||||
ingredient: "Ingredient",
|
||||
ingredients: "Ingredients",
|
||||
"insert-section": "Insert Section",
|
||||
instructions: "Instructions",
|
||||
"key-name-required": "Key Name Required",
|
||||
"landscape-view-coming-soon": "Landscape View (Coming Soon)",
|
||||
milligrams: "milligrams",
|
||||
"new-key-name": "New Key Name",
|
||||
"no-white-space-allowed": "No White Space Allowed",
|
||||
note: "Note",
|
||||
nutrition: "Nutrition",
|
||||
"object-key": "Object Key",
|
||||
"object-value": "Object Value",
|
||||
"original-url": "Original URL",
|
||||
"perform-time": "Cook Time",
|
||||
"prep-time": "Prep Time",
|
||||
"protein-content": "Protein",
|
||||
"public-recipe": "Public Recipe",
|
||||
"recipe-created": "Recipe created",
|
||||
"recipe-creation-failed": "Recipe creation failed",
|
||||
"recipe-deleted": "Recipe deleted",
|
||||
"recipe-image": "Recipe Image",
|
||||
"recipe-image-updated": "Recipe image updated",
|
||||
"recipe-name": "Recipe Name",
|
||||
"recipe-settings": "Recipe Settings",
|
||||
"recipe-update-failed": "Recipe update failed",
|
||||
"recipe-updated": "Recipe updated",
|
||||
"remove-from-favorites": "Remove from Favorites",
|
||||
"remove-section": "Remove Section",
|
||||
"save-recipe-before-use": "Save recipe before use",
|
||||
"section-title": "Section Title",
|
||||
servings: "Servings",
|
||||
"share-recipe-message": "I wanted to share my {0} recipe with you.",
|
||||
"show-nutrition-values": "Show Nutrition Values",
|
||||
"sodium-content": "Sodium",
|
||||
"step-index": "Step: {step}",
|
||||
"sugar-content": "Sugar",
|
||||
title: "Title",
|
||||
"total-time": "Total Time",
|
||||
"unable-to-delete-recipe": "Unable to Delete Recipe",
|
||||
},
|
||||
reicpe: {
|
||||
"no-recipe": "No Recipe",
|
||||
},
|
||||
search: {
|
||||
"advanced-search": "Advanced Search",
|
||||
and: "and",
|
||||
exclude: "Exclude",
|
||||
include: "Include",
|
||||
"max-results": "Max Results",
|
||||
or: "Or",
|
||||
results: "Results",
|
||||
search: "Search",
|
||||
"search-mealie": "Search Mealie (press /)",
|
||||
"search-placeholder": "Search...",
|
||||
"tag-filter": "Tag Filter",
|
||||
},
|
||||
settings: {
|
||||
"add-a-new-theme": "Add a New Theme",
|
||||
"admin-settings": "Admin Settings",
|
||||
backup: {
|
||||
"backup-created-at-response-export_path": "Backup Created at {path}",
|
||||
"backup-deleted": "Backup deleted",
|
||||
"backup-tag": "Backup Tag",
|
||||
"create-heading": "Create a Backup",
|
||||
"delete-backup": "Delete Backup",
|
||||
"error-creating-backup-see-log-file": "Error Creating Backup. See Log File",
|
||||
"full-backup": "Full Backup",
|
||||
"import-summary": "Import Summary",
|
||||
"partial-backup": "Partial Backup",
|
||||
"unable-to-delete-backup": "Unable to Delete Backup.",
|
||||
},
|
||||
"backup-and-exports": "Backups",
|
||||
"change-password": "Change Password",
|
||||
current: "Version:",
|
||||
"custom-pages": "Custom Pages",
|
||||
"edit-page": "Edit Page",
|
||||
events: "Events",
|
||||
"first-day-of-week": "First day of the week",
|
||||
"group-settings-updated": "Group Settings Updated",
|
||||
homepage: {
|
||||
"all-categories": "All Categories",
|
||||
"card-per-section": "Card Per Section",
|
||||
"home-page": "Home Page",
|
||||
"home-page-sections": "Home Page Sections",
|
||||
"show-recent": "Show Recent",
|
||||
},
|
||||
language: "Language",
|
||||
latest: "Latest",
|
||||
"local-api": "Local API",
|
||||
"locale-settings": "Locale settings",
|
||||
migrations: "Migrations",
|
||||
"new-page": "New Page",
|
||||
notify: "Notify",
|
||||
organize: "Organize",
|
||||
"page-name": "Page Name",
|
||||
pages: "Pages",
|
||||
profile: "Profile",
|
||||
"remove-existing-entries-matching-imported-entries": "Remove existing entries matching imported entries",
|
||||
"set-new-time": "Set New Time",
|
||||
"settings-update-failed": "Settings update failed",
|
||||
"settings-updated": "Settings updated",
|
||||
"site-settings": "Site Settings",
|
||||
theme: {
|
||||
accent: "Accent",
|
||||
dark: "Dark",
|
||||
"default-to-system": "Default to system",
|
||||
error: "Error",
|
||||
"error-creating-theme-see-log-file": "Error creating theme. See log file.",
|
||||
"error-deleting-theme": "Error deleting theme",
|
||||
"error-updating-theme": "Error updating theme",
|
||||
info: "Info",
|
||||
light: "Light",
|
||||
primary: "Primary",
|
||||
secondary: "Secondary",
|
||||
success: "Success",
|
||||
"switch-to-dark-mode": "Switch to dark mode",
|
||||
"switch-to-light-mode": "Switch to light mode",
|
||||
"theme-deleted": "Theme deleted",
|
||||
"theme-name": "Theme Name",
|
||||
"theme-name-is-required": "Theme Name is required.",
|
||||
"theme-saved": "Theme Saved",
|
||||
"theme-updated": "Theme updated",
|
||||
warning: "Warning",
|
||||
},
|
||||
token: {
|
||||
"active-tokens": "ACTIVE TOKENS",
|
||||
"api-token": "API Token",
|
||||
"api-tokens": "API Tokens",
|
||||
"copy-this-token-for-use-with-an-external-application-this-token-will-not-be-viewable-again":
|
||||
"Copy this token for use with an external application. This token will not be viewable again.",
|
||||
"create-an-api-token": "Create an API Token",
|
||||
"token-name": "Token Name",
|
||||
},
|
||||
toolbox: {
|
||||
"assign-all": "Assign All",
|
||||
"bulk-assign": "Bulk Assign",
|
||||
"new-name": "New Name",
|
||||
"no-unused-items": "No Unused Items",
|
||||
"recipes-affected": "No Recipes Affected|One Recipe Affected|{count} Recipes Affected",
|
||||
"remove-unused": "Remove Unused",
|
||||
"title-case-all": "Title Case All",
|
||||
toolbox: "Toolbox",
|
||||
unorganized: "Unorganized",
|
||||
},
|
||||
webhooks: {
|
||||
"test-webhooks": "Test Webhooks",
|
||||
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at":
|
||||
"The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
|
||||
"webhook-url": "Webhook URL",
|
||||
"webhooks-caps": "WEBHOOKS",
|
||||
},
|
||||
},
|
||||
"shopping-list": {
|
||||
"all-lists": "All Lists",
|
||||
"create-shopping-list": "Create Shopping List",
|
||||
"from-recipe": "From Recipe",
|
||||
"list-name": "List Name",
|
||||
"new-list": "New List",
|
||||
quantity: "Quantity: {0}",
|
||||
"shopping-list": "Shopping List",
|
||||
"shopping-lists": "Shopping Lists",
|
||||
},
|
||||
sidebar: {
|
||||
"all-recipes": "All Recipes",
|
||||
categories: "Categories",
|
||||
dashboard: "Dashboard",
|
||||
"home-page": "Home Page",
|
||||
"manage-users": "Manage Users",
|
||||
migrations: "Migrations",
|
||||
profile: "Profile",
|
||||
search: "Search",
|
||||
"site-settings": "Site Settings",
|
||||
tags: "Tags",
|
||||
toolbox: "Toolbox",
|
||||
},
|
||||
signup: {
|
||||
"error-signing-up": "Error Signing Up",
|
||||
"sign-up": "Sign Up",
|
||||
"sign-up-link-created": "Sign up link created",
|
||||
"sign-up-link-creation-failed": "Sign up link creation failed",
|
||||
"sign-up-links": "Sign Up Links",
|
||||
"sign-up-token-deleted": "Sign Up Token Deleted",
|
||||
"sign-up-token-deletion-failed": "Sign up token deletion failed",
|
||||
"welcome-to-mealie":
|
||||
"Welcome to Mealie! To become a user of this instance you are required to have a valid invitation link. If you haven't recieved an invitation you are unable to sign-up. To recieve a link, contact the sites administrator.",
|
||||
},
|
||||
tag: {
|
||||
"tag-created": "Tag created",
|
||||
"tag-creation-failed": "Tag creation failed",
|
||||
"tag-deleted": "Tag deleted",
|
||||
"tag-deletion-failed": "Tag deletion failed",
|
||||
"tag-update-failed": "Tag update failed",
|
||||
"tag-updated": "Tag updated",
|
||||
tags: "Tags",
|
||||
"untagged-count": "Untagged {count}",
|
||||
},
|
||||
user: {
|
||||
admin: "Admin",
|
||||
"are-you-sure-you-want-to-delete-the-link": "Are you sure you want to delete the link <b>{link}<b/>?",
|
||||
"are-you-sure-you-want-to-delete-the-user":
|
||||
"Are you sure you want to delete the user <b>{activeName} ID: {activeId}<b/>?",
|
||||
"confirm-link-deletion": "Confirm Link Deletion",
|
||||
"confirm-password": "Confirm Password",
|
||||
"confirm-user-deletion": "Confirm User Deletion",
|
||||
"could-not-validate-credentials": "Could Not Validate Credentials",
|
||||
"create-link": "Create Link",
|
||||
"create-user": "Create User",
|
||||
"current-password": "Current Password",
|
||||
"e-mail-must-be-valid": "E-mail must be valid",
|
||||
"edit-user": "Edit User",
|
||||
email: "Email",
|
||||
"error-cannot-delete-super-user": "Error! Cannot Delete Super User",
|
||||
"existing-password-does-not-match": "Existing password does not match",
|
||||
"full-name": "Full Name",
|
||||
"link-id": "Link ID",
|
||||
"link-name": "Link Name",
|
||||
login: "Login",
|
||||
logout: "Logout",
|
||||
"manage-users": "Manage Users",
|
||||
"new-password": "New Password",
|
||||
"new-user": "New User",
|
||||
password: "Password",
|
||||
"password-has-been-reset-to-the-default-password": "Password has been reset to the default password",
|
||||
"password-must-match": "Password must match",
|
||||
"password-reset-failed": "Password reset failed",
|
||||
"password-updated": "Password updated",
|
||||
"reset-password": "Reset Password",
|
||||
"sign-in": "Sign in",
|
||||
"total-mealplans": "Total MealPlans",
|
||||
"total-users": "Total Users",
|
||||
"upload-photo": "Upload Photo",
|
||||
"use-8-characters-or-more-for-your-password": "Use 8 characters or more for your password",
|
||||
user: "User",
|
||||
"user-created": "User created",
|
||||
"user-creation-failed": "User creation failed",
|
||||
"user-deleted": "User deleted",
|
||||
"user-id": "User ID",
|
||||
"user-id-with-value": "User ID: {id}",
|
||||
"user-password": "User Password",
|
||||
"user-successfully-logged-in": "User Successfully Logged In",
|
||||
"user-update-failed": "User update failed",
|
||||
"user-updated": "User updated",
|
||||
username: "Username",
|
||||
users: "Users",
|
||||
"users-header": "USERS",
|
||||
"webhook-time": "Webhook Time",
|
||||
"webhooks-enabled": "Webhooks Enabled",
|
||||
"you-are-not-allowed-to-create-a-user": "You are not allowed to create a user",
|
||||
"you-are-not-allowed-to-delete-this-user": "You are not allowed to delete this user",
|
||||
},
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user