mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-02 20:15:24 +02:00
feature/mobile-layout (#431)
* lazy load cards * shopping list recipe search bug * admin layout fluid * site loader * username support * mobile tabs * set username at signup * update user tests * patch bug on shopping list * public mealplan links * support link (I'm a monster) * icon only on mobile * padding Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
8f8127a5fc
commit
822663905d
33 changed files with 273 additions and 119 deletions
|
@ -9,6 +9,7 @@ const mealPlanURLs = {
|
||||||
all: `${prefix}all`,
|
all: `${prefix}all`,
|
||||||
create: `${prefix}create`,
|
create: `${prefix}create`,
|
||||||
thisWeek: `${prefix}this-week`,
|
thisWeek: `${prefix}this-week`,
|
||||||
|
byId: planID => `${prefix}${planID}`,
|
||||||
update: planID => `${prefix}${planID}`,
|
update: planID => `${prefix}${planID}`,
|
||||||
delete: planID => `${prefix}${planID}`,
|
delete: planID => `${prefix}${planID}`,
|
||||||
today: `${prefix}today`,
|
today: `${prefix}today`,
|
||||||
|
@ -40,6 +41,11 @@ export const mealplanAPI = {
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getById(id) {
|
||||||
|
let response = await apiReq.get(mealPlanURLs.byId(id));
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
delete(id) {
|
delete(id) {
|
||||||
return apiReq.delete(
|
return apiReq.delete(
|
||||||
mealPlanURLs.delete(id),
|
mealPlanURLs.delete(id),
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
</div>
|
</div>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-tabs v-model="tab">
|
<v-tabs v-model="tab" show-arrows="">
|
||||||
<v-tab>{{ $t("general.recipes") }}</v-tab>
|
<v-tab>{{ $t("general.recipes") }}</v-tab>
|
||||||
<v-tab>{{ $t("general.themes") }}</v-tab>
|
<v-tab>{{ $t("general.themes") }}</v-tab>
|
||||||
<v-tab>{{ $t("general.settings") }}</v-tab>
|
<v-tab>{{ $t("general.settings") }}</v-tab>
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
v-model="user.email"
|
v-model="user.email"
|
||||||
prepend-icon="mdi-email"
|
prepend-icon="mdi-email"
|
||||||
validate-on-blur
|
validate-on-blur
|
||||||
:label="$t('user.email')"
|
:label="`${$t('user.email')} or ${$t('user.username')} `"
|
||||||
type="email"
|
type="email"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
|
|
|
@ -21,8 +21,15 @@
|
||||||
:prepend-icon="$globals.icons.user"
|
:prepend-icon="$globals.icons.user"
|
||||||
validate-on-blur
|
validate-on-blur
|
||||||
:rules="[existsRule]"
|
:rules="[existsRule]"
|
||||||
:label="$t('signup.display-name')"
|
:label="$t('user.full-name')"
|
||||||
type="email"
|
></v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
v-model="user.username"
|
||||||
|
light="light"
|
||||||
|
:prepend-icon="$globals.icons.user"
|
||||||
|
validate-on-blur
|
||||||
|
:rules="[existsRule]"
|
||||||
|
:label="$t('user.username')"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="user.email"
|
v-model="user.email"
|
||||||
|
@ -111,6 +118,7 @@ export default {
|
||||||
|
|
||||||
const userData = {
|
const userData = {
|
||||||
fullName: this.user.name,
|
fullName: this.user.name,
|
||||||
|
username: this.user.username,
|
||||||
email: this.user.email,
|
email: this.user.email,
|
||||||
group: "default",
|
group: "default",
|
||||||
password: this.user.password,
|
password: this.user.password,
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
<v-icon left dark>
|
<v-icon left dark>
|
||||||
mdi-clipboard-check
|
mdi-clipboard-check
|
||||||
</v-icon>
|
</v-icon>
|
||||||
{{ $t("general.coppied") }}!
|
<slot> {{ $t("general.coppied") }}! </slot>
|
||||||
</span>
|
</span>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="recipes">
|
<div v-if="recipes">
|
||||||
<v-app-bar color="transparent" flat class="mt-n1 rounded" v-if="!disableToolbar">
|
<v-app-bar color="transparent" flat class="mt-n1 flex-sm-wrap rounded " v-if="!disableToolbar">
|
||||||
<v-icon large left v-if="title">
|
<v-icon large left v-if="title">
|
||||||
{{ displayTitleIcon }}
|
{{ displayTitleIcon }}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
|
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn text @click="navigateRandom">
|
<v-btn :icon="$vuetify.breakpoint.xsOnly" text @click="navigateRandom">
|
||||||
<v-icon left>
|
<v-icon :left="!$vuetify.breakpoint.xsOnly">
|
||||||
mdi-dice-multiple
|
mdi-dice-multiple
|
||||||
</v-icon>
|
</v-icon>
|
||||||
{{ $t("general.random") }}
|
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.random") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-menu offset-y left v-if="$listeners.sort">
|
<v-menu offset-y left v-if="$listeners.sort">
|
||||||
<template v-slot:activator="{ on, attrs }">
|
<template v-slot:activator="{ on, attrs }">
|
||||||
<v-btn text v-bind="attrs" v-on="on" :loading="sortLoading">
|
<v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" v-on="on" :loading="sortLoading">
|
||||||
<v-icon left>
|
<v-icon :left="!$vuetify.breakpoint.xsOnly">
|
||||||
mdi-sort
|
mdi-sort
|
||||||
</v-icon>
|
</v-icon>
|
||||||
{{ $t("general.sort") }}
|
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.sort") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-list>
|
<v-list>
|
||||||
|
@ -58,14 +58,16 @@
|
||||||
<div v-if="recipes" class="mt-2">
|
<div v-if="recipes" class="mt-2">
|
||||||
<v-row v-if="!viewScale">
|
<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 :sm="6" :md="6" :lg="4" :xl="3" v-for="recipe in recipes.slice(0, cardLimit)" :key="recipe.name">
|
||||||
<RecipeCard
|
<v-lazy>
|
||||||
:name="recipe.name"
|
<RecipeCard
|
||||||
:description="recipe.description"
|
:name="recipe.name"
|
||||||
:slug="recipe.slug"
|
:description="recipe.description"
|
||||||
:rating="recipe.rating"
|
:slug="recipe.slug"
|
||||||
:image="recipe.image"
|
:rating="recipe.rating"
|
||||||
:tags="recipe.tags"
|
:image="recipe.image"
|
||||||
/>
|
:tags="recipe.tags"
|
||||||
|
/>
|
||||||
|
</v-lazy>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-row v-else dense>
|
<v-row v-else dense>
|
||||||
|
@ -78,33 +80,29 @@
|
||||||
v-for="recipe in recipes.slice(0, cardLimit)"
|
v-for="recipe in recipes.slice(0, cardLimit)"
|
||||||
:key="recipe.name"
|
:key="recipe.name"
|
||||||
>
|
>
|
||||||
<MobileRecipeCard
|
<v-lazy>
|
||||||
:name="recipe.name"
|
<MobileRecipeCard
|
||||||
:description="recipe.description"
|
:name="recipe.name"
|
||||||
:slug="recipe.slug"
|
:description="recipe.description"
|
||||||
:rating="recipe.rating"
|
:slug="recipe.slug"
|
||||||
:image="recipe.image"
|
:rating="recipe.rating"
|
||||||
:tags="recipe.tags"
|
:image="recipe.image"
|
||||||
/>
|
:tags="recipe.tags"
|
||||||
|
/>
|
||||||
|
</v-lazy>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
<div v-intersect="bumpList" class="d-flex">
|
<div v-intersect="bumpList" class="d-flex">
|
||||||
<v-expand-x-transition>
|
<v-expand-x-transition>
|
||||||
<v-progress-circular
|
<SiteLoader v-if="loading" :loading="loading" :size="150" />
|
||||||
v-if="loading"
|
|
||||||
class="mx-auto mt-1"
|
|
||||||
:size="50"
|
|
||||||
:width="7"
|
|
||||||
color="primary"
|
|
||||||
indeterminate
|
|
||||||
></v-progress-circular>
|
|
||||||
</v-expand-x-transition>
|
</v-expand-x-transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import SiteLoader from "@/components/UI/SiteLoader";
|
||||||
import RecipeCard from "../Recipe/RecipeCard";
|
import RecipeCard from "../Recipe/RecipeCard";
|
||||||
import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard";
|
import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard";
|
||||||
import { utils } from "@/utils";
|
import { utils } from "@/utils";
|
||||||
|
@ -114,6 +112,7 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
RecipeCard,
|
RecipeCard,
|
||||||
MobileRecipeCard,
|
MobileRecipeCard,
|
||||||
|
SiteLoader,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
disableToolbar: {
|
disableToolbar: {
|
||||||
|
@ -139,7 +138,7 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
sortLoading: false,
|
sortLoading: false,
|
||||||
cardLimit: 30,
|
cardLimit: 50,
|
||||||
loading: false,
|
loading: false,
|
||||||
EVENTS: {
|
EVENTS: {
|
||||||
az: "az",
|
az: "az",
|
||||||
|
|
|
@ -43,25 +43,31 @@
|
||||||
</div>
|
</div>
|
||||||
<router-link to="/search"> Advanced Search </router-link>
|
<router-link to="/search"> Advanced Search </router-link>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
<MobileRecipeCard
|
<v-card-actions v-if="loading">
|
||||||
v-for="(recipe, index) in results.slice(0, 10)"
|
<SiteLoader :loading="loading" />
|
||||||
:tabindex="index"
|
</v-card-actions>
|
||||||
:key="index"
|
<div v-else>
|
||||||
class="ma-1 arrow-nav"
|
<MobileRecipeCard
|
||||||
:name="recipe.name"
|
v-for="(recipe, index) in results.slice(0, 10)"
|
||||||
:description="recipe.description"
|
:tabindex="index"
|
||||||
:slug="recipe.slug"
|
:key="index"
|
||||||
:rating="recipe.rating"
|
class="ma-1 arrow-nav"
|
||||||
:image="recipe.image"
|
:name="recipe.name"
|
||||||
:route="true"
|
:description="recipe.description"
|
||||||
v-on="$listeners.selected ? { selected: () => grabRecipe(recipe) } : {}"
|
:slug="recipe.slug"
|
||||||
/>
|
:rating="recipe.rating"
|
||||||
|
:image="recipe.image"
|
||||||
|
:route="true"
|
||||||
|
v-on="$listeners.selected ? { selected: () => grabRecipe(recipe) } : {}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import SiteLoader from "@/components/UI/SiteLoader";
|
||||||
const SELECTED_EVENT = "selected";
|
const SELECTED_EVENT = "selected";
|
||||||
import FuseSearchBar from "@/components/UI/Search/FuseSearchBar";
|
import FuseSearchBar from "@/components/UI/Search/FuseSearchBar";
|
||||||
import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard";
|
import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard";
|
||||||
|
@ -69,9 +75,11 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
FuseSearchBar,
|
FuseSearchBar,
|
||||||
MobileRecipeCard,
|
MobileRecipeCard,
|
||||||
|
SiteLoader,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
loading: false,
|
||||||
selectedIndex: -1,
|
selectedIndex: -1,
|
||||||
dialog: false,
|
dialog: false,
|
||||||
searchString: "",
|
searchString: "",
|
||||||
|
@ -82,14 +90,17 @@ export default {
|
||||||
$route() {
|
$route() {
|
||||||
this.dialog = false;
|
this.dialog = false;
|
||||||
},
|
},
|
||||||
dialog(val) {
|
async dialog(val) {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
this.resetSelected();
|
this.resetSelected();
|
||||||
|
} else if (this.allItems.length <= 0) {
|
||||||
|
this.loading = true;
|
||||||
|
await this.$store.dispatch("requestAllRecipes");
|
||||||
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch("requestAllRecipes");
|
|
||||||
document.addEventListener("keydown", this.onUpDown);
|
document.addEventListener("keydown", this.onUpDown);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
|
25
frontend/src/components/UI/SiteLoader.vue
Normal file
25
frontend/src/components/UI/SiteLoader.vue
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<v-progress-circular class="mx-auto" :width="size / 20" :size="size" color="primary lighten-2" indeterminate>
|
||||||
|
<div class="text-center">
|
||||||
|
<v-icon :size="size / 2" color="primary lighten-2">
|
||||||
|
{{ $globals.icons.primary }}
|
||||||
|
</v-icon>
|
||||||
|
<div>
|
||||||
|
Loading Recipes
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-progress-circular>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
loading: {
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -18,12 +18,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<SearchBar
|
<div v-if="!isMobile" style="width: 350px;">
|
||||||
v-if="!isMobile"
|
<SearchBar :show-results="true" @selected="navigateFromSearch" :max-width="isMobile ? '100%' : '450px'" />
|
||||||
:show-results="true"
|
</div>
|
||||||
@selected="navigateFromSearch"
|
|
||||||
:max-width="isMobile ? '100%' : '450px'"
|
|
||||||
/>
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<v-btn icon @click="$refs.recipeSearch.open()">
|
<v-btn icon @click="$refs.recipeSearch.open()">
|
||||||
<v-icon> mdi-magnify </v-icon>
|
<v-icon> mdi-magnify </v-icon>
|
||||||
|
|
|
@ -29,6 +29,14 @@
|
||||||
|
|
||||||
<!-- Version List Item -->
|
<!-- Version List Item -->
|
||||||
<v-list nav dense class="fixedBottom" v-if="!isMain">
|
<v-list nav dense class="fixedBottom" v-if="!isMain">
|
||||||
|
<v-list-item href="https://github.com/sponsors/hay-kot" target="_target">
|
||||||
|
<v-list-item-icon >
|
||||||
|
<v-icon color="pink">
|
||||||
|
mdi-heart
|
||||||
|
</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
<v-list-item-title> Support </v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
<v-list-item to="/admin/about">
|
<v-list-item to="/admin/about">
|
||||||
<v-list-item-icon class="mr-3 pt-1">
|
<v-list-item-icon class="mr-3 pt-1">
|
||||||
<v-icon :color="newVersionAvailable ? 'red--text' : ''">
|
<v-icon :color="newVersionAvailable ? 'red--text' : ''">
|
||||||
|
|
|
@ -377,6 +377,7 @@
|
||||||
"untagged-count": "Untagged {count}"
|
"untagged-count": "Untagged {count}"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
|
"username": "Username",
|
||||||
"admin": "Admin",
|
"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-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/>?",
|
"are-you-sure-you-want-to-delete-the-user": "Are you sure you want to delete the user <b>{activeName} ID: {activeId}<b/>?",
|
||||||
|
|
|
@ -1,5 +1,33 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<v-app-bar color="primary">
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn href="https://github.com/sponsors/hay-kot" target="_blank" class="mx-1" color="secondary">
|
||||||
|
<v-icon left>
|
||||||
|
mdi-heart
|
||||||
|
</v-icon>
|
||||||
|
Support
|
||||||
|
</v-btn>
|
||||||
|
<v-btn href="https://github.com/hay-kot" target="_blank" class="mx-1" color="secondary">
|
||||||
|
<v-icon left>
|
||||||
|
mdi-github
|
||||||
|
</v-icon>
|
||||||
|
Github
|
||||||
|
</v-btn>
|
||||||
|
<v-btn href="https://hay-kot.dev" target="_blank" class="mx-1" color="secondary">
|
||||||
|
<v-icon left>
|
||||||
|
mdi-account
|
||||||
|
</v-icon>
|
||||||
|
Portfolio
|
||||||
|
</v-btn>
|
||||||
|
<v-btn href="https://hay-kot.github.io/mealie/" target="_blank" class="mx-1" color="secondary">
|
||||||
|
<v-icon left>
|
||||||
|
mdi-folder-outline
|
||||||
|
</v-icon>
|
||||||
|
Docs
|
||||||
|
</v-btn>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
</v-app-bar>
|
||||||
<v-card class="mt-3">
|
<v-card class="mt-3">
|
||||||
<v-card-title class="headline">
|
<v-card-title class="headline">
|
||||||
{{ $t("about.about-mealie") }}
|
{{ $t("about.about-mealie") }}
|
||||||
|
|
|
@ -155,6 +155,7 @@ export default {
|
||||||
sortable: false,
|
sortable: false,
|
||||||
value: "id",
|
value: "id",
|
||||||
},
|
},
|
||||||
|
{ text: this.$t("user.username"), value: "username" },
|
||||||
{ text: this.$t("user.full-name"), value: "fullName" },
|
{ text: this.$t("user.full-name"), value: "fullName" },
|
||||||
{ text: this.$t("user.email"), value: "email" },
|
{ text: this.$t("user.email"), value: "email" },
|
||||||
{ text: this.$t("group.group"), value: "group" },
|
{ text: this.$t("group.group"), value: "group" },
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-card flat>
|
<v-card flat>
|
||||||
<v-tabs v-model="tab" background-color="primary" centered dark icons-and-text>
|
<v-tabs v-model="tab" background-color="primary" centered dark icons-and-text show-arrows>
|
||||||
<v-tabs-slider></v-tabs-slider>
|
<v-tabs-slider></v-tabs-slider>
|
||||||
|
|
||||||
<v-tab href="#users">
|
<v-tab href="#users">
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
</div>
|
</div>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-tabs v-model="tab">
|
<v-tabs v-model="tab" show-arrows="">
|
||||||
<v-tab>{{ $t("general.recipes") }}</v-tab>
|
<v-tab>{{ $t("general.recipes") }}</v-tab>
|
||||||
</v-tabs>
|
</v-tabs>
|
||||||
<v-tabs-items v-model="tab">
|
<v-tabs-items v-model="tab">
|
||||||
|
|
|
@ -67,7 +67,15 @@
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:bottom>
|
<template v-slot:bottom>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-form>
|
<v-form ref="userUpdate">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('user.username')"
|
||||||
|
required
|
||||||
|
v-model="user.username"
|
||||||
|
:rules="[existsRule]"
|
||||||
|
validate-on-blur
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
:label="$t('user.full-name')"
|
:label="$t('user.full-name')"
|
||||||
required
|
required
|
||||||
|
@ -151,6 +159,9 @@ export default {
|
||||||
this.user.avatar = avatar;
|
this.user.avatar = avatar;
|
||||||
},
|
},
|
||||||
async updateUser() {
|
async updateUser() {
|
||||||
|
if (!this.$refs.userUpdate.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
const response = await api.users.update(this.user);
|
const response = await api.users.update(this.user);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-card flat>
|
<v-card flat>
|
||||||
<v-tabs v-model="tab" background-color="primary" centered dark icons-and-text>
|
<v-tabs v-model="tab" background-color="primary" centered dark icons-and-text show-arrows>
|
||||||
<v-tabs-slider></v-tabs-slider>
|
<v-tabs-slider></v-tabs-slider>
|
||||||
|
|
||||||
<v-tab href="#event-notifications">
|
<v-tab href="#event-notifications">
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<v-container fluid class="pa-5">
|
||||||
<v-container>
|
<v-slide-x-transition hide-on-leave>
|
||||||
<v-slide-x-transition hide-on-leave>
|
<router-view></router-view>
|
||||||
<router-view></router-view>
|
</v-slide-x-transition>
|
||||||
</v-slide-x-transition>
|
</v-container>
|
||||||
</v-container>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -37,6 +37,8 @@
|
||||||
</v-icon>
|
</v-icon>
|
||||||
Shopping List
|
Shopping List
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<TheCopyButton color="info" :copy-text="mealPlanURL(mealplan.uid)"> Link Coppied </TheCopyButton>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
|
|
||||||
<v-list class="mt-0 pt-0">
|
<v-list class="mt-0 pt-0">
|
||||||
|
@ -84,11 +86,12 @@ import { api } from "@/api";
|
||||||
import { utils } from "@/utils";
|
import { utils } from "@/utils";
|
||||||
import NewMeal from "@/components/MealPlan/MealPlanNew";
|
import NewMeal from "@/components/MealPlan/MealPlanNew";
|
||||||
import EditPlan from "@/components/MealPlan/MealPlanEditor";
|
import EditPlan from "@/components/MealPlan/MealPlanEditor";
|
||||||
|
import TheCopyButton from "@/components/UI/Buttons/TheCopyButton";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NewMeal,
|
NewMeal,
|
||||||
EditPlan,
|
EditPlan,
|
||||||
|
TheCopyButton,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
plannedMeals: [],
|
plannedMeals: [],
|
||||||
|
@ -103,6 +106,9 @@ export default {
|
||||||
this.plannedMeals = response.data;
|
this.plannedMeals = response.data;
|
||||||
console.log(this.plannedMeals);
|
console.log(this.plannedMeals);
|
||||||
},
|
},
|
||||||
|
mealPlanURL(uid) {
|
||||||
|
return window.location.origin + "/meal-plan?id=" + uid;
|
||||||
|
},
|
||||||
generateKey(name, index) {
|
generateKey(name, index) {
|
||||||
return utils.generateUniqueKey(name, index);
|
return utils.generateUniqueKey(name, index);
|
||||||
},
|
},
|
||||||
|
|
|
@ -46,11 +46,20 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.mealPlan = await api.mealPlans.thisWeek();
|
if (this.mealplanId) {
|
||||||
|
this.mealPlan = await api.mealPlans.getById(this.mealplanId);
|
||||||
|
} else {
|
||||||
|
this.mealPlan = await api.mealPlans.thisWeek();
|
||||||
|
}
|
||||||
|
console.log(this.mealPlans);
|
||||||
if (!this.mealPlan) {
|
if (!this.mealPlan) {
|
||||||
utils.notify.warning(this.$t("meal-plan.no-meal-plan-defined-yet"));
|
utils.notify.warning(this.$t("meal-plan.no-meal-plan-defined-yet"));
|
||||||
}
|
}
|
||||||
console.log(this.mealPlan);
|
},
|
||||||
|
computed: {
|
||||||
|
mealplanId() {
|
||||||
|
return this.$route.query.id || false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getOrder(index) {
|
getOrder(index) {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
|
|
||||||
<CardSection
|
<CardSection
|
||||||
title-icon=""
|
title-icon=""
|
||||||
:sortable="true"
|
:sortable="true"
|
||||||
|
@ -8,14 +7,19 @@
|
||||||
:recipes="shownRecipes"
|
:recipes="shownRecipes"
|
||||||
@sort="assignSorted"
|
@sort="assignSorted"
|
||||||
/>
|
/>
|
||||||
|
<v-row class="d-flex">
|
||||||
|
<SiteLoader class="mx-auto" v-if="loading" :loading="loading" :size="200" />
|
||||||
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import SiteLoader from "@/components/UI/SiteLoader";
|
||||||
import CardSection from "@/components/UI/CardSection";
|
import CardSection from "@/components/UI/CardSection";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
SiteLoader,
|
||||||
CardSection,
|
CardSection,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
|
|
||||||
<div v-if="render">
|
<div v-if="render">
|
||||||
<v-tabs v-model="tab" background-color="transparent" grow>
|
<v-tabs v-model="tab" background-color="transparent" grow show-arrows="">
|
||||||
<v-tab v-for="item in page.categories" :key="item.slug" :href="`#${item.slug}`">
|
<v-tab v-for="item in page.categories" :key="item.slug" :href="`#${item.slug}`">
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</v-tab>
|
</v-tab>
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-divider class="mx-2 mb-1"></v-divider>
|
<v-divider class="mx-2 mb-1"></v-divider>
|
||||||
|
|
||||||
<SearchDialog ref="searchRecipe" @select="importIngredients" />
|
<SearchDialog ref="searchRecipe" @selected="importIngredients" />
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-row dense v-for="(item, index) in activeList.items" :key="index">
|
<v-row dense v-for="(item, index) in activeList.items" :key="index">
|
||||||
<v-col v-if="edit" cols="12" class="d-flex no-wrap align-center">
|
<v-col v-if="edit" cols="12" class="d-flex no-wrap align-center">
|
||||||
|
@ -80,7 +80,7 @@
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="12" class="d-flex no-wrap align-center">
|
<v-col cols="12" class="no-wrap align-center" :class="!edit ? 'd-flex' : null">
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-if="!edit"
|
v-if="!edit"
|
||||||
hide-details
|
hide-details
|
||||||
|
@ -97,18 +97,16 @@
|
||||||
</v-icon>
|
</v-icon>
|
||||||
|
|
||||||
<v-lazy>
|
<v-lazy>
|
||||||
<div>
|
<vue-markdown v-if="!edit" class="dense-markdown" :source="item.text"> </vue-markdown>
|
||||||
<vue-markdown v-if="!edit" class="dense-markdown" :source="item.text"> </vue-markdown>
|
<v-textarea
|
||||||
<v-textarea
|
single-line
|
||||||
single-line
|
rows="1"
|
||||||
rows="1"
|
auto-grow
|
||||||
auto-grow
|
class="mb-n2 pa-0"
|
||||||
class="mb-n2 pa-0"
|
dense
|
||||||
dense
|
v-else
|
||||||
v-else
|
v-model="activeList.items[index].text"
|
||||||
v-model="activeList.items[index].text"
|
></v-textarea>
|
||||||
></v-textarea>
|
|
||||||
</div>
|
|
||||||
</v-lazy>
|
</v-lazy>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-divider class="ma-1"></v-divider>
|
<v-divider class="ma-1"></v-divider>
|
||||||
|
@ -137,7 +135,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
|
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
|
||||||
import SearchDialog from "@/components/UI/Search/SearchDialog";
|
import SearchDialog from "@/components/UI/Dialogs/SearchDialog";
|
||||||
import TheCopyButton from "@/components/UI/Buttons/TheCopyButton";
|
import TheCopyButton from "@/components/UI/Buttons/TheCopyButton";
|
||||||
import VueMarkdown from "@adapttive/vue-markdown";
|
import VueMarkdown from "@adapttive/vue-markdown";
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
|
@ -194,8 +192,8 @@ export default {
|
||||||
openSearch() {
|
openSearch() {
|
||||||
this.$refs.searchRecipe.open();
|
this.$refs.searchRecipe.open();
|
||||||
},
|
},
|
||||||
async importIngredients(_, slug) {
|
async importIngredients(selected) {
|
||||||
const recipe = await api.recipes.requestDetails(slug);
|
const recipe = await api.recipes.requestDetails(selected.slug);
|
||||||
|
|
||||||
const ingredients = recipe.recipeIngredient.map(x => ({
|
const ingredients = recipe.recipeIngredient.map(x => ({
|
||||||
title: "",
|
title: "",
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
const Admin = () => import("@/pages/Admin");
|
const Admin = () => import(/* webpackChunkName: "admin" */ "@/pages/Admin");
|
||||||
const Migration = () => import("@/pages/Admin/Migration");
|
const Migration = () => import(/* webpackChunkName: "admin" */ "@/pages/Admin/Migration");
|
||||||
const Profile = () => import("@/pages/Admin/Profile");
|
const Profile = () => import(/* webpackChunkName: "admin" */ "@/pages/Admin/Profile");
|
||||||
const ManageUsers = () => import("@/pages/Admin/ManageUsers");
|
const ManageUsers = () => import(/* webpackChunkName: "admin" */ "@/pages/Admin/ManageUsers");
|
||||||
const Settings = () => import("@/pages/Admin/Settings");
|
const Settings = () => import(/* webpackChunkName: "admin" */ "@/pages/Admin/Settings");
|
||||||
const About = () => import("@/pages/Admin/About");
|
const About = () => import(/* webpackChunkName: "admin" */ "@/pages/Admin/About");
|
||||||
const ToolBox = () => import("@/pages/Admin/ToolBox");
|
const ToolBox = () => import(/* webpackChunkName: "admin" */ "@/pages/Admin/ToolBox");
|
||||||
const Dashboard = () => import("@/pages/Admin/Dashboard");
|
const Dashboard = () => import(/* webpackChunkName: "admin" */ "@/pages/Admin/Dashboard");
|
||||||
import { store } from "../store";
|
import { store } from "../store";
|
||||||
|
|
||||||
export const adminRoutes = {
|
export const adminRoutes = {
|
||||||
|
|
|
@ -5,6 +5,13 @@ import { utils } from "@/utils";
|
||||||
import i18n from "@/i18n.js";
|
import i18n from "@/i18n.js";
|
||||||
|
|
||||||
export const mealRoutes = [
|
export const mealRoutes = [
|
||||||
|
{
|
||||||
|
path: "/meal-plan",
|
||||||
|
component: ThisWeek,
|
||||||
|
meta: {
|
||||||
|
title: "meal-plan.dinner-this-week",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/meal-plan/planner",
|
path: "/meal-plan/planner",
|
||||||
component: Planner,
|
component: Planner,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
const ViewRecipe = () => import("@/pages/Recipe/ViewRecipe");
|
const ViewRecipe = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipe/ViewRecipe");
|
||||||
const NewRecipe = () => import("@/pages/Recipe/NewRecipe");
|
const NewRecipe = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipe/NewRecipe");
|
||||||
const CustomPage = () => import("@/pages/Recipes/CustomPage");
|
const CustomPage = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipes/CustomPage");
|
||||||
const AllRecipes = () => import("@/pages/Recipes/AllRecipes");
|
const AllRecipes = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipes/AllRecipes");
|
||||||
const CategoryTagPage = () => import("@/pages/Recipes/CategoryTagPage");
|
const CategoryTagPage = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipes/CategoryTagPage");
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
|
|
||||||
export const recipeRoutes = [
|
export const recipeRoutes = [
|
||||||
|
|
|
@ -41,6 +41,7 @@ const actions = {
|
||||||
this.commit("setRecentRecipes", hash);
|
this.commit("setRecentRecipes", hash);
|
||||||
},
|
},
|
||||||
async requestAllRecipes({ getters }) {
|
async requestAllRecipes({ getters }) {
|
||||||
|
console.log("All Recipes");
|
||||||
const all = getters.getAllRecipes;
|
const all = getters.getAllRecipes;
|
||||||
const payload = await api.recipes.allSummary(all.length, 9999);
|
const payload = await api.recipes.allSummary(all.length, 9999);
|
||||||
const hash = Object.fromEntries([...all, ...payload].map(e => [e.id, e]));
|
const hash = Object.fromEntries([...all, ...payload].map(e => [e.id, e]));
|
||||||
|
|
|
@ -28,8 +28,13 @@ def create_file_token(file_path: Path) -> bool:
|
||||||
|
|
||||||
def authenticate_user(session, email: str, password: str) -> UserInDB:
|
def authenticate_user(session, email: str, password: str) -> UserInDB:
|
||||||
user: UserInDB = db.users.get(session, email, "email", any_case=True)
|
user: UserInDB = db.users.get(session, email, "email", any_case=True)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
user = db.users.get(session, email, "username", any_case=True)
|
||||||
if not user:
|
if not user:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
print(user)
|
||||||
if not verify_password(password, user.password):
|
if not verify_password(password, user.password):
|
||||||
return False
|
return False
|
||||||
return user
|
return user
|
||||||
|
|
|
@ -22,6 +22,11 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
full_name = Column(String, index=True)
|
full_name = Column(String, index=True)
|
||||||
|
username = Column(
|
||||||
|
String,
|
||||||
|
index=True,
|
||||||
|
unique=True,
|
||||||
|
)
|
||||||
email = Column(String, unique=True, index=True)
|
email = Column(String, unique=True, index=True)
|
||||||
password = Column(String)
|
password = Column(String)
|
||||||
group_id = Column(Integer, ForeignKey("groups.id"))
|
group_id = Column(Integer, ForeignKey("groups.id"))
|
||||||
|
@ -32,16 +37,7 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, session, full_name, email, password, group: str = settings.DEFAULT_GROUP, admin=False, **_
|
||||||
session,
|
|
||||||
full_name,
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
group: str = settings.DEFAULT_GROUP,
|
|
||||||
admin=False,
|
|
||||||
id=None,
|
|
||||||
*args,
|
|
||||||
**kwargs
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
group = group or settings.DEFAULT_GROUP
|
group = group or settings.DEFAULT_GROUP
|
||||||
|
@ -51,12 +47,19 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||||
self.admin = admin
|
self.admin = admin
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
def update(self, full_name, email, group, admin, session=None, id=None, password=None, *args, **kwargs):
|
if self.username is None:
|
||||||
|
self.username = full_name
|
||||||
|
|
||||||
|
def update(self, full_name, email, group, admin, username, session=None, id=None, password=None, *args, **kwargs):
|
||||||
|
self.username = username
|
||||||
self.full_name = full_name
|
self.full_name = full_name
|
||||||
self.email = email
|
self.email = email
|
||||||
self.group = Group.get_ref(session, group)
|
self.group = Group.get_ref(session, group)
|
||||||
self.admin = admin
|
self.admin = admin
|
||||||
|
|
||||||
|
if self.username is None:
|
||||||
|
self.username = full_name
|
||||||
|
|
||||||
if password:
|
if password:
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,16 @@ def get_all_meals(
|
||||||
return db.groups.get_meals(session, current_user.group)
|
return db.groups.get_meals(session, current_user.group)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{id}", response_model=MealPlanOut)
|
||||||
|
def get_meal_plan(
|
||||||
|
id,
|
||||||
|
session: Session = Depends(generate_session),
|
||||||
|
):
|
||||||
|
""" Returns a single Meal Plan from the Database """
|
||||||
|
|
||||||
|
return db.meals.get(session, id, "uid")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/create", status_code=status.HTTP_201_CREATED)
|
@router.post("/create", status_code=status.HTTP_201_CREATED)
|
||||||
def create_meal_plan(
|
def create_meal_plan(
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
|
|
|
@ -23,7 +23,7 @@ def get_token(
|
||||||
email = data.username
|
email = data.username
|
||||||
password = data.password
|
password = data.password
|
||||||
|
|
||||||
user = authenticate_user(session, email, password)
|
user: UserInDB = authenticate_user(session, email, password)
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
background_tasks.add_task(
|
background_tasks.add_task(
|
||||||
|
@ -34,7 +34,7 @@ def get_token(
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
|
|
||||||
access_token = security.create_access_token(dict(sub=email))
|
access_token = security.create_access_token(dict(sub=user.email))
|
||||||
return {"access_token": access_token, "token_type": "bearer"}
|
return {"access_token": access_token, "token_type": "bearer"}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ class GroupBase(CamelModel):
|
||||||
|
|
||||||
|
|
||||||
class UserBase(CamelModel):
|
class UserBase(CamelModel):
|
||||||
|
username: Optional[str]
|
||||||
full_name: Optional[str] = None
|
full_name: Optional[str] = None
|
||||||
email: constr(to_lower=True, strip_whitespace=True)
|
email: constr(to_lower=True, strip_whitespace=True)
|
||||||
admin: bool
|
admin: bool
|
||||||
|
@ -59,6 +60,7 @@ class UserBase(CamelModel):
|
||||||
}
|
}
|
||||||
|
|
||||||
schema_extra = {
|
schema_extra = {
|
||||||
|
"username": "ChangeMe",
|
||||||
"fullName": "Change Me",
|
"fullName": "Change Me",
|
||||||
"email": "changeme@email.com",
|
"email": "changeme@email.com",
|
||||||
"group": settings.DEFAULT_GROUP,
|
"group": settings.DEFAULT_GROUP,
|
||||||
|
|
|
@ -10,12 +10,28 @@ from tests.app_routes import AppRoutes
|
||||||
|
|
||||||
@fixture(scope="session")
|
@fixture(scope="session")
|
||||||
def default_user():
|
def default_user():
|
||||||
return UserOut(id=1, fullName="Change Me", email="changeme@email.com", group="Home", admin=True, tokens=[])
|
return UserOut(
|
||||||
|
id=1,
|
||||||
|
fullName="Change Me",
|
||||||
|
username="Change Me",
|
||||||
|
email="changeme@email.com",
|
||||||
|
group="Home",
|
||||||
|
admin=True,
|
||||||
|
tokens=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@fixture(scope="session")
|
@fixture(scope="session")
|
||||||
def new_user():
|
def new_user():
|
||||||
return UserOut(id=3, fullName="My New User", email="newuser@email.com", group="Home", admin=False, tokens=[])
|
return UserOut(
|
||||||
|
id=3,
|
||||||
|
fullName="My New User",
|
||||||
|
username="My New User",
|
||||||
|
email="newuser@email.com",
|
||||||
|
group="Home",
|
||||||
|
admin=False,
|
||||||
|
tokens=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_superuser_login(api_client: TestClient, api_routes: AppRoutes, token):
|
def test_superuser_login(api_client: TestClient, api_routes: AppRoutes, token):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue