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
40 changed files with 723 additions and 796 deletions
|
@ -6,3 +6,7 @@
|
|||
.layout-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.narrow-container {
|
||||
max-width: 700px !important;
|
||||
}
|
||||
|
|
|
@ -102,9 +102,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { utils } from "@/utils";
|
||||
import RecipeCard from "./RecipeCard";
|
||||
import RecipeCardMobile from "./RecipeCardMobile";
|
||||
import { useSorter } from "~/composables/use-recipes";
|
||||
const SORT_EVENT = "sort";
|
||||
|
||||
export default {
|
||||
|
@ -142,6 +142,11 @@ export default {
|
|||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const utils = useSorter();
|
||||
|
||||
return { utils };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
sortLoading: false,
|
||||
|
@ -197,7 +202,7 @@ export default {
|
|||
this.loading = false;
|
||||
},
|
||||
navigateRandom() {
|
||||
const recipe = utils.recipe.randomRecipe(this.recipes);
|
||||
const recipe = this.utils.recipe.randomRecipe(this.recipes);
|
||||
this.$router.push(`/recipe/${recipe.slug}`);
|
||||
},
|
||||
sortRecipes(sortType) {
|
||||
|
@ -205,19 +210,19 @@ export default {
|
|||
const sortTarget = [...this.recipes];
|
||||
switch (sortType) {
|
||||
case this.EVENTS.az:
|
||||
utils.recipe.sortAToZ(sortTarget);
|
||||
this.utils.sortAToZ(sortTarget);
|
||||
break;
|
||||
case this.EVENTS.rating:
|
||||
utils.recipe.sortByRating(sortTarget);
|
||||
this.utils.sortByRating(sortTarget);
|
||||
break;
|
||||
case this.EVENTS.created:
|
||||
utils.recipe.sortByCreated(sortTarget);
|
||||
this.utils.sortByCreated(sortTarget);
|
||||
break;
|
||||
case this.EVENTS.updated:
|
||||
utils.recipe.sortByUpdated(sortTarget);
|
||||
this.utils.sortByUpdated(sortTarget);
|
||||
break;
|
||||
case this.EVENTS.shuffle:
|
||||
utils.recipe.shuffle(sortTarget);
|
||||
this.utils.shuffle(sortTarget);
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown Event", sortType);
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
>
|
||||
<template #selection="data">
|
||||
<v-chip
|
||||
v-if="showSelected"
|
||||
:key="data.index"
|
||||
class="ma-1"
|
||||
:input-value="data.selected"
|
||||
|
@ -78,6 +79,10 @@ export default {
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showSelected: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
|
|
|
@ -41,7 +41,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { utils } from "@/utils";
|
||||
import { defineComponent, ref } from "@nuxtjs/composition-api";
|
||||
import { useApiSingleton } from "~/composables/use-api";
|
||||
export default defineComponent({
|
||||
|
@ -203,7 +202,6 @@ export default defineComponent({
|
|||
navigator.clipboard.writeText(copyText).then(
|
||||
() => {
|
||||
console.log("Copied to Clipboard", copyText);
|
||||
utils.notify.success("Copied to Clipboard");
|
||||
},
|
||||
() => console.log("Copied Failed", copyText)
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div v-if="value && value.length > 0">
|
||||
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
|
||||
<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>
|
||||
<v-divider v-if="showTitleEditor[index]"></v-divider>
|
||||
<v-list-item dense @click="toggleChecked(index)">
|
||||
|
@ -20,7 +20,6 @@
|
|||
<script>
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
import { useFraction } from "@/composables/use-fraction";
|
||||
import { utils } from "@/utils";
|
||||
export default {
|
||||
components: {
|
||||
VueMarkdown,
|
||||
|
@ -87,10 +86,6 @@ export default {
|
|||
this.showTitleEditor = this.value.map((x) => this.validateTitle(x.title));
|
||||
},
|
||||
methods: {
|
||||
generateKey(item, index) {
|
||||
return utils.generateUniqueKey(item, index);
|
||||
},
|
||||
|
||||
toggleChecked(index) {
|
||||
this.$set(this.checked, index, !this.checked[index]);
|
||||
},
|
||||
|
|
|
@ -88,7 +88,6 @@
|
|||
<script>
|
||||
import draggable from "vuedraggable";
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
import { utils } from "@/utils";
|
||||
export default {
|
||||
components: {
|
||||
VueMarkdown,
|
||||
|
@ -126,9 +125,6 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
generateKey(item, index) {
|
||||
return utils.generateUniqueKey(item, index);
|
||||
},
|
||||
removeByIndex(list, index) {
|
||||
list.splice(index, 1);
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div v-if="value.length > 0 || edit">
|
||||
<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">
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
|
@ -35,7 +35,6 @@
|
|||
|
||||
<script>
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
import { utils } from "@/utils";
|
||||
export default {
|
||||
components: {
|
||||
VueMarkdown,
|
||||
|
@ -52,9 +51,6 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
generateKey(item, index) {
|
||||
return utils.generateUniqueKey(item, index);
|
||||
},
|
||||
addNote() {
|
||||
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: {
|
||||
text: "Cancel",
|
||||
icon: this.$globals.icons.cancel,
|
||||
icon: this.$globals.icons.close,
|
||||
color: "grey",
|
||||
},
|
||||
download: {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<template>
|
||||
<v-card flat class="pb-2">
|
||||
<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">
|
||||
<slot />
|
||||
</p>
|
||||
<v-divider class="my-4"></v-divider>
|
||||
</v-card>
|
||||
</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 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) => {
|
||||
const api = useApiSingleton();
|
||||
|
||||
|
|
|
@ -46,28 +46,6 @@ export default defineComponent({
|
|||
return {
|
||||
sidebar: null,
|
||||
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,
|
||||
to: "/admin/dashboard",
|
||||
|
@ -157,9 +135,6 @@ export default defineComponent({
|
|||
],
|
||||
};
|
||||
},
|
||||
head: {
|
||||
title: "Admin",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
:top-link="topLinks"
|
||||
secondary-header="Cookbooks"
|
||||
:secondary-links="cookbookLinks || []"
|
||||
:bottom-links="bottomLink"
|
||||
:bottom-links="$auth.user.admin ? bottomLink : []"
|
||||
@input="sidebar = !sidebar"
|
||||
/>
|
||||
|
||||
|
@ -62,7 +62,7 @@ export default defineComponent({
|
|||
{
|
||||
icon: this.$globals.icons.cog,
|
||||
title: this.$t("general.settings"),
|
||||
to: "/user/profile",
|
||||
to: "/admin/dashboard",
|
||||
restricted: true,
|
||||
},
|
||||
],
|
||||
|
|
|
@ -30,7 +30,7 @@ export default {
|
|||
css: [{ src: "~/assets/main.css" }, { src: "~/assets/style-overrides.scss" }],
|
||||
|
||||
// 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
|
||||
components: true,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<v-container>
|
||||
<RecipeCardSection
|
||||
v-if="category"
|
||||
:icon="$globals.icons.tags"
|
||||
:title="category.name"
|
||||
:recipes="category.recipes"
|
||||
|
|
|
@ -7,9 +7,10 @@
|
|||
<v-toolbar-title class="headline"> {{ $t("recipe.categories") }} </v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</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-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-actions>
|
||||
<v-icon>
|
||||
|
@ -21,12 +22,12 @@
|
|||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-slide-x-transition>
|
||||
</section>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<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 { useAsyncKey } from "~/composables/use-utils";
|
||||
|
||||
|
@ -38,7 +39,25 @@ export default defineComponent({
|
|||
const { data } = await api.categories.getAll();
|
||||
return data;
|
||||
}, 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>
|
||||
|
|
|
@ -7,9 +7,10 @@
|
|||
<v-toolbar-title class="headline"> {{ $t("tag.tags") }} </v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</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-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-actions>
|
||||
<v-icon>
|
||||
|
@ -21,12 +22,12 @@
|
|||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-slide-x-transition>
|
||||
</section>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<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 { useAsyncKey } from "~/composables/use-utils";
|
||||
|
||||
|
@ -38,7 +39,25 @@ export default defineComponent({
|
|||
const { data } = await api.tags.getAll();
|
||||
return data;
|
||||
}, 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>
|
||||
|
|
|
@ -16,31 +16,47 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row dense class="my-0 flex-row align-center justify-space-around">
|
||||
<v-col>
|
||||
<h3 class="pl-2 text-center headline">
|
||||
{{ $t("category.category-filter") }}
|
||||
</h3>
|
||||
<RecipeSearchFilterSelector class="mb-1" @update="updateCatParams" />
|
||||
<RecipeCategoryTagSelector v-model="includeCategories" :solo="true" :dense="false" :return-object="false" />
|
||||
</v-col>
|
||||
<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>
|
||||
<ToggleState>
|
||||
<template #activator="{ state, toggle }">
|
||||
<v-switch :value="state" color="info" class="ma-0 pa-0" label="Advanced" @input="toggle" @click="toggle">
|
||||
Advanced
|
||||
</v-switch>
|
||||
</template>
|
||||
<template #default="{ state }">
|
||||
<v-expand-transition>
|
||||
<v-row v-show="state" dense class="my-0 dense flex-row align-center justify-space-around">
|
||||
<v-col>
|
||||
<h3 class="pl-2 text-center headline">
|
||||
{{ $t("category.category-filter") }}
|
||||
</h3>
|
||||
<RecipeSearchFilterSelector class="mb-1" @update="updateCatParams" />
|
||||
<RecipeCategoryTagSelector
|
||||
v-model="includeCategories"
|
||||
:solo="true"
|
||||
:dense="false"
|
||||
:return-object="false"
|
||||
/>
|
||||
</v-col>
|
||||
<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
|
||||
class="mt-n9"
|
||||
class="mt-n5"
|
||||
:title-icon="$globals.icons.magnify"
|
||||
:recipes="showRecipes"
|
||||
:hard-limit="maxResults"
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
<template>
|
||||
<v-container fluid>
|
||||
<BaseCardSectionTitle title="Cookbooks"> </BaseCardSectionTitle>
|
||||
<v-container class="narrow-container">
|
||||
<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()" />
|
||||
<v-expansion-panels class="mt-2">
|
||||
<draggable v-model="cookbooks" handle=".handle" style="width: 100%" @change="actions.updateOrder()">
|
||||
|
@ -48,7 +55,6 @@ import draggable from "vuedraggable";
|
|||
|
||||
export default defineComponent({
|
||||
components: { draggable },
|
||||
layout: "admin",
|
||||
setup() {
|
||||
const { cookbooks, actions } = useCookbooks();
|
||||
|
||||
|
@ -62,6 +68,6 @@ export default defineComponent({
|
|||
|
||||
<style>
|
||||
.my-border {
|
||||
border-left: 5px solid var(--v-primary-base);
|
||||
border-left: 5px solid var(--v-primary-base) !important;
|
||||
}
|
||||
</style>
|
|
@ -1,17 +1,23 @@
|
|||
<template>
|
||||
<v-container fluid>
|
||||
<section>
|
||||
<BaseCardSectionTitle title="Group Settings">
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Alias error provident, eveniet, laboriosam assumenda
|
||||
earum amet quaerat vel consequatur molestias sed enim. Adipisci a consequuntur dolor culpa expedita voluptatem
|
||||
praesentium optio iste atque, ea reiciendis iure non aut suscipit modi ducimus ratione, quam numquam quaerat
|
||||
distinctio illum nemo. Dicta, doloremque!
|
||||
</BaseCardSectionTitle>
|
||||
<div v-if="categories" class="d-flex">
|
||||
<DomainRecipeCategoryTagSelector v-model="categories" class="mt-5 mr-5" />
|
||||
<BaseButton save class="mt-auto mb-3" @click="actions.updateAll()" />
|
||||
</div>
|
||||
</section>
|
||||
<v-container>
|
||||
<BasePageTitle divider>
|
||||
<template #header>
|
||||
<v-img max-height="100" max-width="100" :src="require('~/static/svgs/manage-group-settings.svg')"></v-img>
|
||||
</template>
|
||||
<template #title> Group Settings </template>
|
||||
These items are shared within your group. Editing one of them will change it for the whole group!
|
||||
</BasePageTitle>
|
||||
<v-card tag="section" outlined>
|
||||
<v-card-text>
|
||||
<BaseCardSectionTitle title="Mealplan Categories">
|
||||
Set the categories below for the ones that you want to be included in your mealplan random generation.
|
||||
<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>
|
||||
</template>
|
||||
|
||||
|
@ -20,7 +26,6 @@ import { defineComponent } from "@nuxtjs/composition-api";
|
|||
import { useGroup } from "~/composables/use-groups";
|
||||
|
||||
export default defineComponent({
|
||||
layout: "admin",
|
||||
setup() {
|
||||
const { categories, actions } = useGroup();
|
||||
|
||||
|
@ -32,5 +37,3 @@ export default defineComponent({
|
|||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
|
@ -1,11 +1,14 @@
|
|||
<template>
|
||||
<v-container fluid>
|
||||
<BaseCardSectionTitle title="MealPlan Webhooks">
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Alias error provident, eveniet, laboriosam assumenda
|
||||
earum amet quaerat vel consequatur molestias sed enim. Adipisci a consequuntur dolor culpa expedita voluptatem
|
||||
praesentium optio iste atque, ea reiciendis iure non aut suscipit modi ducimus ratione, quam numquam quaerat
|
||||
distinctio illum nemo. Dicta, doloremque!
|
||||
</BaseCardSectionTitle>
|
||||
<v-container class="narrow-container">
|
||||
<BasePageTitle divider>
|
||||
<template #header>
|
||||
<v-img max-height="125" max-width="125" :src="require('~/static/svgs/manage-webhooks.svg')"></v-img>
|
||||
</template>
|
||||
<template #title> Webhooks </template>
|
||||
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()" />
|
||||
<v-expansion-panels class="mt-2">
|
||||
<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";
|
||||
|
||||
export default defineComponent({
|
||||
layout: "admin",
|
||||
setup() {
|
||||
const { actions, webhooks } = useGroupWebhooks();
|
||||
|
||||
return {
|
||||
actions,
|
||||
webhooks,
|
||||
actions,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
</script>
|
|
@ -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
|
||||
export default ({}, inject) => {
|
||||
export default ({}, inject: any) => {
|
||||
// Inject $hello(msg) in Vue, context and store.
|
||||
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 KiB |
|
@ -60,8 +60,8 @@ export interface Recipe {
|
|||
recipeCategory: string[];
|
||||
tags: string[];
|
||||
rating: number;
|
||||
dateAdded?: string;
|
||||
dateUpdated?: string;
|
||||
dateAdded: string;
|
||||
dateUpdated: string;
|
||||
recipeYield?: string;
|
||||
recipeIngredient: RecipeIngredient[];
|
||||
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 fastapi_camelcase import CamelModel
|
||||
|
@ -19,6 +20,7 @@ class LoingLiveTokenIn(CamelModel):
|
|||
|
||||
class LongLiveTokenOut(LoingLiveTokenIn):
|
||||
id: int
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
|
Loading…
Reference in a new issue