mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
refactor(frontend): ♻️ split user profile/management (#670)
* refactor(frontend): ♻️ major rewrite/improvement of use-profile pages * refactor(frontend): ♻️ split webhooks into their own page Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
3d87ffc3a5
commit
e179dcdb10
@ -6,3 +6,7 @@
|
|||||||
.layout-leave-active {
|
.layout-leave-active {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.narrow-container {
|
||||||
|
max-width: 700px !important;
|
||||||
|
}
|
||||||
|
@ -102,9 +102,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { utils } from "@/utils";
|
|
||||||
import RecipeCard from "./RecipeCard";
|
import RecipeCard from "./RecipeCard";
|
||||||
import RecipeCardMobile from "./RecipeCardMobile";
|
import RecipeCardMobile from "./RecipeCardMobile";
|
||||||
|
import { useSorter } from "~/composables/use-recipes";
|
||||||
const SORT_EVENT = "sort";
|
const SORT_EVENT = "sort";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -142,6 +142,11 @@ export default {
|
|||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const utils = useSorter();
|
||||||
|
|
||||||
|
return { utils };
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
sortLoading: false,
|
sortLoading: false,
|
||||||
@ -197,7 +202,7 @@ export default {
|
|||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
navigateRandom() {
|
navigateRandom() {
|
||||||
const recipe = utils.recipe.randomRecipe(this.recipes);
|
const recipe = this.utils.recipe.randomRecipe(this.recipes);
|
||||||
this.$router.push(`/recipe/${recipe.slug}`);
|
this.$router.push(`/recipe/${recipe.slug}`);
|
||||||
},
|
},
|
||||||
sortRecipes(sortType) {
|
sortRecipes(sortType) {
|
||||||
@ -205,19 +210,19 @@ export default {
|
|||||||
const sortTarget = [...this.recipes];
|
const sortTarget = [...this.recipes];
|
||||||
switch (sortType) {
|
switch (sortType) {
|
||||||
case this.EVENTS.az:
|
case this.EVENTS.az:
|
||||||
utils.recipe.sortAToZ(sortTarget);
|
this.utils.sortAToZ(sortTarget);
|
||||||
break;
|
break;
|
||||||
case this.EVENTS.rating:
|
case this.EVENTS.rating:
|
||||||
utils.recipe.sortByRating(sortTarget);
|
this.utils.sortByRating(sortTarget);
|
||||||
break;
|
break;
|
||||||
case this.EVENTS.created:
|
case this.EVENTS.created:
|
||||||
utils.recipe.sortByCreated(sortTarget);
|
this.utils.sortByCreated(sortTarget);
|
||||||
break;
|
break;
|
||||||
case this.EVENTS.updated:
|
case this.EVENTS.updated:
|
||||||
utils.recipe.sortByUpdated(sortTarget);
|
this.utils.sortByUpdated(sortTarget);
|
||||||
break;
|
break;
|
||||||
case this.EVENTS.shuffle:
|
case this.EVENTS.shuffle:
|
||||||
utils.recipe.shuffle(sortTarget);
|
this.utils.shuffle(sortTarget);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log("Unknown Event", sortType);
|
console.log("Unknown Event", sortType);
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
>
|
>
|
||||||
<template #selection="data">
|
<template #selection="data">
|
||||||
<v-chip
|
<v-chip
|
||||||
|
v-if="showSelected"
|
||||||
:key="data.index"
|
:key="data.index"
|
||||||
class="ma-1"
|
class="ma-1"
|
||||||
:input-value="data.selected"
|
:input-value="data.selected"
|
||||||
@ -78,6 +79,10 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
showSelected: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
|
@ -41,7 +41,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { utils } from "@/utils";
|
|
||||||
import { defineComponent, ref } from "@nuxtjs/composition-api";
|
import { defineComponent, ref } from "@nuxtjs/composition-api";
|
||||||
import { useApiSingleton } from "~/composables/use-api";
|
import { useApiSingleton } from "~/composables/use-api";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@ -203,7 +202,6 @@ export default defineComponent({
|
|||||||
navigator.clipboard.writeText(copyText).then(
|
navigator.clipboard.writeText(copyText).then(
|
||||||
() => {
|
() => {
|
||||||
console.log("Copied to Clipboard", copyText);
|
console.log("Copied to Clipboard", copyText);
|
||||||
utils.notify.success("Copied to Clipboard");
|
|
||||||
},
|
},
|
||||||
() => console.log("Copied Failed", copyText)
|
() => console.log("Copied Failed", copyText)
|
||||||
);
|
);
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div v-if="value && value.length > 0">
|
<div v-if="value && value.length > 0">
|
||||||
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
|
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
|
||||||
<div>
|
<div>
|
||||||
<div v-for="(ingredient, index) in value" :key="generateKey('ingredient', index)">
|
<div v-for="(ingredient, index) in value" :key="'ingredient' + index">
|
||||||
<h3 v-if="showTitleEditor[index]" class="mt-2">{{ ingredient.title }}</h3>
|
<h3 v-if="showTitleEditor[index]" class="mt-2">{{ ingredient.title }}</h3>
|
||||||
<v-divider v-if="showTitleEditor[index]"></v-divider>
|
<v-divider v-if="showTitleEditor[index]"></v-divider>
|
||||||
<v-list-item dense @click="toggleChecked(index)">
|
<v-list-item dense @click="toggleChecked(index)">
|
||||||
@ -20,7 +20,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import VueMarkdown from "@adapttive/vue-markdown";
|
import VueMarkdown from "@adapttive/vue-markdown";
|
||||||
import { useFraction } from "@/composables/use-fraction";
|
import { useFraction } from "@/composables/use-fraction";
|
||||||
import { utils } from "@/utils";
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
VueMarkdown,
|
VueMarkdown,
|
||||||
@ -87,10 +86,6 @@ export default {
|
|||||||
this.showTitleEditor = this.value.map((x) => this.validateTitle(x.title));
|
this.showTitleEditor = this.value.map((x) => this.validateTitle(x.title));
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
generateKey(item, index) {
|
|
||||||
return utils.generateUniqueKey(item, index);
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleChecked(index) {
|
toggleChecked(index) {
|
||||||
this.$set(this.checked, index, !this.checked[index]);
|
this.$set(this.checked, index, !this.checked[index]);
|
||||||
},
|
},
|
||||||
|
@ -88,7 +88,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import draggable from "vuedraggable";
|
import draggable from "vuedraggable";
|
||||||
import VueMarkdown from "@adapttive/vue-markdown";
|
import VueMarkdown from "@adapttive/vue-markdown";
|
||||||
import { utils } from "@/utils";
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
VueMarkdown,
|
VueMarkdown,
|
||||||
@ -126,9 +125,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
generateKey(item, index) {
|
|
||||||
return utils.generateUniqueKey(item, index);
|
|
||||||
},
|
|
||||||
removeByIndex(list, index) {
|
removeByIndex(list, index) {
|
||||||
list.splice(index, 1);
|
list.splice(index, 1);
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="value.length > 0 || edit">
|
<div v-if="value.length > 0 || edit">
|
||||||
<h2 class="my-4">{{ $t("recipe.note") }}</h2>
|
<h2 class="my-4">{{ $t("recipe.note") }}</h2>
|
||||||
<v-card v-for="(note, index) in value" :key="generateKey('note', index)" class="mt-1">
|
<v-card v-for="(note, index) in value" :key="'note' + index" class="mt-1">
|
||||||
<div v-if="edit">
|
<div v-if="edit">
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-row align="center">
|
<v-row align="center">
|
||||||
@ -35,7 +35,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import VueMarkdown from "@adapttive/vue-markdown";
|
import VueMarkdown from "@adapttive/vue-markdown";
|
||||||
import { utils } from "@/utils";
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
VueMarkdown,
|
VueMarkdown,
|
||||||
@ -52,9 +51,6 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
generateKey(item, index) {
|
|
||||||
return utils.generateUniqueKey(item, index);
|
|
||||||
},
|
|
||||||
addNote() {
|
addNote() {
|
||||||
this.value.push({ title: "", text: "" });
|
this.value.push({ title: "", text: "" });
|
||||||
},
|
},
|
||||||
|
@ -1,159 +0,0 @@
|
|||||||
<template>
|
|
||||||
<BaseStatCard :icon="$globals.icons.api" color="accent">
|
|
||||||
<template #after-heading>
|
|
||||||
<div class="ml-auto text-right">
|
|
||||||
<h2 class="body-3 grey--text font-weight-light">
|
|
||||||
{{ $t("settings.token.api-tokens") }}
|
|
||||||
</h2>
|
|
||||||
<h3 class="display-2 font-weight-light text--primary">
|
|
||||||
<small> {{ tokens.length }} </small>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #bottom>
|
|
||||||
<v-subheader class="mb-n2">{{ $t("settings.token.active-tokens") }}</v-subheader>
|
|
||||||
<v-virtual-scroll height="210" item-height="70" :items="tokens" class="mt-2">
|
|
||||||
<template #default="{ item }">
|
|
||||||
<v-divider></v-divider>
|
|
||||||
<v-list-item @click.prevent>
|
|
||||||
<v-list-item-avatar>
|
|
||||||
<v-icon large dark color="accent">
|
|
||||||
{{ $globals.icons.api }}
|
|
||||||
</v-icon>
|
|
||||||
</v-list-item-avatar>
|
|
||||||
|
|
||||||
<v-list-item-content>
|
|
||||||
<v-list-item-title v-text="item.name"></v-list-item-title>
|
|
||||||
</v-list-item-content>
|
|
||||||
|
|
||||||
<v-list-item-action class="ml-auto">
|
|
||||||
<v-btn large icon @click.stop="deleteToken(item.id)">
|
|
||||||
<v-icon color="accent">{{ $globals.icons.delete }}</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-list-item-action>
|
|
||||||
</v-list-item>
|
|
||||||
<v-divider></v-divider>
|
|
||||||
</template>
|
|
||||||
</v-virtual-scroll>
|
|
||||||
|
|
||||||
<v-divider></v-divider>
|
|
||||||
<v-card-actions class="pb-1 pt-3">
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<BaseDialog
|
|
||||||
:title="$t('settings.token.create-an-api-token')"
|
|
||||||
:title-icon="$globals.icons.api"
|
|
||||||
:submit-text="buttonText"
|
|
||||||
:loading="loading"
|
|
||||||
@submit="createToken(name)"
|
|
||||||
>
|
|
||||||
<v-card-text>
|
|
||||||
<v-form ref="domNewTokenForm" @submit.prevent>
|
|
||||||
<v-text-field v-model="name" :label="$t('settings.token.token-name')" required> </v-text-field>
|
|
||||||
</v-form>
|
|
||||||
|
|
||||||
<div v-if="createdToken != ''">
|
|
||||||
<v-textarea
|
|
||||||
v-model="createdToken"
|
|
||||||
class="mb-0 pb-0"
|
|
||||||
:label="$t('settings.token.api-token')"
|
|
||||||
readonly
|
|
||||||
:append-outer-icon="$globals.icons.contentCopy"
|
|
||||||
@click="copyToken"
|
|
||||||
@click:append-outer="copyToken"
|
|
||||||
>
|
|
||||||
</v-textarea>
|
|
||||||
<v-subheader class="text-center">
|
|
||||||
{{
|
|
||||||
$t(
|
|
||||||
"settings.token.copy-this-token-for-use-with-an-external-application-this-token-will-not-be-viewable-again"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</v-subheader>
|
|
||||||
</div>
|
|
||||||
</v-card-text>
|
|
||||||
|
|
||||||
<template #activator="{ open }">
|
|
||||||
<BaseButton create @click="open" />
|
|
||||||
</template>
|
|
||||||
</BaseDialog>
|
|
||||||
</v-card-actions>
|
|
||||||
</template>
|
|
||||||
</BaseStatCard>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, ref } from "@nuxtjs/composition-api";
|
|
||||||
import { useApiSingleton } from "~/composables/use-api";
|
|
||||||
|
|
||||||
const REFRESH_EVENT = "refresh";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
tokens: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(_, context) {
|
|
||||||
const api = useApiSingleton();
|
|
||||||
|
|
||||||
const domNewTokenForm = ref<VForm | null>(null);
|
|
||||||
|
|
||||||
const createdToken = ref("");
|
|
||||||
const name = ref("");
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
function resetCreate() {
|
|
||||||
createdToken.value = "";
|
|
||||||
loading.value = false;
|
|
||||||
name.value = "";
|
|
||||||
context.emit(REFRESH_EVENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createToken(name: string) {
|
|
||||||
if (loading.value) {
|
|
||||||
resetCreate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
|
|
||||||
if (domNewTokenForm?.value?.validate()) {
|
|
||||||
console.log("Created");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await api.users.createAPIToken({ name });
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
createdToken.value = data.token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteToken(id: string | number) {
|
|
||||||
const { data } = await api.users.deleteAPIToken(id);
|
|
||||||
context.emit(REFRESH_EVENT);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyToken() {
|
|
||||||
navigator.clipboard.writeText(createdToken.value).then(
|
|
||||||
() => console.log("Copied", createdToken.value),
|
|
||||||
() => console.log("Copied Failed", createdToken.value)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { createToken, deleteToken, copyToken, createdToken, loading, name };
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
buttonText(): any {
|
|
||||||
if (this.createdToken === "") {
|
|
||||||
return this.$t("general.create");
|
|
||||||
} else {
|
|
||||||
return this.$t("general.close");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,177 +0,0 @@
|
|||||||
<template>
|
|
||||||
<BaseStatCard :icon="$globals.icons.user" color="accent">
|
|
||||||
<template #after-heading>
|
|
||||||
<div class="ml-auto text-right">
|
|
||||||
<div class="body-3 grey--text font-weight-light" v-text="$t('user.user-id-with-value', { id: user.id })" />
|
|
||||||
|
|
||||||
<h3 class="display-2 font-weight-light text--primary">
|
|
||||||
<small> {{ $t("group.group-with-value", { groupID: user.group }) }}</small>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Change Password -->
|
|
||||||
<template #actions>
|
|
||||||
<BaseDialog
|
|
||||||
:title="$t('user.reset-password')"
|
|
||||||
:title-icon="$globals.icons.lock"
|
|
||||||
:submit-text="$t('settings.change-password')"
|
|
||||||
:loading="loading"
|
|
||||||
:top="true"
|
|
||||||
@submit="updatePassword"
|
|
||||||
>
|
|
||||||
<template #activator="{ open }">
|
|
||||||
<v-btn color="info" class="mr-1" small @click="open">
|
|
||||||
<v-icon left>{{ $globals.icons.lock }}</v-icon>
|
|
||||||
{{ $t("settings.change-password") }}
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<v-card-text>
|
|
||||||
<v-form ref="passChange">
|
|
||||||
<v-text-field
|
|
||||||
v-model="password.current"
|
|
||||||
:prepend-icon="$globals.icons.lock"
|
|
||||||
:label="$t('user.current-password')"
|
|
||||||
validate-on-blur
|
|
||||||
:type="showPassword ? 'text' : 'password'"
|
|
||||||
@click:append="showPassword.current = !showPassword.current"
|
|
||||||
></v-text-field>
|
|
||||||
<v-text-field
|
|
||||||
v-model="password.newOne"
|
|
||||||
:prepend-icon="$globals.icons.lock"
|
|
||||||
:label="$t('user.new-password')"
|
|
||||||
:type="showPassword ? 'text' : 'password'"
|
|
||||||
@click:append="showPassword.newOne = !showPassword.newOne"
|
|
||||||
></v-text-field>
|
|
||||||
<v-text-field
|
|
||||||
v-model="password.newTwo"
|
|
||||||
:prepend-icon="$globals.icons.lock"
|
|
||||||
:label="$t('user.confirm-password')"
|
|
||||||
:rules="[password.newOne === password.newTwo || $t('user.password-must-match')]"
|
|
||||||
validate-on-blur
|
|
||||||
:type="showPassword ? 'text' : 'password'"
|
|
||||||
@click:append="showPassword.newTwo = !showPassword.newTwo"
|
|
||||||
></v-text-field>
|
|
||||||
</v-form>
|
|
||||||
</v-card-text>
|
|
||||||
</BaseDialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Update User -->
|
|
||||||
<template #bottom>
|
|
||||||
<v-card-text>
|
|
||||||
<v-form ref="userUpdate">
|
|
||||||
<v-text-field v-model="userCopy.username" :label="$t('user.username')" required validate-on-blur>
|
|
||||||
</v-text-field>
|
|
||||||
<v-text-field v-model="userCopy.fullName" :label="$t('user.full-name')" required validate-on-blur>
|
|
||||||
</v-text-field>
|
|
||||||
<v-text-field v-model="userCopy.email" :label="$t('user.email')" validate-on-blur required> </v-text-field>
|
|
||||||
</v-form>
|
|
||||||
</v-card-text>
|
|
||||||
<v-divider></v-divider>
|
|
||||||
<v-card-actions class="pb-1 pt-3">
|
|
||||||
<AppButtonUpload :icon="$globals.icons.fileImage" :text="$t('user.upload-photo')" file-name="profile_image" />
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<BaseButton update @click="updateUser" />
|
|
||||||
</v-card-actions>
|
|
||||||
</template>
|
|
||||||
</BaseStatCard>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { ref, reactive, defineComponent } from "@nuxtjs/composition-api";
|
|
||||||
import { useApiSingleton } from "~/composables/use-api";
|
|
||||||
const events = {
|
|
||||||
UPDATE_USER: "update",
|
|
||||||
CHANGE_PASSWORD: "change-password",
|
|
||||||
UPLOAD_PHOTO: "upload-photo",
|
|
||||||
REFRESH: "refresh",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
user: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props, context) {
|
|
||||||
const userCopy = ref({ ...props.user });
|
|
||||||
const api = useApiSingleton();
|
|
||||||
|
|
||||||
const domUpdatePassword = ref<VForm | null>(null);
|
|
||||||
const password = reactive({
|
|
||||||
current: "",
|
|
||||||
newOne: "",
|
|
||||||
newTwo: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
async function updateUser() {
|
|
||||||
// @ts-ignore
|
|
||||||
const { response } = await api.users.updateOne(userCopy.value.id, userCopy.value);
|
|
||||||
if (response?.status === 200) {
|
|
||||||
context.emit(events.REFRESH);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updatePassword() {
|
|
||||||
const { response } = await api.users.changePassword(userCopy.value.id, {
|
|
||||||
currentPassword: password.current,
|
|
||||||
newPassword: password.newOne,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response?.status === 200) {
|
|
||||||
console.log("Password Changed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { updateUser, updatePassword, userCopy, password, domUpdatePassword };
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hideImage: false,
|
|
||||||
passwordLoading: false,
|
|
||||||
showPassword: false,
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
// async updateUser() {
|
|
||||||
// if (!this.$refs.userUpdate.validate()) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// this.loading = true;
|
|
||||||
// const response = await api.users.update(this.user);
|
|
||||||
// if (response) {
|
|
||||||
// this.$store.commit("setToken", response.data.access_token);
|
|
||||||
// this.refreshProfile();
|
|
||||||
// this.loading = false;
|
|
||||||
// this.$store.dispatch("requestUserData");
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
async changePassword() {
|
|
||||||
// @ts-ignore
|
|
||||||
this.paswordLoading = true;
|
|
||||||
const data = {
|
|
||||||
currentPassword: this.password.current,
|
|
||||||
newPassword: this.password.newOne,
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
if (this.$refs.passChange.validate()) {
|
|
||||||
// @ts-ignore
|
|
||||||
if (await api.users.changePassword(this.user.id, data)) {
|
|
||||||
this.$emit("refresh");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
this.paswordLoading = false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
56
frontend/components/Domain/User/UserProfileLinkCard.vue
Normal file
56
frontend/components/Domain/User/UserProfileLinkCard.vue
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<v-card outlined nuxt :to="link.to" height="100%" class="d-flex flex-column">
|
||||||
|
<div v-if="$vuetify.breakpoint.smAndDown" class="pa-2 mx-auto">
|
||||||
|
<v-img max-width="150px" max-height="125" :src="image"></v-img>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center justify-space-between">
|
||||||
|
<div>
|
||||||
|
<v-card-title class="headline pb-0">
|
||||||
|
<slot name="title"> </slot>
|
||||||
|
</v-card-title>
|
||||||
|
<div class="d-flex justify-center align-center">
|
||||||
|
<v-card-text class="d-flex flex-row mb-auto">
|
||||||
|
<slot name="default"></slot>
|
||||||
|
</v-card-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="$vuetify.breakpoint.mdAndUp" class="px-10">
|
||||||
|
<v-img max-width="150px" max-height="125" :src="image"></v-img>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<v-divider class="mt-auto"></v-divider>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-btn text color="info" :to="link.to">
|
||||||
|
{{ link.text }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "@nuxtjs/composition-api";
|
||||||
|
|
||||||
|
interface LinkProp {
|
||||||
|
text: string;
|
||||||
|
url?: string;
|
||||||
|
to: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
link: {
|
||||||
|
type: Object as () => LinkProp,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
type: String,
|
||||||
|
requried: false,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -131,7 +131,7 @@ export default {
|
|||||||
},
|
},
|
||||||
cancel: {
|
cancel: {
|
||||||
text: "Cancel",
|
text: "Cancel",
|
||||||
icon: this.$globals.icons.cancel,
|
icon: this.$globals.icons.close,
|
||||||
color: "grey",
|
color: "grey",
|
||||||
},
|
},
|
||||||
download: {
|
download: {
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-card flat class="pb-2">
|
<v-card flat class="pb-2">
|
||||||
<h2 class="headline">{{ title }}</h2>
|
<h2 class="headline">{{ title }}</h2>
|
||||||
<BaseDivider width="200px" color="primary" class="my-2" thickness="1px" />
|
<!-- <BaseDivider width="200px" color="primary" class="my-2" thickness="1px" /> -->
|
||||||
<p class="pb-0 mb-0">
|
<p class="pb-0 mb-0">
|
||||||
<slot />
|
<slot />
|
||||||
</p>
|
</p>
|
||||||
|
<v-divider class="my-4"></v-divider>
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
26
frontend/components/global/BasePageTitle.vue
Normal file
26
frontend/components/global/BasePageTitle.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mt-4">
|
||||||
|
<section class="d-flex flex-column align-center">
|
||||||
|
<slot name="header"></slot>
|
||||||
|
<h2 class="headline">
|
||||||
|
<slot name="title"> 👋 Here's a Title </slot>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<h3 class="subtitle-1">
|
||||||
|
<slot> </slot>
|
||||||
|
</h3>
|
||||||
|
</section>
|
||||||
|
<v-divider v-if="divider" class="my-4"></v-divider>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
divider: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
29
frontend/components/global/ToggleState.vue
Normal file
29
frontend/components/global/ToggleState.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<component :is="tag">
|
||||||
|
<slot name="activator" v-bind="{ toggle, state }"> </slot>
|
||||||
|
<slot v-bind="{ state, toggle }"></slot>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "@nuxtjs/composition-api";
|
||||||
|
import { useToggle } from "@vueuse/shared";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
tag: {
|
||||||
|
type: String,
|
||||||
|
default: "div",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const [state, toggle] = useToggle();
|
||||||
|
console.log(state, toggle);
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
toggle,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -7,6 +7,56 @@ import { Recipe } from "~/types/api-types/recipe";
|
|||||||
export const allRecipes = ref<Recipe[] | null>([]);
|
export const allRecipes = ref<Recipe[] | null>([]);
|
||||||
export const recentRecipes = ref<Recipe[] | null>([]);
|
export const recentRecipes = ref<Recipe[] | null>([]);
|
||||||
|
|
||||||
|
const rand = (n: number) => Math.floor(Math.random() * n);
|
||||||
|
|
||||||
|
function swap(t: Array<any>, i: number, j: number) {
|
||||||
|
const q = t[i];
|
||||||
|
t[i] = t[j];
|
||||||
|
t[j] = q;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSorter = () => {
|
||||||
|
function sortAToZ(list: Array<Recipe>) {
|
||||||
|
list.sort((a, b) => {
|
||||||
|
const textA = a.name.toUpperCase();
|
||||||
|
const textB = b.name.toUpperCase();
|
||||||
|
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function sortByCreated(list: Array<Recipe>) {
|
||||||
|
list.sort((a, b) => (a.dateAdded > b.dateAdded ? -1 : 1));
|
||||||
|
}
|
||||||
|
function sortByUpdated(list: Array<Recipe>) {
|
||||||
|
list.sort((a, b) => (a.dateUpdated > b.dateUpdated ? -1 : 1));
|
||||||
|
}
|
||||||
|
function sortByRating(list: Array<Recipe>) {
|
||||||
|
list.sort((a, b) => (a.rating > b.rating ? -1 : 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomRecipe(list: Array<Recipe>): Recipe {
|
||||||
|
return list[Math.floor(Math.random() * list.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function shuffle(list: Array<Recipe>) {
|
||||||
|
let last = list.length;
|
||||||
|
let n;
|
||||||
|
while (last > 0) {
|
||||||
|
n = rand(last);
|
||||||
|
swap(list, n, --last);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sortAToZ,
|
||||||
|
sortByCreated,
|
||||||
|
sortByUpdated,
|
||||||
|
sortByRating,
|
||||||
|
randomRecipe,
|
||||||
|
shuffle,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const useRecipes = (all = false, fetchRecipes = true) => {
|
export const useRecipes = (all = false, fetchRecipes = true) => {
|
||||||
const api = useApiSingleton();
|
const api = useApiSingleton();
|
||||||
|
|
||||||
|
@ -46,28 +46,6 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
sidebar: null,
|
sidebar: null,
|
||||||
topLinks: [
|
topLinks: [
|
||||||
{
|
|
||||||
icon: this.$globals.icons.user,
|
|
||||||
to: "/user/profile",
|
|
||||||
title: this.$t("sidebar.profile"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: this.$globals.icons.group,
|
|
||||||
to: "/user/group",
|
|
||||||
title: this.$t("group.group"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: this.$globals.icons.pages,
|
|
||||||
to: "/user/group/cookbooks",
|
|
||||||
title: this.$t("sidebar.cookbooks"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: this.$globals.icons.webhook,
|
|
||||||
to: "/user/group/webhooks",
|
|
||||||
title: "Webhooks",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
adminLinks: [
|
|
||||||
{
|
{
|
||||||
icon: this.$globals.icons.viewDashboard,
|
icon: this.$globals.icons.viewDashboard,
|
||||||
to: "/admin/dashboard",
|
to: "/admin/dashboard",
|
||||||
@ -157,9 +135,6 @@ export default defineComponent({
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
head: {
|
|
||||||
title: "Admin",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
:top-link="topLinks"
|
:top-link="topLinks"
|
||||||
secondary-header="Cookbooks"
|
secondary-header="Cookbooks"
|
||||||
:secondary-links="cookbookLinks || []"
|
:secondary-links="cookbookLinks || []"
|
||||||
:bottom-links="bottomLink"
|
:bottom-links="$auth.user.admin ? bottomLink : []"
|
||||||
@input="sidebar = !sidebar"
|
@input="sidebar = !sidebar"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ export default defineComponent({
|
|||||||
{
|
{
|
||||||
icon: this.$globals.icons.cog,
|
icon: this.$globals.icons.cog,
|
||||||
title: this.$t("general.settings"),
|
title: this.$t("general.settings"),
|
||||||
to: "/user/profile",
|
to: "/admin/dashboard",
|
||||||
restricted: true,
|
restricted: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -30,7 +30,7 @@ export default {
|
|||||||
css: [{ src: "~/assets/main.css" }, { src: "~/assets/style-overrides.scss" }],
|
css: [{ src: "~/assets/main.css" }, { src: "~/assets/style-overrides.scss" }],
|
||||||
|
|
||||||
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
|
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
|
||||||
plugins: ["~/plugins/globals.js"],
|
plugins: ["~/plugins/globals.ts"],
|
||||||
|
|
||||||
// Auto import components: https://go.nuxtjs.dev/config-components
|
// Auto import components: https://go.nuxtjs.dev/config-components
|
||||||
components: true,
|
components: true,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<RecipeCardSection
|
<RecipeCardSection
|
||||||
|
v-if="category"
|
||||||
:icon="$globals.icons.tags"
|
:icon="$globals.icons.tags"
|
||||||
:title="category.name"
|
:title="category.name"
|
||||||
:recipes="category.recipes"
|
:recipes="category.recipes"
|
||||||
|
@ -7,9 +7,10 @@
|
|||||||
<v-toolbar-title class="headline"> {{ $t("recipe.categories") }} </v-toolbar-title>
|
<v-toolbar-title class="headline"> {{ $t("recipe.categories") }} </v-toolbar-title>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
<v-slide-x-transition hide-on-leave>
|
<section v-for="(items, key, idx) in categoriesByLetter" :key="'header' + idx" :class="idx === 1 ? null : 'my-4'">
|
||||||
|
<BaseCardSectionTitle :title="key"> </BaseCardSectionTitle>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col v-for="item in categories" :key="item.id" cols="12" :sm="12" :md="6" :lg="4" :xl="3">
|
<v-col v-for="(item, index) in items" :key="'cat' + index" cols="12" :sm="12" :md="6" :lg="4" :xl="3">
|
||||||
<v-card hover :to="`/recipes/categories/${item.slug}`">
|
<v-card hover :to="`/recipes/categories/${item.slug}`">
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-icon>
|
<v-icon>
|
||||||
@ -21,12 +22,12 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-slide-x-transition>
|
</section>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, useAsync } from "@nuxtjs/composition-api";
|
import { computed, defineComponent, useAsync } from "@nuxtjs/composition-api";
|
||||||
import { useApiSingleton } from "~/composables/use-api";
|
import { useApiSingleton } from "~/composables/use-api";
|
||||||
import { useAsyncKey } from "~/composables/use-utils";
|
import { useAsyncKey } from "~/composables/use-utils";
|
||||||
|
|
||||||
@ -38,7 +39,25 @@ export default defineComponent({
|
|||||||
const { data } = await api.categories.getAll();
|
const { data } = await api.categories.getAll();
|
||||||
return data;
|
return data;
|
||||||
}, useAsyncKey());
|
}, useAsyncKey());
|
||||||
return { categories, api };
|
|
||||||
|
const categoriesByLetter: any = computed(() => {
|
||||||
|
const catsByLetter: { [key: string]: Array<any> } = {};
|
||||||
|
|
||||||
|
if (!categories.value) return catsByLetter;
|
||||||
|
|
||||||
|
categories.value.forEach((item) => {
|
||||||
|
const letter = item.name[0].toUpperCase();
|
||||||
|
if (!catsByLetter[letter]) {
|
||||||
|
catsByLetter[letter] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
catsByLetter[letter].push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return catsByLetter;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { categories, api, categoriesByLetter };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -7,9 +7,10 @@
|
|||||||
<v-toolbar-title class="headline"> {{ $t("tag.tags") }} </v-toolbar-title>
|
<v-toolbar-title class="headline"> {{ $t("tag.tags") }} </v-toolbar-title>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
<v-slide-x-transition hide-on-leave>
|
<section v-for="(items, key, idx) in tagsByLetter" :key="'header' + idx" :class="idx === 1 ? null : 'my-4'">
|
||||||
|
<BaseCardSectionTitle :title="key"> </BaseCardSectionTitle>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col v-for="item in tags" :key="item.id" cols="12" :sm="12" :md="6" :lg="4" :xl="3">
|
<v-col v-for="(item, index) in items" :key="'cat' + index" cols="12" :sm="12" :md="6" :lg="4" :xl="3">
|
||||||
<v-card hover :to="`/recipes/tags/${item.slug}`">
|
<v-card hover :to="`/recipes/tags/${item.slug}`">
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-icon>
|
<v-icon>
|
||||||
@ -21,12 +22,12 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-slide-x-transition>
|
</section>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, useAsync } from "@nuxtjs/composition-api";
|
import { defineComponent, useAsync, computed } from "@nuxtjs/composition-api";
|
||||||
import { useApiSingleton } from "~/composables/use-api";
|
import { useApiSingleton } from "~/composables/use-api";
|
||||||
import { useAsyncKey } from "~/composables/use-utils";
|
import { useAsyncKey } from "~/composables/use-utils";
|
||||||
|
|
||||||
@ -38,7 +39,25 @@ export default defineComponent({
|
|||||||
const { data } = await api.tags.getAll();
|
const { data } = await api.tags.getAll();
|
||||||
return data;
|
return data;
|
||||||
}, useAsyncKey());
|
}, useAsyncKey());
|
||||||
return { tags, api };
|
|
||||||
|
const tagsByLetter: any = computed(() => {
|
||||||
|
const tagsByLetter: { [key: string]: Array<any> } = {};
|
||||||
|
|
||||||
|
if (!tags.value) return tagsByLetter;
|
||||||
|
|
||||||
|
tags.value.forEach((item) => {
|
||||||
|
const letter = item.name[0].toUpperCase();
|
||||||
|
if (!tagsByLetter[letter]) {
|
||||||
|
tagsByLetter[letter] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
tagsByLetter[letter].push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return tagsByLetter;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { tags, api, tagsByLetter };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -16,31 +16,47 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<v-row dense class="my-0 flex-row align-center justify-space-around">
|
<ToggleState>
|
||||||
<v-col>
|
<template #activator="{ state, toggle }">
|
||||||
<h3 class="pl-2 text-center headline">
|
<v-switch :value="state" color="info" class="ma-0 pa-0" label="Advanced" @input="toggle" @click="toggle">
|
||||||
{{ $t("category.category-filter") }}
|
Advanced
|
||||||
</h3>
|
</v-switch>
|
||||||
<RecipeSearchFilterSelector class="mb-1" @update="updateCatParams" />
|
</template>
|
||||||
<RecipeCategoryTagSelector v-model="includeCategories" :solo="true" :dense="false" :return-object="false" />
|
<template #default="{ state }">
|
||||||
</v-col>
|
<v-expand-transition>
|
||||||
<v-col>
|
<v-row v-show="state" dense class="my-0 dense flex-row align-center justify-space-around">
|
||||||
<h3 class="pl-2 text-center headline">
|
<v-col>
|
||||||
{{ $t("search.tag-filter") }}
|
<h3 class="pl-2 text-center headline">
|
||||||
</h3>
|
{{ $t("category.category-filter") }}
|
||||||
<RecipeSearchFilterSelector class="mb-1" @update="updateTagParams" />
|
</h3>
|
||||||
<RecipeCategoryTagSelector
|
<RecipeSearchFilterSelector class="mb-1" @update="updateCatParams" />
|
||||||
v-model="includeTags"
|
<RecipeCategoryTagSelector
|
||||||
:solo="true"
|
v-model="includeCategories"
|
||||||
:dense="false"
|
:solo="true"
|
||||||
:return-object="false"
|
:dense="false"
|
||||||
:tag-selector="true"
|
:return-object="false"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
<v-col>
|
||||||
|
<h3 class="pl-2 text-center headline">
|
||||||
|
{{ $t("search.tag-filter") }}
|
||||||
|
</h3>
|
||||||
|
<RecipeSearchFilterSelector class="mb-1" @update="updateTagParams" />
|
||||||
|
<RecipeCategoryTagSelector
|
||||||
|
v-model="includeTags"
|
||||||
|
:solo="true"
|
||||||
|
:dense="false"
|
||||||
|
:return-object="false"
|
||||||
|
:tag-selector="true"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-expand-transition>
|
||||||
|
</template>
|
||||||
|
</ToggleState>
|
||||||
|
|
||||||
<RecipeCardSection
|
<RecipeCardSection
|
||||||
class="mt-n9"
|
class="mt-n5"
|
||||||
:title-icon="$globals.icons.magnify"
|
:title-icon="$globals.icons.magnify"
|
||||||
:recipes="showRecipes"
|
:recipes="showRecipes"
|
||||||
:hard-limit="maxResults"
|
:hard-limit="maxResults"
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container fluid>
|
<v-container class="narrow-container">
|
||||||
<BaseCardSectionTitle title="Cookbooks"> </BaseCardSectionTitle>
|
<BasePageTitle divider>
|
||||||
|
<template #header>
|
||||||
|
<v-img max-height="100" max-width="100" :src="require('~/static/svgs/manage-cookbooks.svg')"></v-img>
|
||||||
|
</template>
|
||||||
|
<template #title> Cookbooks </template>
|
||||||
|
Arrange and edit your cookbooks here.
|
||||||
|
</BasePageTitle>
|
||||||
|
|
||||||
<BaseButton create @click="actions.createOne()" />
|
<BaseButton create @click="actions.createOne()" />
|
||||||
<v-expansion-panels class="mt-2">
|
<v-expansion-panels class="mt-2">
|
||||||
<draggable v-model="cookbooks" handle=".handle" style="width: 100%" @change="actions.updateOrder()">
|
<draggable v-model="cookbooks" handle=".handle" style="width: 100%" @change="actions.updateOrder()">
|
||||||
@ -48,7 +55,6 @@ import draggable from "vuedraggable";
|
|||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { draggable },
|
components: { draggable },
|
||||||
layout: "admin",
|
|
||||||
setup() {
|
setup() {
|
||||||
const { cookbooks, actions } = useCookbooks();
|
const { cookbooks, actions } = useCookbooks();
|
||||||
|
|
||||||
@ -62,6 +68,6 @@ export default defineComponent({
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.my-border {
|
.my-border {
|
||||||
border-left: 5px solid var(--v-primary-base);
|
border-left: 5px solid var(--v-primary-base) !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -1,17 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container fluid>
|
<v-container>
|
||||||
<section>
|
<BasePageTitle divider>
|
||||||
<BaseCardSectionTitle title="Group Settings">
|
<template #header>
|
||||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Alias error provident, eveniet, laboriosam assumenda
|
<v-img max-height="100" max-width="100" :src="require('~/static/svgs/manage-group-settings.svg')"></v-img>
|
||||||
earum amet quaerat vel consequatur molestias sed enim. Adipisci a consequuntur dolor culpa expedita voluptatem
|
</template>
|
||||||
praesentium optio iste atque, ea reiciendis iure non aut suscipit modi ducimus ratione, quam numquam quaerat
|
<template #title> Group Settings </template>
|
||||||
distinctio illum nemo. Dicta, doloremque!
|
These items are shared within your group. Editing one of them will change it for the whole group!
|
||||||
</BaseCardSectionTitle>
|
</BasePageTitle>
|
||||||
<div v-if="categories" class="d-flex">
|
<v-card tag="section" outlined>
|
||||||
<DomainRecipeCategoryTagSelector v-model="categories" class="mt-5 mr-5" />
|
<v-card-text>
|
||||||
<BaseButton save class="mt-auto mb-3" @click="actions.updateAll()" />
|
<BaseCardSectionTitle title="Mealplan Categories">
|
||||||
</div>
|
Set the categories below for the ones that you want to be included in your mealplan random generation.
|
||||||
</section>
|
<div class="mt-2">
|
||||||
|
<BaseButton save @click="actions.updateAll()" />
|
||||||
|
</div>
|
||||||
|
</BaseCardSectionTitle>
|
||||||
|
<DomainRecipeCategoryTagSelector v-if="categories" v-model="categories" />
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -20,7 +26,6 @@ import { defineComponent } from "@nuxtjs/composition-api";
|
|||||||
import { useGroup } from "~/composables/use-groups";
|
import { useGroup } from "~/composables/use-groups";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
layout: "admin",
|
|
||||||
setup() {
|
setup() {
|
||||||
const { categories, actions } = useGroup();
|
const { categories, actions } = useGroup();
|
||||||
|
|
||||||
@ -32,5 +37,3 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
@ -1,11 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container fluid>
|
<v-container class="narrow-container">
|
||||||
<BaseCardSectionTitle title="MealPlan Webhooks">
|
<BasePageTitle divider>
|
||||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Alias error provident, eveniet, laboriosam assumenda
|
<template #header>
|
||||||
earum amet quaerat vel consequatur molestias sed enim. Adipisci a consequuntur dolor culpa expedita voluptatem
|
<v-img max-height="125" max-width="125" :src="require('~/static/svgs/manage-webhooks.svg')"></v-img>
|
||||||
praesentium optio iste atque, ea reiciendis iure non aut suscipit modi ducimus ratione, quam numquam quaerat
|
</template>
|
||||||
distinctio illum nemo. Dicta, doloremque!
|
<template #title> Webhooks </template>
|
||||||
</BaseCardSectionTitle>
|
The webhooks defined below will be executed when a meal is defined for the day. At the scheduled time the webhooks
|
||||||
|
will be sent with the data from the recipe that is scheduled for the day
|
||||||
|
</BasePageTitle>
|
||||||
|
|
||||||
<BaseButton create @click="actions.createOne()" />
|
<BaseButton create @click="actions.createOne()" />
|
||||||
<v-expansion-panels class="mt-2">
|
<v-expansion-panels class="mt-2">
|
||||||
<v-expansion-panel v-for="(webhook, index) in webhooks" :key="index" class="my-2 my-border rounded">
|
<v-expansion-panel v-for="(webhook, index) in webhooks" :key="index" class="my-2 my-border rounded">
|
||||||
@ -53,16 +56,13 @@ import { defineComponent } from "@nuxtjs/composition-api";
|
|||||||
import { useGroupWebhooks } from "~/composables/use-group-webhooks";
|
import { useGroupWebhooks } from "~/composables/use-group-webhooks";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
layout: "admin",
|
|
||||||
setup() {
|
setup() {
|
||||||
const { actions, webhooks } = useGroupWebhooks();
|
const { actions, webhooks } = useGroupWebhooks();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
actions,
|
|
||||||
webhooks,
|
webhooks,
|
||||||
|
actions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
@ -1,31 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-container fluid>
|
|
||||||
<v-row>
|
|
||||||
<v-col cols="12" sm="12" md="12" lg="6">
|
|
||||||
<UserProfileCard :user="user" class="mt-14" @refresh="$auth.fetchUser()" />
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" sm="12" md="12" lg="6">
|
|
||||||
<UserAPITokenCard :tokens="user.tokens" class="mt-14" @refresh="$auth.fetchUser()" />
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
|
|
||||||
import UserProfileCard from "~/components/Domain/User/UserProfileCard.vue";
|
|
||||||
import UserAPITokenCard from "~/components/Domain/User/UserAPITokenCard.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: { UserProfileCard, UserAPITokenCard },
|
|
||||||
layout: "admin",
|
|
||||||
setup() {
|
|
||||||
const user = computed(() => {
|
|
||||||
return useContext().$auth.user;
|
|
||||||
});
|
|
||||||
|
|
||||||
return { user };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
131
frontend/pages/user/profile/api-tokens.vue
Normal file
131
frontend/pages/user/profile/api-tokens.vue
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<v-container class="narrow-container">
|
||||||
|
<BasePageTitle divider>
|
||||||
|
<template #header>
|
||||||
|
<v-img max-height="200px" max-width="200px" :src="require('~/static/svgs/manage-api-tokens.svg')"></v-img>
|
||||||
|
</template>
|
||||||
|
<template #title> API Tokens </template>
|
||||||
|
You have {{ user.tokens.length }} active tokens.
|
||||||
|
</BasePageTitle>
|
||||||
|
<section class="d-flex justify-center">
|
||||||
|
<v-card class="mt-4" width="500px">
|
||||||
|
<v-card-text>
|
||||||
|
<v-form ref="domNewTokenForm" @submit.prevent>
|
||||||
|
<v-text-field v-model="name" :label="$t('settings.token.token-name')"> </v-text-field>
|
||||||
|
</v-form>
|
||||||
|
|
||||||
|
<template v-if="createdToken != ''">
|
||||||
|
<v-textarea
|
||||||
|
v-model="createdToken"
|
||||||
|
class="mb-0 pb-0"
|
||||||
|
:label="$t('settings.token.api-token')"
|
||||||
|
readonly
|
||||||
|
:append-outer-icon="$globals.icons.contentCopy"
|
||||||
|
@click="copyToken"
|
||||||
|
@click:append-outer="copyToken"
|
||||||
|
>
|
||||||
|
</v-textarea>
|
||||||
|
<v-subheader class="text-center">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
"settings.token.copy-this-token-for-use-with-an-external-application-this-token-will-not-be-viewable-again"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</v-subheader>
|
||||||
|
</template>
|
||||||
|
</v-card-text>
|
||||||
|
<v-expand-transition>
|
||||||
|
<v-card-actions v-show="name != ''">
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<BaseButton v-if="createdToken" cancel @click="resetCreate()"> Close </BaseButton>
|
||||||
|
<BaseButton v-else :cancel="false" @click="createToken(name)"> Generate </BaseButton>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-expand-transition>
|
||||||
|
</v-card>
|
||||||
|
</section>
|
||||||
|
<BaseCardSectionTitle class="mt-10" title="Active Tokens"> </BaseCardSectionTitle>
|
||||||
|
<section class="d-flex flex-column align-center justify-center">
|
||||||
|
<div v-for="(token, index) in $auth.user.tokens" :key="index" class="d-flex my-2">
|
||||||
|
<v-card outlined width="500px">
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>
|
||||||
|
{{ token.name }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle> Created on: {{ $d(token.created_at) }} </v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
<v-list-item-action>
|
||||||
|
<BaseButton delete small @click="deleteToken(token.id)"></BaseButton>
|
||||||
|
</v-list-item-action>
|
||||||
|
</v-list-item>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, useContext, ref } from "@nuxtjs/composition-api";
|
||||||
|
import { useApiSingleton } from "~/composables/use-api";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const nuxtContext = useContext();
|
||||||
|
|
||||||
|
const user = computed(() => {
|
||||||
|
return nuxtContext.$auth.user;
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = useApiSingleton();
|
||||||
|
|
||||||
|
const domNewTokenForm = ref<VForm | null>(null);
|
||||||
|
|
||||||
|
const createdToken = ref("");
|
||||||
|
const name = ref("");
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
function resetCreate() {
|
||||||
|
createdToken.value = "";
|
||||||
|
loading.value = false;
|
||||||
|
name.value = "";
|
||||||
|
nuxtContext.$auth.fetchUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createToken(name: string) {
|
||||||
|
if (loading.value) {
|
||||||
|
resetCreate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
if (domNewTokenForm?.value?.validate()) {
|
||||||
|
console.log("Created");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await api.users.createAPIToken({ name });
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
createdToken.value = data.token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteToken(id: string | number) {
|
||||||
|
const { data } = await api.users.deleteAPIToken(id);
|
||||||
|
nuxtContext.$auth.fetchUser();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToken() {
|
||||||
|
navigator.clipboard.writeText(createdToken.value).then(
|
||||||
|
() => console.log("Copied", createdToken.value),
|
||||||
|
() => console.log("Copied Failed", createdToken.value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { createToken, deleteToken, copyToken, createdToken, loading, name, user, resetCreate };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
169
frontend/pages/user/profile/edit.vue
Normal file
169
frontend/pages/user/profile/edit.vue
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<v-container class="narrow-container">
|
||||||
|
<BasePageTitle divider>
|
||||||
|
<template #header>
|
||||||
|
<v-img max-height="200" max-width="200" class="mb-2" :src="require('~/static/svgs/manage-profile.svg')"></v-img>
|
||||||
|
</template>
|
||||||
|
<template #title> Your Profile Settings </template>
|
||||||
|
Some text here...
|
||||||
|
</BasePageTitle>
|
||||||
|
<section>
|
||||||
|
<ToggleState tag="article">
|
||||||
|
<template #activator="{ toggle, state }">
|
||||||
|
<v-btn v-if="!state" text color="info" class="mt-2 mb-n3" @click="toggle">
|
||||||
|
<v-icon left>{{ $globals.icons.lock }}</v-icon>
|
||||||
|
{{ $t("settings.change-password") }}
|
||||||
|
</v-btn>
|
||||||
|
<v-btn v-else text color="info" class="mt-2 mb-n3" @click="toggle">
|
||||||
|
<v-icon left>{{ $globals.icons.user }}</v-icon>
|
||||||
|
{{ $t("settings.profile") }}
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #default="{ state }">
|
||||||
|
<v-slide-x-transition group mode="in" hide-on-leave>
|
||||||
|
<div v-if="!state" key="personal-info">
|
||||||
|
<BaseCardSectionTitle class="mt-10" title="Personal Information"> </BaseCardSectionTitle>
|
||||||
|
<v-card tag="article" outlined>
|
||||||
|
<v-card-text class="pb-0">
|
||||||
|
<v-form ref="userUpdate">
|
||||||
|
<v-text-field v-model="userCopy.username" :label="$t('user.username')" required validate-on-blur>
|
||||||
|
</v-text-field>
|
||||||
|
<v-text-field v-model="userCopy.fullName" :label="$t('user.full-name')" required validate-on-blur>
|
||||||
|
</v-text-field>
|
||||||
|
<v-text-field v-model="userCopy.email" :label="$t('user.email')" validate-on-blur required>
|
||||||
|
</v-text-field>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<BaseButton update @click="updateUser" />
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
<div v-if="state" key="change-password">
|
||||||
|
<BaseCardSectionTitle class="mt-10" :title="$t('settings.change-password')"> </BaseCardSectionTitle>
|
||||||
|
<v-card outlined>
|
||||||
|
<v-card-text class="pb-0">
|
||||||
|
<v-form ref="passChange">
|
||||||
|
<v-text-field
|
||||||
|
v-model="password.current"
|
||||||
|
:prepend-icon="$globals.icons.lock"
|
||||||
|
:label="$t('user.current-password')"
|
||||||
|
validate-on-blur
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
@click:append="showPassword.current = !showPassword.current"
|
||||||
|
></v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
v-model="password.newOne"
|
||||||
|
:prepend-icon="$globals.icons.lock"
|
||||||
|
:label="$t('user.new-password')"
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
@click:append="showPassword.newOne = !showPassword.newOne"
|
||||||
|
></v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
v-model="password.newTwo"
|
||||||
|
:prepend-icon="$globals.icons.lock"
|
||||||
|
:label="$t('user.confirm-password')"
|
||||||
|
:rules="[password.newOne === password.newTwo || $t('user.password-must-match')]"
|
||||||
|
validate-on-blur
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
@click:append="showPassword.newTwo = !showPassword.newTwo"
|
||||||
|
></v-text-field>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<BaseButton update @click="updateUser" />
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</v-slide-x-transition>
|
||||||
|
</template>
|
||||||
|
</ToggleState>
|
||||||
|
</section>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { ref, reactive, defineComponent, computed, useContext, watch } from "@nuxtjs/composition-api";
|
||||||
|
import { useApiSingleton } from "~/composables/use-api";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const nuxtContext = useContext();
|
||||||
|
const user = computed(() => nuxtContext.$auth.user);
|
||||||
|
|
||||||
|
watch(user, () => {
|
||||||
|
userCopy.value = { ...user.value };
|
||||||
|
});
|
||||||
|
|
||||||
|
const userCopy = ref({ ...user.value });
|
||||||
|
|
||||||
|
const api = useApiSingleton();
|
||||||
|
|
||||||
|
const domUpdatePassword = ref<VForm | null>(null);
|
||||||
|
const password = reactive({
|
||||||
|
current: "",
|
||||||
|
newOne: "",
|
||||||
|
newTwo: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateUser() {
|
||||||
|
// @ts-ignore
|
||||||
|
const { response } = await api.users.updateOne(userCopy.value.id, userCopy.value);
|
||||||
|
if (response?.status === 200) {
|
||||||
|
nuxtContext.$auth.fetchUser();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePassword() {
|
||||||
|
if (!userCopy.value?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
const { response } = await api.users.changePassword(userCopy.value.id, {
|
||||||
|
currentPassword: password.current,
|
||||||
|
newPassword: password.newOne,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.status === 200) {
|
||||||
|
console.log("Password Changed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { updateUser, updatePassword, userCopy, password, domUpdatePassword };
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hideImage: false,
|
||||||
|
passwordLoading: false,
|
||||||
|
showPassword: false,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async changePassword() {
|
||||||
|
// @ts-ignore
|
||||||
|
this.paswordLoading = true;
|
||||||
|
const data = {
|
||||||
|
currentPassword: this.password.current,
|
||||||
|
newPassword: this.password.newOne,
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (this.$refs.passChange.validate()) {
|
||||||
|
// @ts-ignore
|
||||||
|
if (await api.users.changePassword(this.user.id, data)) {
|
||||||
|
this.$emit("refresh");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
this.paswordLoading = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
93
frontend/pages/user/profile/index.vue
Normal file
93
frontend/pages/user/profile/index.vue
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<v-container v-if="user">
|
||||||
|
<section class="d-flex flex-column align-center">
|
||||||
|
<v-avatar color="primary" size="75" class="mb-2">
|
||||||
|
<v-img :src="require(`~/static/account.png`)" />
|
||||||
|
</v-avatar>
|
||||||
|
<h2 class="headline">👋 Welcome, {{ user.fullName }}</h2>
|
||||||
|
<p class="subtitle-1 mb-0">
|
||||||
|
Manage your profile, recipes, and group settings.
|
||||||
|
<a href="https://hay-kot.github.io/mealie/" target="_blank"> Learn More </a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<div>
|
||||||
|
<h3 class="headline">Personal</h3>
|
||||||
|
<p>These are settings that are personal to you. Changes here won't affect other users</p>
|
||||||
|
</div>
|
||||||
|
<v-row tag="section">
|
||||||
|
<v-col cols="12" sm="12" md="6">
|
||||||
|
<UserProfileLinkCard
|
||||||
|
:link="{ text: 'Manage User Profile', to: '/user/profile/edit' }"
|
||||||
|
:image="require('~/static/svgs/manage-profile.svg')"
|
||||||
|
>
|
||||||
|
<template #title> User Profile </template>
|
||||||
|
Manage your preferences, change your password, and update your email
|
||||||
|
</UserProfileLinkCard>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="12" md="6">
|
||||||
|
<UserProfileLinkCard
|
||||||
|
:link="{ text: 'Manage Your API Tokens', to: '/user/profile/api-tokens' }"
|
||||||
|
:image="require('~/static/svgs/manage-api-tokens.svg')"
|
||||||
|
>
|
||||||
|
<template #title> API Tokens </template>
|
||||||
|
Manage your API Tokens for access from external applications
|
||||||
|
</UserProfileLinkCard>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</section>
|
||||||
|
<v-divider class="my-7"></v-divider>
|
||||||
|
<section>
|
||||||
|
<div>
|
||||||
|
<h3 class="headline">Group</h3>
|
||||||
|
<p>These items are shared within your group. Editing one of them will change it for the whole group!</p>
|
||||||
|
</div>
|
||||||
|
<v-row tag="section">
|
||||||
|
<v-col cols="12" sm="12" md="6">
|
||||||
|
<UserProfileLinkCard
|
||||||
|
:link="{ text: 'Group Settings', to: '/user/group' }"
|
||||||
|
:image="require('~/static/svgs/manage-group-settings.svg')"
|
||||||
|
>
|
||||||
|
<template #title> Group Settings </template>
|
||||||
|
Manage your common group settings like mealplan and privacy settings.
|
||||||
|
</UserProfileLinkCard>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="12" md="6">
|
||||||
|
<UserProfileLinkCard
|
||||||
|
:link="{ text: 'Manage Cookbooks', to: '/user/group/cookbooks' }"
|
||||||
|
:image="require('~/static/svgs/manage-cookbooks.svg')"
|
||||||
|
>
|
||||||
|
<template #title> Cookbooks </template>
|
||||||
|
Manage a collection of recipe categories and generate pages for them.
|
||||||
|
</UserProfileLinkCard>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="12" md="6">
|
||||||
|
<UserProfileLinkCard
|
||||||
|
:link="{ text: 'Manage Webhooks', to: '/user/group/webhooks' }"
|
||||||
|
:image="require('~/static/svgs/manage-webhooks.svg')"
|
||||||
|
>
|
||||||
|
<template #title> Webhooks </template>
|
||||||
|
Setup webhooks that trigger on days that you have have mealplan scheduled.
|
||||||
|
</UserProfileLinkCard>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</section>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||||
|
import UserProfileLinkCard from "@/components/Domain/User/UserProfileLinkCard.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
UserProfileLinkCard,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const user = computed(() => useContext().$auth.user);
|
||||||
|
|
||||||
|
return { user };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -205,7 +205,7 @@ const icons = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line no-empty-pattern
|
// eslint-disable-next-line no-empty-pattern
|
||||||
export default ({}, inject) => {
|
export default ({}, inject: any) => {
|
||||||
// Inject $hello(msg) in Vue, context and store.
|
// Inject $hello(msg) in Vue, context and store.
|
||||||
inject("globals", { icons });
|
inject("globals", { icons });
|
||||||
};
|
};
|
1
frontend/static/svgs/manage-api-tokens.svg
Normal file
1
frontend/static/svgs/manage-api-tokens.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 22 KiB |
1
frontend/static/svgs/manage-cookbooks.svg
Normal file
1
frontend/static/svgs/manage-cookbooks.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 16 KiB |
1
frontend/static/svgs/manage-group-settings.svg
Normal file
1
frontend/static/svgs/manage-group-settings.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 11 KiB |
1
frontend/static/svgs/manage-profile.svg
Normal file
1
frontend/static/svgs/manage-profile.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 13 KiB |
1
frontend/static/svgs/manage-webhooks.svg
Normal file
1
frontend/static/svgs/manage-webhooks.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg id="ad6b5295-7ebf-4dc3-a7a8-a4a4b8d35fca" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="790" height="512.20805" viewBox="0 0 790 512.20805"><path d="M925.56335,704.58909,903,636.49819s24.81818,24.81818,24.81818,45.18181l-4.45454-47.09091s12.72727,17.18182,11.45454,43.27273S925.56335,704.58909,925.56335,704.58909Z" transform="translate(-205 -193.89598)" fill="#e6e6e6"/><path d="M441.02093,642.58909,419,576.13509s24.22155,24.22155,24.22155,44.09565l-4.34745-45.95885s12.42131,16.76877,11.17917,42.23245S441.02093,642.58909,441.02093,642.58909Z" transform="translate(-205 -193.89598)" fill="#e6e6e6"/><path d="M784.72555,673.25478c.03773,43.71478-86.66489,30.26818-192.8092,30.35979s-191.53562,13.68671-191.57335-30.028,86.63317-53.29714,192.77748-53.38876S784.68782,629.54,784.72555,673.25478Z" transform="translate(-205 -193.89598)" fill="#e6e6e6"/><rect y="509.69312" width="790" height="2" fill="#3f3d56"/><polygon points="505.336 420.322 491.459 420.322 484.855 366.797 505.336 366.797 505.336 420.322" fill="#a0616a"/><path d="M480.00587,416.35743H508.3101a0,0,0,0,1,0,0V433.208a0,0,0,0,1,0,0H464.69674a0,0,0,0,1,0,0v-1.54149A15.30912,15.30912,0,0,1,480.00587,416.35743Z" fill="#2f2e41"/><polygon points="607.336 499.322 593.459 499.322 586.855 445.797 607.336 445.797 607.336 499.322" fill="#a0616a"/><path d="M582.00587,495.35743H610.3101a0,0,0,0,1,0,0V512.208a0,0,0,0,1,0,0H566.69674a0,0,0,0,1,0,0v-1.54149A15.30912,15.30912,0,0,1,582.00587,495.35743Z" fill="#2f2e41"/><path d="M876.34486,534.205A10.31591,10.31591,0,0,0,873.449,518.654l-32.23009-131.2928L820.6113,396.2276l38.33533,126.949a10.37185,10.37185,0,0,0,17.39823,11.0284Z" transform="translate(-205 -193.89598)" fill="#a0616a"/><path d="M851.20767,268.85955a11.38227,11.38227,0,0,0-17.41522,1.15247l-49.88538,5.72709,7.58861,19.24141,45.36779-8.49083a11.44393,11.44393,0,0,0,14.3442-17.63014Z" transform="translate(-205 -193.89598)" fill="#a0616a"/><path d="M769,520.58909l21.76811,163.37417,27.09338-5.578s-3.98437-118.98157,9.56238-133.32513S810,505.58909,810,505.58909Z" transform="translate(-205 -193.89598)" fill="#2f2e41"/><path d="M778,475.58909l-10,15s-77-31.99929-77,19-4.40631,85.60944-6,88,18.43762,8.59375,28,7c0,0,11.79687-82.21884,11-87,0,0,75.53355,37.03335,89.87712,33.84591S831.60944,536.964,834,530.58909s-1-57-1-57l-47.81-14.59036Z" transform="translate(-205 -193.89598)" fill="#2f2e41"/><path d="M779.34915,385.52862l-2.85032-3.42039s-31.92361-71.82815-19.3822-91.21035,67.26762-22.23252,68.97783-21.0924-4.08488,15.9428-.09446,22.78361c0,0-42.394,9.19121-45.24435,10.33134s21.96615,43.2737,21.96615,43.2737l-2.85031,25.6529Z" transform="translate(-205 -193.89598)" fill="#ccc"/><path d="M835.21549,350.18459S805.57217,353.605,804.432,353.605s-1.71017-7.41084-1.71017-7.41084l-26.223,35.91406S763.57961,486.29929,767,484.58909s66.50531,8.11165,67.07539,3.55114-.57008-27.3631,1.14014-28.50324,29.64328-71.82811,29.64328-71.82811-2.85032-14.82168-12.54142-19.95227S835.21549,350.18459,835.21549,350.18459Z" transform="translate(-205 -193.89598)" fill="#ccc"/><path d="M855.73783,378.11779l9.121,9.69109S878.41081,499.1687,871,502.58909s-22,3-22,3l-14.35458-52.79286Z" transform="translate(-205 -193.89598)" fill="#ccc"/><circle cx="601.72966" cy="122.9976" r="26.2388" fill="#a0616a"/><path d="M800.57267,320.98789c-.35442-5.44445-7.22306-5.631-12.67878-5.68255s-11.97836.14321-15.0654-4.35543c-2.0401-2.973-1.65042-7.10032.035-10.28779s4.45772-5.639,7.18508-7.99742c7.04139-6.08884,14.29842-12.12936,22.7522-16.02662s18.36045-5.472,27.12788-2.3435c10.77008,3.84307,25.32927,23.62588,26.5865,34.99176s-3.28507,22.95252-10.9419,31.44586-25.18188,5.0665-36.21069,8.088c6.7049-9.48964,2.28541-26.73258-8.45572-31.164Z" transform="translate(-205 -193.89598)" fill="#2f2e41"/><circle cx="361.7217" cy="403.5046" r="62.98931" fill="#e58325"/><path d="M524.65625,529.9355a45.15919,45.15919,0,0,1-41.25537-26.78614L383.44873,278.05757a59.83039,59.83039,0,1,1,111.87012-41.86426l72.37744,235.41211a45.07978,45.07978,0,0,1-43.04,58.33008Z" transform="translate(-205 -193.89598)" fill="#e58325"/></svg>
|
After Width: | Height: | Size: 4.0 KiB |
@ -60,8 +60,8 @@ export interface Recipe {
|
|||||||
recipeCategory: string[];
|
recipeCategory: string[];
|
||||||
tags: string[];
|
tags: string[];
|
||||||
rating: number;
|
rating: number;
|
||||||
dateAdded?: string;
|
dateAdded: string;
|
||||||
dateUpdated?: string;
|
dateUpdated: string;
|
||||||
recipeYield?: string;
|
recipeYield?: string;
|
||||||
recipeIngredient: RecipeIngredient[];
|
recipeIngredient: RecipeIngredient[];
|
||||||
recipeInstructions: RecipeStep[];
|
recipeInstructions: RecipeStep[];
|
||||||
|
@ -1,205 +0,0 @@
|
|||||||
import {
|
|
||||||
mdiAccount,
|
|
||||||
mdiSilverwareVariant,
|
|
||||||
mdiPlus,
|
|
||||||
mdiPlusCircle,
|
|
||||||
mdiDelete,
|
|
||||||
mdiContentSave,
|
|
||||||
mdiContentSaveEdit,
|
|
||||||
mdiSquareEditOutline,
|
|
||||||
mdiClose,
|
|
||||||
mdiTagMultipleOutline,
|
|
||||||
mdiBookOutline,
|
|
||||||
mdiAccountCog,
|
|
||||||
mdiAccountGroup,
|
|
||||||
mdiHome,
|
|
||||||
mdiMagnify,
|
|
||||||
mdiTranslate,
|
|
||||||
mdiClockTimeFourOutline,
|
|
||||||
mdiImport,
|
|
||||||
mdiEmail,
|
|
||||||
mdiLock,
|
|
||||||
mdiEye,
|
|
||||||
mdiEyeOff,
|
|
||||||
mdiCalendarMinus,
|
|
||||||
mdiCalendar,
|
|
||||||
mdiDiceMultiple,
|
|
||||||
mdiAlertCircle,
|
|
||||||
mdiDotsVertical,
|
|
||||||
mdiPrinter,
|
|
||||||
mdiShareVariant,
|
|
||||||
mdiHeart,
|
|
||||||
mdiHeartOutline,
|
|
||||||
mdiDotsHorizontal,
|
|
||||||
mdiCheckboxBlankOutline,
|
|
||||||
mdiCommentTextMultipleOutline,
|
|
||||||
mdiDownload,
|
|
||||||
mdiFile,
|
|
||||||
mdiFilePdfBox,
|
|
||||||
mdiFileImage,
|
|
||||||
mdiCodeJson,
|
|
||||||
mdiArrowUpDown,
|
|
||||||
mdiCog,
|
|
||||||
mdiSort,
|
|
||||||
mdiOrderAlphabeticalAscending,
|
|
||||||
mdiStar,
|
|
||||||
mdiNewBox,
|
|
||||||
mdiShuffleVariant,
|
|
||||||
mdiAlert,
|
|
||||||
mdiCheckboxMarkedCircle,
|
|
||||||
mdiInformation,
|
|
||||||
mdiBellAlert,
|
|
||||||
mdiRefreshCircle,
|
|
||||||
mdiMenu,
|
|
||||||
mdiWeatherSunny,
|
|
||||||
mdiWeatherNight,
|
|
||||||
mdiLink,
|
|
||||||
mdiRobot,
|
|
||||||
mdiLinkVariant,
|
|
||||||
mdiViewModule,
|
|
||||||
mdiViewDashboard,
|
|
||||||
mdiTools,
|
|
||||||
mdiCalendarWeek,
|
|
||||||
mdiCalendarToday,
|
|
||||||
mdiCalendarMultiselect,
|
|
||||||
mdiFormatListChecks,
|
|
||||||
mdiLogout,
|
|
||||||
mdiContentCopy,
|
|
||||||
mdiClipboardCheck,
|
|
||||||
mdiCloudUpload,
|
|
||||||
mdiDatabase,
|
|
||||||
mdiGithub,
|
|
||||||
mdiFolderOutline,
|
|
||||||
mdiApi,
|
|
||||||
mdiTestTube,
|
|
||||||
mdiDevTo,
|
|
||||||
mdiBackupRestore,
|
|
||||||
mdiNotificationClearAll,
|
|
||||||
mdiFood,
|
|
||||||
mdiWebhook,
|
|
||||||
mdiFilter,
|
|
||||||
mdiAccountPlusOutline,
|
|
||||||
mdiDesktopTowerMonitor,
|
|
||||||
mdiFormatColorFill,
|
|
||||||
mdiFormSelect,
|
|
||||||
mdiPageLayoutBody,
|
|
||||||
mdiCalendarWeekBegin,
|
|
||||||
mdiOpenInNew,
|
|
||||||
mdiCheck,
|
|
||||||
mdiBroom,
|
|
||||||
mdiCartCheck,
|
|
||||||
mdiArrowLeftBold,
|
|
||||||
mdiMinus,
|
|
||||||
mdiWindowClose,
|
|
||||||
mdiFolderZipOutline,
|
|
||||||
} from "@mdi/js";
|
|
||||||
|
|
||||||
const icons = {
|
|
||||||
// Primary
|
|
||||||
primary: mdiSilverwareVariant,
|
|
||||||
|
|
||||||
// General
|
|
||||||
alert: mdiAlert,
|
|
||||||
alertCircle: mdiAlertCircle,
|
|
||||||
api: mdiApi,
|
|
||||||
arrowLeftBold: mdiArrowLeftBold,
|
|
||||||
arrowUpDown: mdiArrowUpDown,
|
|
||||||
backupRestore: mdiBackupRestore,
|
|
||||||
bellAlert: mdiBellAlert,
|
|
||||||
broom: mdiBroom,
|
|
||||||
calendar: mdiCalendar,
|
|
||||||
calendarMinus: mdiCalendarMinus,
|
|
||||||
calendarMultiselect: mdiCalendarMultiselect,
|
|
||||||
calendarToday: mdiCalendarToday,
|
|
||||||
calendarWeek: mdiCalendarWeek,
|
|
||||||
calendarWeekBegin: mdiCalendarWeekBegin,
|
|
||||||
cartCheck: mdiCartCheck,
|
|
||||||
check: mdiCheck,
|
|
||||||
checkboxBlankOutline: mdiCheckboxBlankOutline,
|
|
||||||
checkboxMarkedCircle: mdiCheckboxMarkedCircle,
|
|
||||||
clipboardCheck: mdiClipboardCheck,
|
|
||||||
clockOutline: mdiClockTimeFourOutline,
|
|
||||||
codeBraces: mdiCodeJson,
|
|
||||||
codeJson: mdiCodeJson,
|
|
||||||
cog: mdiCog,
|
|
||||||
commentTextMultipleOutline: mdiCommentTextMultipleOutline,
|
|
||||||
contentCopy: mdiContentCopy,
|
|
||||||
database: mdiDatabase,
|
|
||||||
desktopTowerMonitor: mdiDesktopTowerMonitor,
|
|
||||||
devTo: mdiDevTo,
|
|
||||||
diceMultiple: mdiDiceMultiple,
|
|
||||||
dotsHorizontal: mdiDotsHorizontal,
|
|
||||||
dotsVertical: mdiDotsVertical,
|
|
||||||
download: mdiDownload,
|
|
||||||
email: mdiEmail,
|
|
||||||
externalLink: mdiLinkVariant,
|
|
||||||
eye: mdiEye,
|
|
||||||
eyeOff: mdiEyeOff,
|
|
||||||
file: mdiFile,
|
|
||||||
fileImage: mdiFileImage,
|
|
||||||
filePDF: mdiFilePdfBox,
|
|
||||||
filter: mdiFilter,
|
|
||||||
folderOutline: mdiFolderOutline,
|
|
||||||
food: mdiFood,
|
|
||||||
formatColorFill: mdiFormatColorFill,
|
|
||||||
formatListCheck: mdiFormatListChecks,
|
|
||||||
formSelect: mdiFormSelect,
|
|
||||||
github: mdiGithub,
|
|
||||||
heart: mdiHeart,
|
|
||||||
heartOutline: mdiHeartOutline,
|
|
||||||
home: mdiHome,
|
|
||||||
import: mdiImport,
|
|
||||||
information: mdiInformation,
|
|
||||||
link: mdiLink,
|
|
||||||
lock: mdiLock,
|
|
||||||
logout: mdiLogout,
|
|
||||||
menu: mdiMenu,
|
|
||||||
newBox: mdiNewBox,
|
|
||||||
notificationClearAll: mdiNotificationClearAll,
|
|
||||||
openInNew: mdiOpenInNew,
|
|
||||||
orderAlphabeticalAscending: mdiOrderAlphabeticalAscending,
|
|
||||||
pageLayoutBody: mdiPageLayoutBody,
|
|
||||||
printer: mdiPrinter,
|
|
||||||
refreshCircle: mdiRefreshCircle,
|
|
||||||
robot: mdiRobot,
|
|
||||||
search: mdiMagnify,
|
|
||||||
shareVariant: mdiShareVariant,
|
|
||||||
shuffleVariant: mdiShuffleVariant,
|
|
||||||
sort: mdiSort,
|
|
||||||
star: mdiStar,
|
|
||||||
testTube: mdiTestTube,
|
|
||||||
tools: mdiTools,
|
|
||||||
translate: mdiTranslate,
|
|
||||||
upload: mdiCloudUpload,
|
|
||||||
viewDashboard: mdiViewDashboard,
|
|
||||||
viewModule: mdiViewModule,
|
|
||||||
weatherNight: mdiWeatherNight,
|
|
||||||
weatherSunny: mdiWeatherSunny,
|
|
||||||
webhook: mdiWebhook,
|
|
||||||
windowClose: mdiWindowClose,
|
|
||||||
zip: mdiFolderZipOutline,
|
|
||||||
|
|
||||||
// Crud
|
|
||||||
createAlt: mdiPlus,
|
|
||||||
create: mdiPlusCircle,
|
|
||||||
delete: mdiDelete,
|
|
||||||
save: mdiContentSave,
|
|
||||||
update: mdiContentSaveEdit,
|
|
||||||
edit: mdiSquareEditOutline,
|
|
||||||
close: mdiClose,
|
|
||||||
minus: mdiMinus,
|
|
||||||
|
|
||||||
// Organization
|
|
||||||
tags: mdiTagMultipleOutline,
|
|
||||||
pages: mdiBookOutline,
|
|
||||||
|
|
||||||
// Admin
|
|
||||||
user: mdiAccount,
|
|
||||||
admin: mdiAccountCog,
|
|
||||||
group: mdiAccountGroup,
|
|
||||||
accountPlusOutline: mdiAccountPlusOutline,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const globals = {
|
|
||||||
icons,
|
|
||||||
};
|
|
@ -1,52 +0,0 @@
|
|||||||
import { recipe } from "@/utils/recipe";
|
|
||||||
import { store } from "@/store";
|
|
||||||
|
|
||||||
// TODO: Migrate to Mixins
|
|
||||||
|
|
||||||
export const utils = {
|
|
||||||
recipe,
|
|
||||||
generateUniqueKey(item, index) {
|
|
||||||
return `${item}-${index}`;
|
|
||||||
},
|
|
||||||
getDateAsPythonDate(dateObject) {
|
|
||||||
if (!dateObject) return null;
|
|
||||||
const month = dateObject.getMonth() + 1;
|
|
||||||
const day = dateObject.getDate();
|
|
||||||
const year = dateObject.getFullYear();
|
|
||||||
return `${year}-${month}-${day}`;
|
|
||||||
},
|
|
||||||
notify: {
|
|
||||||
info(text, title = null) {
|
|
||||||
store.commit("setSnackbar", {
|
|
||||||
open: true,
|
|
||||||
title,
|
|
||||||
text,
|
|
||||||
color: "info",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
success(text, title = null) {
|
|
||||||
store.commit("setSnackbar", {
|
|
||||||
open: true,
|
|
||||||
title,
|
|
||||||
text,
|
|
||||||
color: "success",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
error(text, title = null) {
|
|
||||||
store.commit("setSnackbar", {
|
|
||||||
open: true,
|
|
||||||
title,
|
|
||||||
text,
|
|
||||||
color: "error",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
warning(text, title = null) {
|
|
||||||
store.commit("setSnackbar", {
|
|
||||||
open: true,
|
|
||||||
title,
|
|
||||||
text,
|
|
||||||
color: "warning",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,49 +0,0 @@
|
|||||||
export const recipe = {
|
|
||||||
/**
|
|
||||||
* Sorts a list of recipes in place
|
|
||||||
* @param {Array<Object>} list of recipes
|
|
||||||
* @param {Boolean} inverse - Z or A First
|
|
||||||
*/
|
|
||||||
sortAToZ(list) {
|
|
||||||
list.sort((a, b) => {
|
|
||||||
const textA = a.name.toUpperCase();
|
|
||||||
const textB = b.name.toUpperCase();
|
|
||||||
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
sortByCreated(list) {
|
|
||||||
list.sort((a, b) => (a.dateAdded > b.dateAdded ? -1 : 1));
|
|
||||||
},
|
|
||||||
sortByUpdated(list) {
|
|
||||||
list.sort((a, b) => (a.dateUpdated > b.dateUpdated ? -1 : 1));
|
|
||||||
},
|
|
||||||
sortByRating(list) {
|
|
||||||
list.sort((a, b) => (a.rating > b.rating ? -1 : 1));
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {Array<Object>} list
|
|
||||||
* @returns String / Recipe Slug
|
|
||||||
*/
|
|
||||||
randomRecipe(list) {
|
|
||||||
return list[Math.floor(Math.random() * list.length)];
|
|
||||||
},
|
|
||||||
shuffle(list) {
|
|
||||||
let last = list.length;
|
|
||||||
let n;
|
|
||||||
while (last > 0) {
|
|
||||||
n = rand(last);
|
|
||||||
swap(list, n, --last);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const rand = n =>
|
|
||||||
Math.floor(Math.random() * n)
|
|
||||||
|
|
||||||
function swap(t, i, j) {
|
|
||||||
const q = t[i];
|
|
||||||
t[i] = t[j];
|
|
||||||
t[j] = q;
|
|
||||||
return t;
|
|
||||||
}
|
|
@ -1,3 +1,4 @@
|
|||||||
|
from datetime import datetime
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from fastapi_camelcase import CamelModel
|
from fastapi_camelcase import CamelModel
|
||||||
@ -19,6 +20,7 @@ class LoingLiveTokenIn(CamelModel):
|
|||||||
|
|
||||||
class LongLiveTokenOut(LoingLiveTokenIn):
|
class LongLiveTokenOut(LoingLiveTokenIn):
|
||||||
id: int
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
Loading…
x
Reference in New Issue
Block a user