mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-11-25 13:14:06 -05:00
init 2
This commit is contained in:
44
frontend/src/components/MealPlan/EditPlan.vue
Normal file
44
frontend/src/components/MealPlan/EditPlan.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="accent white--text"> Edit Meal Plan </v-card-title>
|
||||
<v-card-text> </v-card-text>
|
||||
|
||||
<v-card-text>
|
||||
<MealPlanCard v-model="mealPlan.meals" />
|
||||
<v-row align="center" justify="end">
|
||||
<v-card-actions>
|
||||
<v-btn color="success" text @click="update"> Update </v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
</v-card-actions>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "../../api";
|
||||
import utils from "../../utils";
|
||||
import MealPlanCard from "./MealPlanCard";
|
||||
export default {
|
||||
components: {
|
||||
MealPlanCard,
|
||||
},
|
||||
props: {
|
||||
mealPlan: Object,
|
||||
},
|
||||
methods: {
|
||||
formatDate(timestamp) {
|
||||
let dateObject = new Date(timestamp);
|
||||
return utils.getDateAsPythonDate(dateObject);
|
||||
},
|
||||
async update() {
|
||||
this.process();
|
||||
await api.mealPlans.update(this.mealPlan.uid, this.mealPlan);
|
||||
this.$emit("updated");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
76
frontend/src/components/MealPlan/MealPlanCard.vue
Normal file
76
frontend/src/components/MealPlan/MealPlanCard.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<MealSelect
|
||||
:forceDialog="dialog"
|
||||
@close="dialog = false"
|
||||
@select="setSlug($event)"
|
||||
/>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
xl="3"
|
||||
v-for="(meal, index) in value"
|
||||
:key="index"
|
||||
>
|
||||
<v-hover v-slot="{ hover }" :open-delay="50">
|
||||
<v-card :class="{ 'on-hover': hover }" :elevation="hover ? 12 : 2">
|
||||
<v-img
|
||||
height="200"
|
||||
:src="getImage(meal.slug)"
|
||||
@click="selectRecipe(index)"
|
||||
></v-img>
|
||||
<v-card-title class="my-n3 mb-n6">{{ meal.dateText }}</v-card-title>
|
||||
<v-card-subtitle> {{ meal.slug }}</v-card-subtitle>
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from "../../utils";
|
||||
import MealSelect from "./MealSelect";
|
||||
export default {
|
||||
components: {
|
||||
MealSelect,
|
||||
},
|
||||
props: {
|
||||
value: Array,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
recipeData: [],
|
||||
cardData: [],
|
||||
activeIndex: 0,
|
||||
dialog: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getImage(slug) {
|
||||
if (slug) {
|
||||
return utils.getImageURL(slug);
|
||||
}
|
||||
},
|
||||
setSlug(slug) {
|
||||
let index = this.activeIndex;
|
||||
this.value[index]["slug"] = slug;
|
||||
},
|
||||
selectRecipe(index) {
|
||||
this.activeIndex = index;
|
||||
this.dialog = true;
|
||||
},
|
||||
getProperty(index, property) {
|
||||
try {
|
||||
return this.recipeData[index][property];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
124
frontend/src/components/MealPlan/MealPlanner.vue
Normal file
124
frontend/src/components/MealPlan/MealPlanner.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div>
|
||||
<EditPlan
|
||||
v-if="editMealPlan"
|
||||
:meal-plan="editMealPlan"
|
||||
@updated="planUpdated"
|
||||
/>
|
||||
<NewMeal v-else @created="requestMeals" />
|
||||
|
||||
<v-card class="my-1">
|
||||
<v-card-title class="accent white--text"> Meal Plans </v-card-title>
|
||||
|
||||
<v-timeline align-top :dense="$vuetify.breakpoint.smAndDown">
|
||||
<v-timeline-item
|
||||
class="px-1"
|
||||
v-for="(mealplan, i) in plannedMeals"
|
||||
:key="i"
|
||||
color="accent lighten-2"
|
||||
icon="mdi-silverware-variant"
|
||||
fill-dot
|
||||
>
|
||||
<v-card color="accent lighten-2" dark>
|
||||
<v-card-title class="title">
|
||||
{{ formatDate(mealplan.startDate) }} -
|
||||
{{ formatDate(mealplan.endDate) }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="white text--primary">
|
||||
<v-row dense align="center">
|
||||
<v-col></v-col>
|
||||
<v-col
|
||||
v-for="(meal, index) in mealplan.meals"
|
||||
:key="generateKey(meal.slug, index)"
|
||||
>
|
||||
<v-img
|
||||
class="rounded-lg"
|
||||
:src="getImage(meal.image)"
|
||||
height="80"
|
||||
width="80"
|
||||
>
|
||||
</v-img>
|
||||
</v-col>
|
||||
<v-col></v-col>
|
||||
</v-row>
|
||||
<v-btn
|
||||
color="accent lighten-2"
|
||||
class="mx-0"
|
||||
outlined
|
||||
@click="editPlan(mealplan.uid)"
|
||||
>
|
||||
Edit
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error lighten-2"
|
||||
class="mx-2"
|
||||
outlined
|
||||
@click="deletePlan(mealplan.uid)"
|
||||
>
|
||||
Delete
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-timeline-item>
|
||||
</v-timeline>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "../../api";
|
||||
import utils from "../../utils";
|
||||
import NewMeal from "./NewMeal";
|
||||
import EditPlan from "./EditPlan";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NewMeal,
|
||||
EditPlan,
|
||||
},
|
||||
data: () => ({
|
||||
plannedMeals: [],
|
||||
editMealPlan: null,
|
||||
}),
|
||||
async mounted() {
|
||||
this.requestMeals();
|
||||
},
|
||||
methods: {
|
||||
async requestMeals() {
|
||||
const response = await api.mealPlans.all();
|
||||
this.plannedMeals = response.data;
|
||||
},
|
||||
generateKey(name, index) {
|
||||
return utils.generateUniqueKey(name, index);
|
||||
},
|
||||
formatDate(timestamp) {
|
||||
let dateObject = new Date(timestamp);
|
||||
return utils.getDateAsTextAlt(dateObject);
|
||||
},
|
||||
getImage(image) {
|
||||
return utils.getImageURL(image);
|
||||
},
|
||||
|
||||
editPlan(id) {
|
||||
this.plannedMeals.forEach((element) => {
|
||||
if (element.uid === id) {
|
||||
console.log(element);
|
||||
this.editMealPlan = element;
|
||||
}
|
||||
});
|
||||
},
|
||||
planUpdated() {
|
||||
this.editMealPlan = null;
|
||||
this.requestMeals();
|
||||
},
|
||||
deletePlan(id) {
|
||||
api.mealPlans.delete(id);
|
||||
this.requestMeals();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
102
frontend/src/components/MealPlan/MealSelect.vue
Normal file
102
frontend/src/components/MealPlan/MealSelect.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<v-row justify="center">
|
||||
<v-dialog v-model="dialog" persistent max-width="800">
|
||||
<v-card>
|
||||
<v-card-title class="headline"> Choose a Recipe </v-card-title>
|
||||
<v-card-text>
|
||||
<v-autocomplete
|
||||
:items="avaiableRecipes"
|
||||
v-model="selected"
|
||||
clearable
|
||||
return
|
||||
dense
|
||||
hide-details
|
||||
hide-selected
|
||||
item-text="slug"
|
||||
label="Search for a Recipe"
|
||||
single-line
|
||||
>
|
||||
<template v-slot:no-data>
|
||||
<v-list-item>
|
||||
<v-list-item-title>
|
||||
Search for your Favorite
|
||||
<strong>Recipe</strong>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<template v-slot:item="{ item }">
|
||||
<v-row align="center" @click="dialog = false">
|
||||
<v-col sm="2">
|
||||
<v-img
|
||||
max-height="100"
|
||||
max-width="100"
|
||||
:src="getImage(item.image)"
|
||||
></v-img>
|
||||
</v-col>
|
||||
<v-col sm="10">
|
||||
<h3>
|
||||
{{ item.name }}
|
||||
</h3>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="secondary" text @click="dialog = false"> Close </v-btn>
|
||||
<v-btn color="secondary" text @click="dialog = false"> Select </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from "../../utils";
|
||||
export default {
|
||||
props: {
|
||||
forceDialog: Boolean,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
selected: "",
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
forceDialog() {
|
||||
this.dialog = this.forceDialog;
|
||||
},
|
||||
selected() {
|
||||
if (this.selected) {
|
||||
this.$emit("select", this.selected);
|
||||
}
|
||||
},
|
||||
dialog() {
|
||||
if (this.dialog === false) {
|
||||
this.$emit("close");
|
||||
} else {
|
||||
this.selected = "";
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
avaiableRecipes() {
|
||||
return this.$store.getters.getRecentRecipes;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getImage(slug) {
|
||||
return utils.getImageURL(slug);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
207
frontend/src/components/MealPlan/NewMeal.vue
Normal file
207
frontend/src/components/MealPlan/NewMeal.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="accent white--text">
|
||||
Create a New Meal Plan
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row dense>
|
||||
<v-col cols="12" lg="6" md="6" sm="12">
|
||||
<v-menu
|
||||
ref="menu1"
|
||||
v-model="menu1"
|
||||
:close-on-content-click="true"
|
||||
transition="scale-transition"
|
||||
offset-y
|
||||
max-width="290px"
|
||||
min-width="290px"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-text-field
|
||||
v-model="startComputedDateFormatted"
|
||||
label="Start Date"
|
||||
persistent-hint
|
||||
prepend-icon="mdi-calendar"
|
||||
readonly
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
></v-text-field>
|
||||
</template>
|
||||
<v-date-picker
|
||||
v-model="startDate"
|
||||
no-title
|
||||
@input="menu2 = false"
|
||||
></v-date-picker>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
<v-col cols="12" lg="6" md="6" sm="12">
|
||||
<v-menu
|
||||
ref="menu2"
|
||||
v-model="menu2"
|
||||
:close-on-content-click="true"
|
||||
transition="scale-transition"
|
||||
offset-y
|
||||
max-width="290px"
|
||||
min-width="290px"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-text-field
|
||||
v-model="endComputedDateFormatted"
|
||||
label="End Date"
|
||||
persistent-hint
|
||||
prepend-icon="mdi-calendar"
|
||||
readonly
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
></v-text-field>
|
||||
</template>
|
||||
<v-date-picker
|
||||
v-model="endDate"
|
||||
no-title
|
||||
@input="menu2 = false"
|
||||
></v-date-picker>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text>
|
||||
<MealPlanCard v-model="meals" />
|
||||
</v-card-text>
|
||||
<v-row align="center" justify="end">
|
||||
<v-card-actions>
|
||||
<v-btn color="success" @click="random" v-if="meals[1]" text>
|
||||
Random
|
||||
</v-btn>
|
||||
<v-btn color="success" @click="save" text> Save </v-btn>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon @click="show = !show"> </v-btn>
|
||||
</v-card-actions>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "../../api";
|
||||
import utils from "../../utils";
|
||||
import MealPlanCard from "./MealPlanCard";
|
||||
export default {
|
||||
components: {
|
||||
MealPlanCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
meals: [],
|
||||
|
||||
// Dates
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
menu1: false,
|
||||
menu2: false,
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
dateDif() {
|
||||
this.meals = [];
|
||||
for (let i = 0; i < this.dateDif; i++) {
|
||||
this.meals.push({
|
||||
slug: "",
|
||||
date: this.getDate(i),
|
||||
dateText: this.getDayText(i),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
items() {
|
||||
return this.$store.getters.getRecentRecipes;
|
||||
},
|
||||
actualStartDate() {
|
||||
return Date.parse(this.startDate);
|
||||
},
|
||||
actualEndDate() {
|
||||
return Date.parse(this.endDate);
|
||||
},
|
||||
dateDif() {
|
||||
let startDate = new Date(this.startDate);
|
||||
let endDate = new Date(this.endDate);
|
||||
|
||||
let dateDif = (endDate - startDate) / (1000 * 3600 * 24) + 1;
|
||||
|
||||
if (dateDif <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dateDif;
|
||||
},
|
||||
startComputedDateFormatted() {
|
||||
return this.formatDate(this.startDate);
|
||||
},
|
||||
endComputedDateFormatted() {
|
||||
return this.formatDate(this.endDate);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
get_random(list) {
|
||||
const object = list[Math.floor(Math.random() * list.length)];
|
||||
return object.slug;
|
||||
},
|
||||
random() {
|
||||
this.meals.forEach((element, index) => {
|
||||
this.meals[index]["slug"] = this.get_random(this.items);
|
||||
});
|
||||
},
|
||||
processTime(index) {
|
||||
let dateText = new Date(
|
||||
this.actualStartDate.valueOf() + 1000 * 3600 * 24 * index
|
||||
);
|
||||
return dateText;
|
||||
},
|
||||
getDayText(index) {
|
||||
const dateObj = this.processTime(index);
|
||||
return utils.getDateAsText(dateObj);
|
||||
},
|
||||
getDate(index) {
|
||||
const dateObj = this.processTime(index);
|
||||
return utils.getDateAsPythonDate(dateObj);
|
||||
},
|
||||
|
||||
async save() {
|
||||
const mealBody = {
|
||||
startDate: this.startDate,
|
||||
endDate: this.endDate,
|
||||
meals: this.meals,
|
||||
};
|
||||
await api.mealPlans.create(mealBody);
|
||||
this.$emit("created");
|
||||
this.startDate = null;
|
||||
this.endDate = null;
|
||||
this.meals = [];
|
||||
},
|
||||
|
||||
getImage(image) {
|
||||
return utils.getImageURL(image);
|
||||
},
|
||||
|
||||
formatDate(date) {
|
||||
if (!date) return null;
|
||||
|
||||
const [year, month, day] = date.split("-");
|
||||
return `${month}/${day}/${year}`;
|
||||
},
|
||||
parseDate(date) {
|
||||
if (!date) return null;
|
||||
|
||||
const [month, day, year] = date.split("/");
|
||||
return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
67
frontend/src/components/MealPlan/ThisWeek.vue
Normal file
67
frontend/src/components/MealPlan/ThisWeek.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<v-container fill-height>
|
||||
<v-row justify="center" align="center">
|
||||
<v-col sm="12">
|
||||
<v-card
|
||||
v-for="(meal, index) in mealPlan.meals"
|
||||
:key="index"
|
||||
class="my-2"
|
||||
>
|
||||
<v-row dense no-gutters align="center" justify="center">
|
||||
<v-col order="1" md="6" sm="12">
|
||||
<v-card flat>
|
||||
<v-card-title> {{ meal.name }} </v-card-title>
|
||||
<v-card-subtitle> {{ meal.dateText }}</v-card-subtitle>
|
||||
|
||||
<v-card-text> {{ meal.description }} </v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
color="accent"
|
||||
text
|
||||
@click="$router.push(`/recipe/${meal.slug}`)"
|
||||
>
|
||||
View Recipe
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col order-sm="0" :order-md="getOrder(index)" md="6" sm="12">
|
||||
<v-card>
|
||||
<v-img :src="getImage(meal.image)" max-height="300"> </v-img>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "../../api";
|
||||
import utils from "../../utils";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
mealPlan: {},
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.mealPlan = await api.mealPlans.thisWeek();
|
||||
console.log(this.mealPlan);
|
||||
},
|
||||
methods: {
|
||||
getOrder(index) {
|
||||
if (index % 2 == 0) return 2;
|
||||
else return 0;
|
||||
},
|
||||
getImage(image) {
|
||||
return utils.getImageURL(image);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
Reference in New Issue
Block a user