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:
Hayden 2021-09-04 20:24:32 -08:00 committed by GitHub
parent 3d87ffc3a5
commit e179dcdb10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 723 additions and 796 deletions

View file

@ -6,3 +6,7 @@
.layout-leave-active {
opacity: 0;
}
.narrow-container {
max-width: 700px !important;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -131,7 +131,7 @@ export default {
},
cancel: {
text: "Cancel",
icon: this.$globals.icons.cancel,
icon: this.$globals.icons.close,
color: "grey",
},
download: {

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
<template>
<v-container>
<RecipeCardSection
v-if="category"
:icon="$globals.icons.tags"
:title="category.name"
:recipes="category.recipes"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

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

View file

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View 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

View file

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

View file

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

View file

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

View file

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

View file

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