feat(frontend): ✨ Add Initial CookBook Support
This commit is contained in:
parent
2e6352cfbd
commit
83ab858e46
10 changed files with 266 additions and 32 deletions
31
frontend/api/class-interfaces/cookbooks.ts
Normal file
31
frontend/api/class-interfaces/cookbooks.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { BaseCRUDAPI } from "./_base";
|
||||
import { Category } from "./categories";
|
||||
import { CategoryBase } from "~/types/api-types/recipe";
|
||||
|
||||
const prefix = "/api";
|
||||
|
||||
export interface CreateCookBook {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface CookBook extends CreateCookBook {
|
||||
id: number;
|
||||
slug: string;
|
||||
position: number;
|
||||
group_id: number;
|
||||
categories: Category[] | CategoryBase[];
|
||||
}
|
||||
|
||||
const routes = {
|
||||
cookbooks: `${prefix}/groups/cookbooks`,
|
||||
cookbooksId: (id: number) => `${prefix}/groups/cookbooks/${id}`,
|
||||
};
|
||||
|
||||
export class CookbookAPI extends BaseCRUDAPI<CookBook, CreateCookBook> {
|
||||
baseRoute: string = routes.cookbooks;
|
||||
itemRoute = routes.cookbooksId;
|
||||
|
||||
async updateAll(payload: CookBook[]) {
|
||||
return await this.requests.put(this.baseRoute, payload);
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import { UtilsAPI } from "./class-interfaces/utils";
|
|||
import { NotificationsAPI } from "./class-interfaces/event-notifications";
|
||||
import { FoodAPI } from "./class-interfaces/recipe-foods";
|
||||
import { UnitAPI } from "./class-interfaces/recipe-units";
|
||||
import { CookbookAPI } from "./class-interfaces/cookbooks";
|
||||
import { ApiRequestInstance } from "~/types/api";
|
||||
|
||||
class Api {
|
||||
|
@ -27,6 +28,7 @@ class Api {
|
|||
public notifications: NotificationsAPI;
|
||||
public foods: FoodAPI;
|
||||
public units: UnitAPI;
|
||||
public cookbooks: CookbookAPI;
|
||||
|
||||
// Utils
|
||||
public upload: UploadFile;
|
||||
|
@ -46,6 +48,7 @@ class Api {
|
|||
// Users
|
||||
this.users = new UserApi(requests);
|
||||
this.groups = new GroupAPI(requests);
|
||||
this.cookbooks = new CookbookAPI(requests);
|
||||
|
||||
// Admin
|
||||
this.debug = new DebugAPI(requests);
|
||||
|
|
|
@ -46,6 +46,7 @@ export default {
|
|||
if (props.disableAmount) {
|
||||
return ingredient.note;
|
||||
}
|
||||
|
||||
const { quantity, food, unit, note } = ingredient;
|
||||
|
||||
let return_qty = "";
|
||||
|
@ -59,7 +60,7 @@ export default {
|
|||
return_qty += ` <sup>${fraction[1]}</sup>⁄<sub>${fraction[2]}</sub>`;
|
||||
}
|
||||
} else {
|
||||
return_qty = quantity;
|
||||
return_qty = quantity * props.scale;
|
||||
}
|
||||
|
||||
return `${return_qty} ${unit?.name || " "} ${food?.name || " "} ${note}`;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<v-navigation-drawer :value="value" clipped app width="240px">
|
||||
<!-- User Profile -->
|
||||
<template v-if="$auth.user">
|
||||
<v-list-item two-line to="/user/profile">
|
||||
<v-list-item two-line to="/user/profile" exact>
|
||||
<v-list-item-avatar color="accent" class="white--text">
|
||||
<v-img :src="require(`~/static/account.png`)" />
|
||||
</v-list-item-avatar>
|
||||
|
@ -31,7 +31,7 @@
|
|||
<v-list-item-title>{{ nav.title }}</v-list-item-title>
|
||||
</template>
|
||||
|
||||
<v-list-item v-for="child in nav.children" :key="child.title" :to="child.to">
|
||||
<v-list-item v-for="child in nav.children" :key="child.title" exact :to="child.to">
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ child.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
@ -47,7 +47,7 @@
|
|||
v-model="secondarySelected"
|
||||
color="primary"
|
||||
>
|
||||
<v-list-item link :to="nav.to">
|
||||
<v-list-item exact link :to="nav.to">
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ nav.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
@ -62,7 +62,7 @@
|
|||
<template v-if="secondaryLinks">
|
||||
<v-subheader v-if="secondaryHeader" class="pb-0">{{ secondaryHeader }}</v-subheader>
|
||||
<v-divider></v-divider>
|
||||
<v-list nav dense>
|
||||
<v-list nav dense exact>
|
||||
<template v-for="nav in secondaryLinks">
|
||||
<!-- Multi Items -->
|
||||
<v-list-group
|
||||
|
@ -76,7 +76,7 @@
|
|||
<v-list-item-title>{{ nav.title }}</v-list-item-title>
|
||||
</template>
|
||||
|
||||
<v-list-item v-for="child in nav.children" :key="child.title" :to="child.to">
|
||||
<v-list-item v-for="child in nav.children" :key="child.title" exact :to="child.to">
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ child.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
@ -87,7 +87,7 @@
|
|||
|
||||
<!-- Single Item -->
|
||||
<v-list-item-group v-else :key="nav.title + 'single-item'" v-model="secondarySelected" color="primary">
|
||||
<v-list-item link :to="nav.to">
|
||||
<v-list-item exact link :to="nav.to">
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ nav.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
@ -105,6 +105,7 @@
|
|||
<v-list-item
|
||||
v-for="nav in bottomLinks"
|
||||
:key="nav.title"
|
||||
exact
|
||||
link
|
||||
:to="nav.to || null"
|
||||
:href="nav.href || null"
|
||||
|
|
|
@ -44,6 +44,10 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
save: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
delete: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
|
120
frontend/composables/use-cookbooks.ts
Normal file
120
frontend/composables/use-cookbooks.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
import { useAsync, ref, reactive, Ref } from "@nuxtjs/composition-api";
|
||||
import { useAsyncKey } from "./use-utils";
|
||||
import { useApiSingleton } from "~/composables/use-api";
|
||||
import { CookBook } from "~/api/class-interfaces/cookbooks";
|
||||
|
||||
let cookbookStore: Ref<CookBook[] | null> | null = null;
|
||||
|
||||
export const useCookbooks = function () {
|
||||
const api = useApiSingleton();
|
||||
const loading = ref(false);
|
||||
const deleteTargetId = ref(0);
|
||||
const validForm = ref(true);
|
||||
|
||||
// @ts-ignore
|
||||
const workingCookbookData: CookBook = reactive({
|
||||
id: 0,
|
||||
name: "",
|
||||
position: 1,
|
||||
categories: [],
|
||||
});
|
||||
|
||||
const actions = {
|
||||
getAll() {
|
||||
loading.value = true;
|
||||
const units = useAsync(async () => {
|
||||
const { data } = await api.cookbooks.getAll();
|
||||
|
||||
return data;
|
||||
}, useAsyncKey());
|
||||
|
||||
loading.value = false;
|
||||
return units;
|
||||
},
|
||||
async refreshAll() {
|
||||
loading.value = true;
|
||||
const { data } = await api.cookbooks.getAll();
|
||||
|
||||
if (data && cookbookStore) {
|
||||
cookbookStore.value = data;
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
},
|
||||
async createOne() {
|
||||
loading.value = true;
|
||||
const { data } = await api.cookbooks.createOne({
|
||||
// @ts-ignore. I"m thinking this will always be defined.
|
||||
name: "New Cookbook" + String(cookbookStore?.value?.length + 1 || 1),
|
||||
});
|
||||
if (data && cookbookStore?.value) {
|
||||
cookbookStore.value.unshift(data);
|
||||
} else {
|
||||
this.refreshAll();
|
||||
}
|
||||
|
||||
this.resetWorking();
|
||||
loading.value = false;
|
||||
},
|
||||
async updateOne(updateData: CookBook) {
|
||||
if (!updateData.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const { data } = await api.cookbooks.updateOne(updateData.id, updateData);
|
||||
if (data && cookbookStore?.value) {
|
||||
this.refreshAll();
|
||||
}
|
||||
loading.value = false;
|
||||
},
|
||||
|
||||
async updateOrder() {
|
||||
if (!cookbookStore?.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
cookbookStore.value.forEach((element, index) => {
|
||||
element.position = index + 1;
|
||||
});
|
||||
|
||||
const { data } = await api.cookbooks.updateAll(cookbookStore.value);
|
||||
|
||||
if (data && cookbookStore?.value) {
|
||||
this.refreshAll();
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
},
|
||||
async deleteOne(id: string | number) {
|
||||
loading.value = true;
|
||||
const { data } = await api.cookbooks.deleteOne(id);
|
||||
if (data && cookbookStore?.value) {
|
||||
this.refreshAll();
|
||||
}
|
||||
},
|
||||
resetWorking() {
|
||||
workingCookbookData.id = 0;
|
||||
workingCookbookData.name = "";
|
||||
workingCookbookData.position = 0;
|
||||
workingCookbookData.categories = [];
|
||||
},
|
||||
setWorking(item: CookBook) {
|
||||
workingCookbookData.id = item.id;
|
||||
workingCookbookData.name = item.name;
|
||||
workingCookbookData.position = item.position;
|
||||
workingCookbookData.categories = item.categories;
|
||||
},
|
||||
flushStore() {
|
||||
cookbookStore = null;
|
||||
},
|
||||
};
|
||||
|
||||
if (!cookbookStore) {
|
||||
cookbookStore = actions.getAll();
|
||||
}
|
||||
|
||||
return { cookbooks: cookbookStore, workingCookbookData, deleteTargetId, actions, validForm };
|
||||
};
|
|
@ -2,7 +2,14 @@
|
|||
<v-app dark>
|
||||
<!-- <TheSnackbar /> -->
|
||||
|
||||
<AppSidebar v-model="sidebar" absolute :top-link="topLinks" @input="sidebar = !sidebar" />
|
||||
<AppSidebar
|
||||
v-model="sidebar"
|
||||
absolute
|
||||
:top-link="topLinks"
|
||||
secondary-header="Cookbooks"
|
||||
:secondary-links="cookbookLinks || []"
|
||||
@input="sidebar = !sidebar"
|
||||
/>
|
||||
|
||||
<AppHeader>
|
||||
<v-btn icon @click.stop="sidebar = !sidebar">
|
||||
|
@ -20,17 +27,32 @@
|
|||
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
import AppHeader from "@/components/Layout/AppHeader.vue";
|
||||
import AppSidebar from "@/components/Layout/AppSidebar.vue";
|
||||
import AppFloatingButton from "@/components/Layout/AppFloatingButton.vue";
|
||||
import { useCookbooks } from "~/composables/use-cookbooks";
|
||||
|
||||
export default defineComponent({
|
||||
components: { AppHeader, AppSidebar, AppFloatingButton },
|
||||
// @ts-ignore
|
||||
// middleware: process.env.GLOBAL_MIDDLEWARE,
|
||||
setup() {
|
||||
return {};
|
||||
const { cookbooks } = useCookbooks();
|
||||
// @ts-ignore
|
||||
const { $globals } = useContext();
|
||||
|
||||
const cookbookLinks = computed(() => {
|
||||
if (!cookbooks.value) return [];
|
||||
return cookbooks.value.map((cookbook) => {
|
||||
return {
|
||||
icon: $globals.icons.pages,
|
||||
title: cookbook.name,
|
||||
to: `/cookbooks/${cookbook.slug}`,
|
||||
};
|
||||
});
|
||||
});
|
||||
return { cookbookLinks };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -242,6 +242,9 @@ export default {
|
|||
icons: false,
|
||||
},
|
||||
theme: {
|
||||
options: {
|
||||
customProperties: true,
|
||||
},
|
||||
dark: false,
|
||||
themes: {
|
||||
dark: {
|
||||
|
|
|
@ -110,26 +110,35 @@
|
|||
/>
|
||||
</draggable>
|
||||
<div class="d-flex justify-end mt-2">
|
||||
<RecipeIngredientParserMenu class="mr-1" :ingredients="recipe.recipeIngredient" />
|
||||
<RecipeDialogBulkAdd class="mr-1" @bulk-data="addIngredient" />
|
||||
<BaseButton class="mr-1" @click="addIngredient"> {{ $t("general.new") }} </BaseButton>
|
||||
<RecipeIngredientParserMenu :ingredients="recipe.recipeIngredient" />
|
||||
<BaseButton @click="addIngredient"> {{ $t("general.new") }} </BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-space-between align-center pb-3">
|
||||
<v-btn
|
||||
v-if="recipe.recipeYield"
|
||||
dense
|
||||
small
|
||||
:hover="false"
|
||||
type="label"
|
||||
:ripple="false"
|
||||
elevation="0"
|
||||
color="secondary darken-1"
|
||||
class="rounded-sm static"
|
||||
>
|
||||
{{ scaledYield }}
|
||||
</v-btn>
|
||||
<v-tooltip small top color="secondary darken-1">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn
|
||||
v-if="recipe.recipeYield"
|
||||
dense
|
||||
small
|
||||
:hover="false"
|
||||
type="label"
|
||||
:ripple="false"
|
||||
elevation="0"
|
||||
color="secondary darken-1"
|
||||
class="rounded-sm static"
|
||||
v-bind="attrs"
|
||||
@click="scale = 1"
|
||||
v-on="on"
|
||||
>
|
||||
{{ scaledYield }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<span> Reset Scale </span>
|
||||
</v-tooltip>
|
||||
|
||||
<template v-if="!recipe.settings.disableAmount">
|
||||
<v-btn color="secondary darken-1" class="mx-1" small @click="scale > 1 ? scale-- : null">
|
||||
<v-icon>
|
||||
|
|
|
@ -1,24 +1,64 @@
|
|||
<template>
|
||||
<v-container fluid>
|
||||
<BaseCardSectionTitle title="Group Pages">
|
||||
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>
|
||||
<BaseCardSectionTitle title="Cookbooks"> </BaseCardSectionTitle>
|
||||
<BaseButton create @click="actions.createOne()" />
|
||||
<v-expansion-panels class="mt-2">
|
||||
<draggable v-model="cookbooks" handle=".handle" style="width: 100%" @change="actions.updateOrder()">
|
||||
<v-expansion-panel v-for="(cookbook, index) in cookbooks" :key="index" class="my-2 my-border rounded">
|
||||
<v-expansion-panel-header disable-icon-rotate class="headline">
|
||||
{{ cookbook.name }}
|
||||
<template #actions>
|
||||
<v-btn color="info" fab small class="ml-auto mr-2">
|
||||
<v-icon color="white">
|
||||
{{ $globals.icons.edit }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
<v-icon class="handle">
|
||||
{{ $globals.icons.arrowUpDown }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<v-card-text>
|
||||
<v-text-field v-model="cookbooks[index].name" label="Cookbook Name"></v-text-field>
|
||||
<DomainRecipeCategoryTagSelector v-model="cookbooks[index].categories" />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<BaseButton delete @click="actions.deleteOne(cookbook.id)" />
|
||||
<BaseButton update @click="actions.updateOne(cookbook)"> </BaseButton>
|
||||
</v-card-actions>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</draggable>
|
||||
</v-expansion-panels>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { useCookbooks } from "@/composables/use-cookbooks";
|
||||
import draggable from "vuedraggable";
|
||||
|
||||
export default defineComponent({
|
||||
components: { draggable },
|
||||
layout: "admin",
|
||||
setup() {
|
||||
return {};
|
||||
const { cookbooks, actions, workingCookbookData, deleteTargetId, validForm } = useCookbooks();
|
||||
|
||||
return {
|
||||
cookbooks,
|
||||
actions,
|
||||
workingCookbookData,
|
||||
deleteTargetId,
|
||||
validForm,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.my-border {
|
||||
border-left: 5px solid var(--v-primary-base);
|
||||
}
|
||||
</style>
|
Loading…
Reference in a new issue