1
0
Fork 0
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:
Hayden 2021-05-25 21:01:22 -07:00 committed by GitHub
parent 8f8127a5fc
commit 822663905d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 273 additions and 119 deletions

View file

@ -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),

View file

@ -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>

View file

@ -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

View file

@ -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,

View file

@ -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>

View file

@ -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",

View file

@ -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() {

View 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>

View file

@ -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>

View file

@ -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' : ''">

View file

@ -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/>?",

View file

@ -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") }}

View file

@ -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" },

View file

@ -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">

View file

@ -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">

View file

@ -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);

View file

@ -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">

View file

@ -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>

View file

@ -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);
}, },

View file

@ -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) {

View file

@ -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() {

View file

@ -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>

View file

@ -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: "",

View file

@ -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 = {

View file

@ -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,

View file

@ -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 = [

View file

@ -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]));

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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"}

View file

@ -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,

View file

@ -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):