feat(frontend): Add Initial CookBook Support

This commit is contained in:
hay-kot 2021-08-31 14:39:02 -08:00
parent 2e6352cfbd
commit 83ab858e46
10 changed files with 266 additions and 32 deletions

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

View file

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

View file

@ -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>&frasl;<sub>${fraction[2]}</sub>`;
}
} else {
return_qty = quantity;
return_qty = quantity * props.scale;
}
return `${return_qty} ${unit?.name || " "} ${food?.name || " "} ${note}`;

View file

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

View file

@ -44,6 +44,10 @@ export default {
type: Boolean,
default: false,
},
save: {
type: Boolean,
default: false,
},
delete: {
type: Boolean,
default: false,

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

View file

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

View file

@ -242,6 +242,9 @@ export default {
icons: false,
},
theme: {
options: {
customProperties: true,
},
dark: false,
themes: {
dark: {

View file

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

View file

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