feat(frontend): 🚧 CRUD Functionality

This commit is contained in:
hay-kot 2021-08-02 22:15:11 -08:00
parent 00a8fdda41
commit afcad2f701
49 changed files with 845 additions and 275 deletions

View File

@ -44,13 +44,13 @@
target="_blank"
rel="noreferrer nofollow"
>
{{ $t('new-recipe.google-ld-json-info') }}
{{ $t("new-recipe.google-ld-json-info") }}
</a>
<a href="https://github.com/hay-kot/mealie/issues" target="_blank" rel="noreferrer nofollow">
{{ $t('new-recipe.github-issues') }}
{{ $t("new-recipe.github-issues") }}
</a>
<a href="https://schema.org/Recipe" target="_blank" rel="noreferrer nofollow">
{{ $t('new-recipe.recipe-markup-specification') }}
{{ $t("new-recipe.recipe-markup-specification") }}
</a>
</div>
<div class="d-flex justify-end">
@ -61,7 +61,7 @@
@click="addRecipe = false"
>
<v-icon left> {{ $globals.icons.externalLink }} </v-icon>
{{ $t('new-recipe.view-scraped-data') }}
{{ $t("new-recipe.view-scraped-data") }}
</v-btn>
</div>
</v-alert>
@ -101,7 +101,7 @@
</v-card-text>
<v-card-actions>
<TheUploadBtn class="mx-auto" :text-btn="false" @uploaded="setFile" :post="false"> </TheUploadBtn>
<AppButtonUpload class="mx-auto" :text-btn="false" @uploaded="setFile" :post="false"> </AppButtonUpload>
</v-card-actions>
</BaseDialog>
<v-speed-dial v-model="fab" :open-on-hover="absolute" :fixed="absolute" :bottom="absolute" :right="absolute">
@ -140,11 +140,11 @@
<script>
import { api } from "@/api";
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn.vue";
import AppButtonUpload from "@/components/UI/Buttons/AppButtonUpload.vue";
import BaseDialog from "@/components/UI/Dialogs/BaseDialog.vue";
export default {
components: {
TheUploadBtn,
AppButtonUpload,
BaseDialog,
},
props: {
@ -232,8 +232,9 @@ export default {
this.processing = false;
},
isValidWebUrl(url) {
let regEx = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,256}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/gm;
return regEx.test(url) ? true : this.$t('new-recipe.must-be-a-valid-url');
let regEx =
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,256}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/gm;
return regEx.test(url) ? true : this.$t("new-recipe.must-be-a-valid-url");
},
},
};

View File

@ -29,13 +29,13 @@
</div>
</template>
<div class="d-flex row py-3 justify-end">
<TheUploadBtn url="/api/backups/upload" @uploaded="getAvailableBackups">
<AppButtonUpload url="/api/backups/upload" @uploaded="getAvailableBackups">
<template v-slot="{ isSelecting, onButtonClick }">
<v-btn :loading="isSelecting" class="mx-2" small color="info" @click="onButtonClick">
<v-icon left> {{ $globals.icons.upload }} </v-icon> {{ $t("general.upload") }}
</v-btn>
</template>
</TheUploadBtn>
</AppButtonUpload>
<BackupDialog :color="color" />
<v-btn :loading="loading" class="mx-2" small color="success" @click="createBackup">
@ -74,7 +74,7 @@
</template>
<script>
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
import AppButtonUpload from "@/components/UI/Buttons/AppButtonUpload";
import ImportSummaryDialog from "@/components/ImportSummaryDialog";
import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog";
import { api } from "@/api";
@ -85,7 +85,7 @@ const IMPORT_EVENT = "import";
const DELETE_EVENT = "delete";
export default {
components: { StatCard, ImportDialog, TheUploadBtn, ImportSummaryDialog, BackupDialog, ConfirmationDialog },
components: { StatCard, ImportDialog, AppButtonUpload, ImportSummaryDialog, BackupDialog, ConfirmationDialog },
data() {
return {
color: "accent",

View File

@ -70,7 +70,7 @@
<v-data-table :headers="headers" :items="links" sort-by="calories">
<template v-slot:item.token="{ item }">
{{ `${baseURL}/sign-up/${item.token}` }}
<TheCopyButton :copy-text="`${baseURL}/sign-up/${item.token}`" />
<AppCopyButton :copy-text="`${baseURL}/sign-up/${item.token}`" />
</template>
<template v-slot:item.admin="{ item }">
<v-btn small :color="item.admin ? 'success' : 'error'" text>
@ -94,12 +94,12 @@
</template>
<script>
import TheCopyButton from "@/components/UI/Buttons/TheCopyButton";
import AppCopyButton from "@/components/UI/Buttons/AppCopyButton";
import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog";
import { api } from "@/api";
import { validators } from "@/mixins/validators";
export default {
components: { ConfirmationDialog, TheCopyButton },
components: { ConfirmationDialog, AppCopyButton },
mixins: [validators],
data() {
return {

View File

@ -5,7 +5,7 @@
{{ title }}
<v-spacer></v-spacer>
<span>
<TheUploadBtn
<AppButtonUpload
class="mt-1"
:url="`/api/migrations/${folder}/upload`"
fileName="archive"
@ -55,7 +55,7 @@
</template>
<script>
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
import AppButtonUpload from "@/components/UI/Buttons/AppButtonUpload";
import { api } from "@/api";
import MigrationDialog from "./MigrationDialog";
export default {
@ -66,7 +66,7 @@ export default {
available: Array,
},
components: {
TheUploadBtn,
AppButtonUpload,
MigrationDialog,
},
data() {

View File

@ -90,7 +90,7 @@
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pb-1 pt-3">
<TheUploadBtn
<AppButtonUpload
:icon="$globals.icons.fileImage"
:text="$t('user.upload-photo')"
:url="userProfileImage"
@ -106,14 +106,14 @@
<script>
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
import StatCard from "@/components/UI/StatCard";
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
import AppButtonUpload from "@/components/UI/Buttons/AppButtonUpload";
import { api } from "@/api";
import { validators } from "@/mixins/validators";
import { initials } from "@/mixins/initials";
export default {
components: {
BaseDialog,
TheUploadBtn,
AppButtonUpload,
StatCard,
},
mixins: [validators, initials],

View File

@ -38,9 +38,9 @@
{{ $t("shopping-list.shopping-list") }}
</v-btn>
<v-spacer></v-spacer>
<TheCopyButton color="info" :copy-text="mealPlanURL(mealplan.uid)">
<AppCopyButton color="info" :copy-text="mealPlanURL(mealplan.uid)">
{{ $t("general.link-copied") }}
</TheCopyButton>
</AppCopyButton>
</v-card-actions>
<v-list class="mt-0 pt-0">
@ -90,12 +90,12 @@ import { api } from "@/api";
import { utils } from "@/utils";
import NewMeal from "@/components/MealPlan/MealPlanNew";
import EditPlan from "@/components/MealPlan/MealPlanEditor";
import TheCopyButton from "@/components/UI/Buttons/TheCopyButton";
import AppCopyButton from "@/components/UI/Buttons/AppCopyButton";
export default {
components: {
NewMeal,
EditPlan,
TheCopyButton,
AppCopyButton,
},
data: () => ({
plannedMeals: [],
@ -120,7 +120,7 @@ export default {
},
editPlan(id) {
this.plannedMeals.forEach(element => {
this.plannedMeals.forEach((element) => {
if (element.uid === id) {
this.editMealPlan = element;
}

View File

@ -51,7 +51,7 @@
<v-card v-else-if="activeList">
<v-card-title class="headline">
<TheCopyButton v-if="!edit" :copy-text="listAsText" color="info" />
<AppCopyButton v-if="!edit" :copy-text="listAsText" color="info" />
<v-text-field label="Name" single-line dense v-if="edit" v-model="activeList.name"> </v-text-field>
<div v-else>
{{ activeList.name }}
@ -141,14 +141,14 @@
<script>
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
import SearchDialog from "@/components/UI/Dialogs/SearchDialog";
import TheCopyButton from "@/components/UI/Buttons/TheCopyButton";
import AppCopyButton from "@/components/UI/Buttons/AppCopyButton";
import VueMarkdown from "@adapttive/vue-markdown";
import { api } from "@/api";
export default {
components: {
BaseDialog,
SearchDialog,
TheCopyButton,
AppCopyButton,
VueMarkdown,
},
data() {
@ -176,7 +176,7 @@ export default {
},
},
listAsText() {
const formatList = this.activeList.items.map(x => {
const formatList = this.activeList.items.map((x) => {
return `${x.quantity} - ${x.text}`;
});
@ -206,7 +206,7 @@ export default {
const recipe = response.data;
const ingredients = recipe.recipeIngredient.map(x => ({
const ingredients = recipe.recipeIngredient.map((x) => ({
title: "",
text: x.note,
quantity: 1,
@ -217,14 +217,14 @@ export default {
this.consolidateList();
},
consolidateList() {
const allText = this.activeList.items.map(x => x.text);
const allText = this.activeList.items.map((x) => x.text);
const uniqueText = allText.filter((item, index) => {
return allText.indexOf(item) === index;
});
const newItems = uniqueText.map(x => {
let matchingItems = this.activeList.items.filter(y => y.text === x);
const newItems = uniqueText.map((x) => {
let matchingItems = this.activeList.items.filter((y) => y.text === x);
matchingItems[0].quantity = this.sumQuantiy(matchingItems);
return matchingItems[0];
});
@ -233,7 +233,7 @@ export default {
},
sumQuantiy(itemList) {
let quantity = 0;
itemList.forEach(element => {
itemList.forEach((element) => {
quantity += element.quantity;
});
return quantity;
@ -241,7 +241,7 @@ export default {
setActiveList() {
if (!this.list) return null;
if (!this.group.shoppingLists) return null;
this.activeList = this.group.shoppingLists.find(x => x.id == this.list);
this.activeList = this.group.shoppingLists.find((x) => x.id == this.list);
},
async createNewList() {
this.newList.group = this.group.name;

View File

@ -1,10 +1,49 @@
import { ApiRequestInstance } from "~/types/api";
export class BaseAPIClass {
requests: ApiRequestInstance
constructor(requests: ApiRequestInstance) {
this.requests = requests;
}
export interface CrudAPIInterface {
requests: ApiRequestInstance;
// Route Properties / Methods
baseRoute: string;
itemRoute(itemId: string): string;
// Methods
}
export abstract class BaseAPIClass<T> implements CrudAPIInterface {
requests: ApiRequestInstance;
abstract baseRoute: string;
abstract itemRoute(itemId: string): string;
constructor(requests: ApiRequestInstance) {
this.requests = requests;
}
async getAll(start = 0, limit = 9999) {
return await this.requests.get<T[]>(this.baseRoute, {
params: { start, limit },
});
}
async getOne(itemId: string) {
return await this.requests.get<T>(this.itemRoute(itemId));
}
async createOne(payload: T) {
return await this.requests.post(this.baseRoute, payload);
}
async updateOne(itemId: string, payload: T){
return await this.requests.put<T>(this.itemRoute(itemId), payload);
}
async patchOne(itemId: string, payload: T) {
return await this.requests.patch(this.itemRoute(itemId), payload);
}
async deleteOne(itemId: string) {
return await this.requests.delete<T>(this.itemRoute(itemId));
}
}

View File

@ -11,30 +11,35 @@ const routes = {
recipesCreateUrl: `${prefix}/recipes/create-url`,
recipesCreateFromZip: `${prefix}/recipes/create-from-zip`,
recipesCategory: `${prefix}/recipes/category`,
recipesRecipeSlug: (recipe_slug: string) => `${prefix}/recipes/${recipe_slug}`,
recipesRecipeSlugZip: (recipe_slug: string) => `${prefix}/recipes/${recipe_slug}/zip`,
recipesRecipeSlugImage: (recipe_slug: string) => `${prefix}/recipes/${recipe_slug}/image`,
recipesRecipeSlugAssets: (recipe_slug: string) => `${prefix}/recipes/${recipe_slug}/assets`,
};
class RecipeAPI extends BaseAPIClass {
async getAll(start = 0, limit = 9999) {
return await this.requests.get<Recipe[]>(routes.recipesSummary, {
params: { start, limit },
class RecipeAPI extends BaseAPIClass<Recipe> {
baseRoute: string = routes.recipesSummary;
itemRoute = (itemid: string) => routes.recipesRecipeSlug(itemid);
async getAllByCategory(categories: string[]) {
return await this.requests.get<Recipe[]>(routes.recipesCategory, {
categories
});
}
async getOne(slug: string) {
return await this.requests.get<Recipe>(routes.recipesRecipeSlug(slug));
}
// @ts-ignore - Override method doesn't take same arguments are parent class
async createOne(name: string) {
return await this.requests.post(routes.recipesBase, { name });
}
async createOneByUrl(url: string) {
return await this.requests.post(routes.recipesCreateUrl, { url });
}
// * Methods to Generate reference urls for assets/images *
recipeImage(recipeSlug: string, version = null, key = null) {
return `/api/media/recipes/${recipeSlug}/images/original.webp?&rnd=${key}&version=${version}`;
}

View File

@ -15,7 +15,9 @@
:icon="$globals.icons.alertCircle"
@confirm="emitDelete()"
>
{{ $t("recipe.delete-confirmation") }}
<v-card-text>
{{ $t("recipe.delete-confirmation") }}
</v-card-text>
</BaseDialog>
<v-spacer></v-spacer>

View File

@ -26,7 +26,7 @@
<v-btn color="error" icon top @click="deleteAsset(i)">
<v-icon>{{ $globals.icons.delete }}</v-icon>
</v-btn>
<TheCopyButton :copy-text="copyLink(item.fileName)" />
<AppCopyButton :copy-text="copyLink(item.fileName)" />
</div>
</v-list-item-action>
</v-list-item>
@ -34,7 +34,7 @@
</v-card>
<div class="d-flex ml-auto mt-2">
<v-spacer></v-spacer>
<BaseDialog :title="$t('asset.new-asset')" :title-icon="getIconDefinition(newAsset.icon).icon" @submit="addAsset">
<BaseDialog :title="$t('asset.new-asset')" :icon="getIconDefinition(newAsset.icon).icon" @submit="addAsset">
<template #open="{ open }">
<v-btn v-if="edit" color="secondary" dark @click="open">
<v-icon>{{ $globals.icons.create }}</v-icon>
@ -61,7 +61,7 @@
{{ item.title }}
</template>
</v-select>
<TheUploadBtn :post="false" file-name="file" :text-btn="false" @uploaded="setFileObject" />
<AppButtonUpload :post="false" file-name="file" :text-btn="false" @uploaded="setFileObject" />
</div>
{{ fileObject.name }}
</v-card-text>
@ -71,26 +71,27 @@
</template>
<script>
import TheCopyButton from "@/components/UI/Buttons/TheCopyButton";
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
import { api } from "@/api";
import { useApiSingleton } from "~/composables/use-api";
export default {
components: {
BaseDialog,
TheUploadBtn,
TheCopyButton,
},
props: {
slug: String,
slug: {
type: String,
required: true,
},
value: {
type: Array,
required: true,
},
edit: {
type: Boolean,
default: true,
},
},
setup() {
const api = useApiSingleton();
return { api };
},
data() {
return {
fileObject: {},
@ -105,38 +106,38 @@ export default {
return window.location.origin;
},
iconOptions() {
return [
{
return [
{
name: "mdi-file",
title: this.$i18n.t("asset.file"),
icon: this.$globals.icons.file
icon: this.$globals.icons.file,
},
{
{
name: "mdi-file-pdf-box",
title: this.$i18n.t("asset.pdf"),
icon: this.$globals.icons.filePDF
icon: this.$globals.icons.filePDF,
},
{
{
name: "mdi-file-image",
title: this.$i18n.t("asset.image"),
icon: this.$globals.icons.fileImage
icon: this.$globals.icons.fileImage,
},
{
{
name: "mdi-code-json",
title: this.$i18n.t("asset.code"),
icon: this.$globals.icons.codeJson
icon: this.$globals.icons.codeJson,
},
{
{
name: "mdi-silverware-fork-knife",
title: this.$i18n.t("asset.recipe"),
icon: this.$globals.icons.primary
icon: this.$globals.icons.primary,
},
];
},
},
methods: {
getIconDefinition(val) {
return this.iconOptions.find(({ name }) => name === val );
return this.iconOptions.find(({ name }) => name === val);
},
assetURL(assetName) {
return api.recipes.recipeAssetPath(this.slug, assetName);

View File

@ -1,40 +1,41 @@
<template>
<v-hover v-slot="{ hover }" :open-delay="50">
<v-card
:class="{ 'on-hover': hover }"
:elevation="hover ? 12 : 2"
:to="route ? `/recipe/${slug}` : ''"
min-height="275"
@click="$emit('click')"
>
<RecipeCardImage icon-size="200" :slug="slug" small :image-version="image">
<v-expand-transition v-if="description">
<div v-if="hover" class="d-flex transition-fast-in-fast-out secondary v-card--reveal" style="height: 100%">
<v-card-text class="v-card--text-show white--text">
{{ description | truncate(300) }}
</v-card-text>
<v-lazy>
<v-hover v-slot="{ hover }" :open-delay="50">
<v-card
:class="{ 'on-hover': hover }"
:elevation="hover ? 12 : 2"
:to="route ? `/recipe/${slug}` : ''"
min-height="275"
@click="$emit('click')"
>
<RecipeCardImage icon-size="200" :slug="slug" small :image-version="image">
<v-expand-transition v-if="description">
<div v-if="hover" class="d-flex transition-fast-in-fast-out secondary v-card--reveal" style="height: 100%">
<v-card-text class="v-card--text-show white--text">
{{ description }}
</v-card-text>
</div>
</v-expand-transition>
</RecipeCardImage>
<v-card-title class="my-n3 mb-n6">
<div class="headerClass">
{{ name }}
</div>
</v-expand-transition>
</RecipeCardImage>
<v-card-title class="my-n3 mb-n6">
<div class="headerClass">
{{ name }}
</div>
</v-card-title>
</v-card-title>
<v-card-actions>
<RecipeFavoriteBadge v-if="loggedIn" :slug="slug" show-always />
<RecipeRating :value="rating" :name="name" :slug="slug" :small="true" />
<v-spacer></v-spacer>
<RecipeChips :truncate="true" :items="tags" :title="false" :limit="2" :small="true" :is-category="false" />
<RecipeContextMenu :slug="slug" :name="name" />
</v-card-actions>
</v-card>
</v-hover>
<v-card-actions>
<RecipeFavoriteBadge v-if="loggedIn" :slug="slug" show-always />
<RecipeRating :value="rating" :name="name" :slug="slug" :small="true" />
<v-spacer></v-spacer>
<RecipeChips :truncate="true" :items="tags" :title="false" :limit="2" :small="true" :is-category="false" />
<RecipeContextMenu :slug="slug" :name="name" />
</v-card-actions>
</v-card>
</v-hover>
</v-lazy>
</template>
<script>
import { api } from "@/api";
import RecipeFavoriteBadge from "./RecipeFavoriteBadge";
import RecipeChips from "./RecipeChips";
import RecipeContextMenu from "./RecipeContextMenu";
@ -82,11 +83,6 @@ export default {
return this.$store.getters.getIsLoggedIn;
},
},
methods: {
getImage(slug) {
return api.recipes.recipeSmallImage(slug, this.image);
},
},
};
</script>

View File

@ -18,7 +18,7 @@
</template>
<script>
import { useApi } from "~/composables/use-api";
import { useApiSingleton } from "~/composables/use-api";
export default {
props: {
tiny: {
@ -51,7 +51,7 @@ export default {
},
},
setup() {
const api = useApi();
const api = useApiSingleton();
return { api };
},

View File

@ -1,52 +1,55 @@
<template>
<v-expand-transition>
<v-card
:ripple="false"
class="mx-auto"
hover
:to="$listeners.selected ? undefined : `/recipe/${slug}`"
@click="$emit('selected')"
>
<v-list-item three-line>
<v-list-item-avatar tile size="125" class="v-mobile-img rounded-sm my-0 ml-n4">
<v-img
v-if="!fallBackImage"
:src="getImage(slug)"
@load="fallBackImage = false"
@error="fallBackImage = true"
></v-img>
<v-icon v-else color="primary" class="icon-position" size="100">
{{ $globals.icons.primary }}
</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title class="mb-1">{{ name }} </v-list-item-title>
<v-list-item-subtitle> {{ description }} </v-list-item-subtitle>
<div class="d-flex justify-center align-center">
<RecipeFavoriteBadge v-if="loggedIn" :slug="slug" show-always />
<v-rating
color="secondary"
class="ml-auto"
background-color="secondary lighten-3"
dense
length="5"
size="15"
:value="rating"
></v-rating>
<v-spacer></v-spacer>
<RecipeContextMenu :slug="slug" :menu-icon="$globals.icons.dotsHorizontal" :name="name" />
</div>
</v-list-item-content>
</v-list-item>
</v-card>
</v-expand-transition>
<v-lazy>
<v-expand-transition>
<v-card
:ripple="false"
class="mx-auto"
hover
:to="$listeners.selected ? undefined : `/recipe/${slug}`"
@click="$emit('selected')"
>
<v-list-item three-line>
<v-list-item-avatar tile size="125" class="v-mobile-img rounded-sm my-0 ml-n4">
<v-img
v-if="!fallBackImage"
:src="getImage(slug)"
@load="fallBackImage = false"
@error="fallBackImage = true"
></v-img>
<v-icon v-else color="primary" class="icon-position" size="100">
{{ $globals.icons.primary }}
</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title class="mb-1">{{ name }} </v-list-item-title>
<v-list-item-subtitle> {{ description }} </v-list-item-subtitle>
<div class="d-flex justify-center align-center">
<RecipeFavoriteBadge v-if="loggedIn" :slug="slug" show-always />
<v-rating
color="secondary"
class="ml-auto"
background-color="secondary lighten-3"
dense
length="5"
size="15"
:value="rating"
></v-rating>
<v-spacer></v-spacer>
<RecipeContextMenu :slug="slug" :menu-icon="$globals.icons.dotsHorizontal" :name="name" />
</div>
</v-list-item-content>
</v-list-item>
</v-card>
</v-expand-transition>
</v-lazy>
</template>
<script>
import { api } from "@/api";
import { defineComponent } from "@nuxtjs/composition-api";
import RecipeFavoriteBadge from "./RecipeFavoriteBadge";
import RecipeContextMenu from "./RecipeContextMenu";
export default {
import { useApiSingleton } from "~/composables/use-api";
export default defineComponent({
components: {
RecipeFavoriteBadge,
RecipeContextMenu,
@ -81,6 +84,11 @@ export default {
default: true,
},
},
setup() {
const api = useApiSingleton();
return { api };
},
data() {
return {
fallBackImage: false,
@ -93,10 +101,10 @@ export default {
},
methods: {
getImage(slug) {
return api.recipes.recipeSmallImage(slug, this.image);
return this.api.recipes.recipeSmallImage(slug, this.image);
},
},
};
});
</script>
<style>

View File

@ -117,7 +117,7 @@ export default {
type: Boolean,
default: false,
},
titleIcon: {
icon: {
type: String,
default: null,
},
@ -172,7 +172,7 @@ export default {
return Math.min(this.hardLimit, this.recipes.length);
},
displayTitleIcon() {
return this.titleIcon || this.$globals.icons.tags;
return this.icon || this.$globals.icons.tags;
},
},
watch: {
@ -223,7 +223,6 @@ export default {
console.log("Unknown Event", sortType);
return;
}
this.$emit(SORT_EVENT, sortTarget);
this.sortLoading = false;
},

View File

@ -1,13 +1,16 @@
<template>
<div class="text-center">
<BaseDialog
ref="deleteRecipieConfirm"
ref="confirmDelete"
:title="$t('recipe.delete-recipe')"
:message="$t('recipe.delete-confirmation')"
color="error"
:icon="$globals.icons.alertCircle"
@confirm="deleteRecipe()"
/>
>
<v-card-text>
{{ $t("recipe.delete-confirmation") }}
</v-card-text>
</BaseDialog>
<v-menu
offset-y
left
@ -38,9 +41,10 @@
</template>
<script>
import { api } from "@/api";
import { utils } from "@/utils";
export default {
import { defineComponent, ref } from "@nuxtjs/composition-api";
import { useApiSingleton } from "~/composables/use-api";
export default defineComponent({
props: {
menuTop: {
type: Boolean,
@ -60,11 +64,14 @@ export default {
},
slug: {
type: String,
required: true,
},
menuIcon: {
type: String,
default: null,
},
name: {
required: true,
type: String,
},
cardMenu: {
@ -72,6 +79,11 @@ export default {
default: true,
},
},
setup() {
const api = useApiSingleton();
const confirmDelete = ref(null);
return { api, confirmDelete };
},
data() {
return {
loading: true,
@ -82,7 +94,7 @@ export default {
return this.menuIcon ? this.menuIcon : this.$globals.icons.dotsVertical;
},
loggedIn() {
return this.$store.getters.getIsLoggedIn;
return this.$auth.loggedIn;
},
baseURL() {
return window.location.origin;
@ -145,12 +157,12 @@ export default {
},
},
methods: {
async menuAction(action) {
menuAction(action) {
this.loading = true;
switch (action) {
case "delete":
this.$refs.deleteRecipieConfirm.open();
this.confirmDelete.open();
break;
case "share":
if (navigator.share) {
@ -183,7 +195,8 @@ export default {
this.loading = false;
},
async deleteRecipe() {
await api.recipes.delete(this.slug);
console.log("Delete Called");
await this.api.recipes.deleteOne(this.slug);
},
updateClipboard() {
const copyText = this.recipeURL;
@ -196,5 +209,5 @@ export default {
);
},
},
};
});
</script>

View File

@ -39,18 +39,18 @@ export default {
},
computed: {
user() {
return this.$store.getters.getUserData;
return this.$auth.user;
},
isFavorite() {
return this.user.favoriteRecipes.includes(this.slug);
return this.$auth.user.favoriteRecipes.includes(this.slug);
},
},
methods: {
async toggleFavorite() {
if (!this.isFavorite) {
await api.users.addFavorite(this.user.id, this.slug);
await api.users.addFavorite(this.$auth.user.id, this.slug);
} else {
await api.users.removeFavorite(this.user.id, this.slug);
await api.users.removeFavorite(this.$auth.user.id, this.slug);
}
this.$store.dispatch("requestUserData");
},

View File

@ -14,7 +14,7 @@
<div>
{{ $t("recipe.recipe-image") }}
</div>
<TheUploadBtn
<AppButtonUpload
class="ml-auto"
url="none"
file-name="image"
@ -40,14 +40,10 @@
</template>
<script>
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
import { api } from "@/api";
const REFRESH_EVENT = "refresh";
const UPLOAD_EVENT = "upload";
export default {
components: {
TheUploadBtn,
},
props: {
slug: String,
},

View File

@ -49,7 +49,7 @@
</draggable>
<div class="d-flex row justify-end">
<BulkAdd class="mr-2" @bulk-data="addIngredient" />
<RecipeDialogBulkAdd class="mr-2" @bulk-data="addIngredient" />
<v-btn color="secondary" dark class="mr-4" @click="addIngredient">
<v-icon>{{ $globals.icons.create }}</v-icon>
</v-btn>
@ -71,13 +71,13 @@
</template>
<script>
import BulkAdd from "@/components/Recipe/Parts/Helpers/BulkAdd";
import VueMarkdown from "@adapttive/vue-markdown";
import draggable from "vuedraggable";
import { utils } from "@/utils";
import RecipeDialogBulkAdd from "./RecipeDialogBulkAdd";
export default {
components: {
BulkAdd,
RecipeDialogBulkAdd,
draggable,
VueMarkdown,
},
@ -101,18 +101,18 @@ export default {
watch: {
value: {
handler() {
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
this.showTitleEditor = this.value.map((x) => this.validateTitle(x.title));
},
},
},
mounted() {
this.checked = this.value.map(() => false);
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
this.showTitleEditor = this.value.map((x) => this.validateTitle(x.title));
},
methods: {
addIngredient(ingredients = null) {
if (ingredients.length) {
const newIngredients = ingredients.map(x => {
const newIngredients = ingredients.map((x) => {
return {
title: null,
note: x,

View File

@ -61,8 +61,8 @@
</template>
<script>
import RecipeTimeCard from "@/components/Recipe/RecipeTimeCard.vue";
import VueMarkdown from "@adapttive/vue-markdown";
import RecipeTimeCard from "./RecipeTimeCard.vue";
export default {
components: {
RecipeTimeCard,

View File

@ -48,7 +48,7 @@ export default {
},
computed: {
loggedIn() {
return this.$store.getters.getIsLoggedIn;
return this.$auth.loggedIn;
},
},
mounted() {

View File

@ -13,16 +13,25 @@
<script>
export default {
props: {
prepTime: String,
totalTime: String,
performTime: String,
prepTime: {
type: String,
default: null,
},
totalTime: {
type: String,
default: null,
},
performTime: {
type: String,
default: null,
},
},
computed: {
showCards() {
return [this.prepTime, this.totalTime, this.performTime].some(x => !this.isEmpty(x));
return [this.prepTime, this.totalTime, this.performTime].some((x) => !this.isEmpty(x));
},
allTimes() {
return [this.validateTotalTime, this.validatePrepTime, this.validatePerformTime].filter(x => x !== null);
return [this.validateTotalTime, this.validatePrepTime, this.validatePerformTime].filter((x) => x !== null);
},
validateTotalTime() {
return !this.isEmpty(this.totalTime) ? { name: this.$t("recipe.total-time"), value: this.totalTime } : null;

View File

@ -7,9 +7,9 @@
:submit-text="$t('general.create')"
:loading="processing"
width="600px"
@submit="uploadZip"
@submit="createOnByUrl"
>
<v-form ref="urlForm" @submit.prevent="createRecipe">
<v-form ref="domImportFromUrlForm" @submit.prevent="createOnByUrl">
<v-card-text>
<v-text-field
v-model="recipeURL"
@ -19,7 +19,7 @@
filled
rounded
class="rounded-lg"
:rules="[isValidWebUrl]"
:rules="[validators.url]"
:hint="$t('new-recipe.url-form-hint')"
persistent-hint
></v-text-field>
@ -84,7 +84,7 @@
</v-card-text>
<v-card-actions>
<!-- <TheUploadBtn class="mx-auto" :text-btn="false" :post="false" @uploaded="setFile"> </TheUploadBtn> -->
<!-- <AppButtonUpload class="mx-auto" :text-btn="false" :post="false" @uploaded="setFile"> </AppButtonUpload> -->
</v-card-actions>
</BaseDialog>
<BaseDialog
@ -137,10 +137,11 @@
<script lang="ts">
// import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn.vue";
// import AppButtonUpload from "@/components/UI/Buttons/AppButtonUpload.vue";
import { defineComponent, ref } from "@nuxtjs/composition-api";
import { fieldTypes } from "~/composables/forms";
import { useApi } from "~/composables/use-api";
import { useApiSingleton } from "~/composables/use-api";
import { validators } from "~/composables/use-validators";
export default defineComponent({
props: {
@ -151,12 +152,26 @@ export default defineComponent({
},
setup() {
const domCreateDialog = ref(null);
const domCreateForm = ref<VForm | null>(null);
const domUploadZipDialog = ref(null);
const domUploadZipForm = ref<VForm | null>(null);
const domImportFromUrlDialog = ref(null);
const domImportFromUrlForm = ref<VForm | null>(null);
const api = useApi();
const api = useApiSingleton();
return { domCreateDialog, domUploadZipDialog, domImportFromUrlDialog, api };
return {
domCreateDialog,
domCreateForm,
domUploadZipDialog,
domUploadZipForm,
domImportFromUrlDialog,
domImportFromUrlForm,
api,
validators,
};
},
data() {
return {
@ -204,43 +219,46 @@ export default defineComponent({
mounted() {
if (this.$route.query.recipe_import_url) {
this.addRecipe = true;
this.createRecipe();
this.createOnByUrl();
}
},
methods: {
async manualCreateRecipe() {
console.log(this.createRecipeData.form);
await this.api.recipes.createOne(this.createRecipeData.form.name);
reset() {
this.fab = false;
this.error = false;
this.addRecipe = false;
this.recipeURL = "";
this.processing = false;
},
resetVars() {
this.uploadData = {
fileName: "archive",
file: null,
};
},
setFile(file) {
setFile(file: any) {
this.uploadData.file = file;
console.log("Uploaded");
},
openZipUploader() {
this.resetVars();
this.$refs.uploadZipDialog.open();
},
async uploadZip() {
const formData = new FormData();
formData.append(this.uploadData.fileName, this.uploadData.file);
const response = await api.utils.uploadFile("/api/recipes/create-from-zip", formData);
const response = await this.api.utils.uploadFile("/api/recipes/create-from-zip", formData);
this.$router.push(`/recipe/${response.data.slug}`);
},
async createRecipe() {
async manualCreateRecipe() {
await this.api.recipes.createOne(this.createRecipeData.form.name);
},
async createOnByUrl() {
this.error = false;
if (this.$refs.urlForm === undefined || this.$refs.urlForm.validate()) {
console.log(this.domImportFromUrlForm?.validate());
if (this.domImportFromUrlForm?.validate()) {
this.processing = true;
const response = await api.recipes.createByURL(this.recipeURL);
const response = await this.api.recipes.createOneByUrl(this.recipeURL);
this.processing = false;
if (response) {
this.addRecipe = false;
@ -251,18 +269,6 @@ export default defineComponent({
}
}
},
reset() {
this.fab = false;
this.error = false;
this.addRecipe = false;
this.recipeURL = "";
this.processing = false;
},
isValidWebUrl(url: string) {
const regEx =
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,256}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/gm;
return regEx.test(url) ? true : this.$t("new-recipe.must-be-a-valid-url");
},
},
});
</script>

View File

@ -0,0 +1,71 @@
<template>
<v-tooltip
ref="copyToolTip"
v-model="show"
color="success lighten-1"
top
:open-on-hover="false"
:open-on-click="true"
close-delay="500"
transition="slide-y-transition"
>
<template #activator="{ on }">
<v-btn
icon
:color="color"
retain-focus-on-click
@click="
on.click;
textToClipboard();
"
@blur="on.blur"
>
<v-icon>{{ $globals.icons.contentCopy }}</v-icon>
</v-btn>
</template>
<span>
<v-icon left dark>
{{ $globals.icons.clipboardCheck }}
</v-icon>
<slot> {{ $t("general.copied") }}! </slot>
</span>
</v-tooltip>
</template>
<script>
export default {
props: {
copyText: {
default: "Default Copy Text",
},
color: {
default: "primary",
},
},
data() {
return {
show: false,
};
},
methods: {
toggleBlur() {
this.$refs.copyToolTip.deactivate();
},
textToClipboard() {
this.show = true;
const copyText = this.copyText;
navigator.clipboard.writeText(copyText).then(
() => console.log("Copied", copyText),
() => console.log("Copied Failed", copyText)
);
setTimeout(() => {
this.toggleBlur();
}, 500);
},
},
};
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,89 @@
<template>
<v-form ref="file">
<input ref="uploader" class="d-none" type="file" @change="onFileChanged" />
<slot v-bind="{ isSelecting, onButtonClick }">
<v-btn :loading="isSelecting" :small="small" color="accent" :text="textBtn" @click="onButtonClick">
<v-icon left> {{ effIcon }}</v-icon>
{{ text ? text : defaultText }}
</v-btn>
</slot>
</v-form>
</template>
<script>
import { api } from "@/api";
const UPLOAD_EVENT = "uploaded";
export default {
props: {
small: {
default: false,
},
post: {
type: Boolean,
default: true,
},
url: String,
text: String,
icon: { default: null },
fileName: { default: "archive" },
textBtn: {
default: true,
},
},
data: () => ({
file: null,
isSelecting: false,
}),
computed: {
effIcon() {
return this.icon ? this.icon : this.$globals.icons.upload;
},
defaultText() {
return this.$t("general.upload");
},
},
methods: {
async upload() {
if (this.file != null) {
this.isSelecting = true;
if (!this.post) {
this.$emit(UPLOAD_EVENT, this.file);
this.isSelecting = false;
return;
}
const formData = new FormData();
formData.append(this.fileName, this.file);
const response = await api.utils.uploadFile(this.url, formData);
if (response) {
this.$emit(UPLOAD_EVENT, response);
}
this.isSelecting = false;
}
},
onButtonClick() {
this.isSelecting = true;
window.addEventListener(
"focus",
() => {
this.isSelecting = false;
},
{ once: true }
);
this.$refs.uploader.click();
},
onFileChanged(e) {
this.file = e.target.files[0];
this.upload();
},
},
};
</script>
<style></style>

View File

@ -1,6 +1,6 @@
<template>
<v-btn
:color="btnAttrs.color"
:color="color || btnAttrs.color"
:small="small"
:x-small="xSmall"
:loading="loading"
@ -76,6 +76,10 @@ export default {
type: String,
default: null,
},
color: {
type: String,
default: null,
},
},
data() {
return {

View File

@ -32,7 +32,7 @@
<v-spacer></v-spacer>
<BaseButton v-if="$listeners.delete" delete secondary @click="deleteEvent" />
<BaseButton v-if="$listeners.confirm" :color="color" type="submit" @click="submitEvent">
<BaseButton v-if="$listeners.confirm" :color="color" type="submit" @click="$emit('confirm')">
{{ $t("general.confirm") }}
</BaseButton>
<BaseButton v-else-if="$listeners.submit" type="submit" @click="submitEvent">

View File

@ -56,7 +56,7 @@ function getRequests(axoisInstance: NuxtAxiosInstance): ApiRequestInstance {
export const useApi = function (): Api {
export const useApiSingleton = function (): Api {
const { $axios } = useContext();
const requests = getRequests($axios);

View File

@ -0,0 +1,38 @@
import { useAsync, ref } from "@nuxtjs/composition-api";
import { useApiSingleton } from "~/composables/use-api";
import { Recipe } from "~/types/api-types/recipe";
export const useRecipeContext = function () {
const api = useApiSingleton();
const loading = ref(false)
function getBySlug(slug: string) {
loading.value = true
const recipe = useAsync(async () => {
const { data } = await api.recipes.getOne(slug);
return data;
}, slug);
loading.value = false
return recipe;
}
async function deleteRecipe(slug: string) {
loading.value = true
const { data } = await api.recipes.deleteOne(slug);
loading.value = false
return data;
}
async function updateRecipe(slug: string, recipe: Recipe) {
loading.value = true
const { data } = await api.recipes.updateOne(slug, recipe);
loading.value = false
return data;
}
return {loading, getBySlug, deleteRecipe, updateRecipe}
};

View File

@ -1,8 +1,11 @@
const EMAIL_REGEX =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(([[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
const URL_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,256}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/gm;
export const validators = {
required: (v: string) => !!v || "This Field is Required",
email: (v: string) => !v || EMAIL_REGEX.test(v) || "Email Must Be Valid",
whitespace: (v: string) => !v || v.split(" ").length <= 1 || "No Whitespace Allowed"
whitespace: (v: string) => !v || v.split(" ").length <= 1 || "No Whitespace Allowed",
url: (v: string) => !v || URL_REGEX.test(v) || "Must Be A Valid URL",
}

View File

@ -28,7 +28,7 @@ import AppFloatingButton from "@/components/Layout/AppFloatingButton.vue";
export default defineComponent({
components: { AppHeader, AppSidebar, AppFloatingButton },
// @ts-ignore
middleware: process.env.PUBLIC_SITE ? null : "auth",
middleware: process.env.GLOBAL_MIDDLEWARE,
setup() {
return {};
},

View File

@ -12,6 +12,10 @@ export default {
link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.ico" }],
},
router: {
base: process.env.SUB_PATH || "",
},
layoutTransition: {
name: "layout",
mode: "out-in",
@ -36,9 +40,13 @@ export default {
"@nuxtjs/composition-api/module",
],
publicRuntimeConfig: {
GLOBAL_MIDDLEWARE: process.env.GLOBAL_MIDDLEWARE || null,
ALLOW_SIGNUP: process.env.ALLOW_SIGNUP || true,
},
env: {
PUBLIC_SITE: process.env.PUBLIC_SITE || true,
BASE_URL: process.env.BASE_URL || "",
GLOBAL_MIDDLEWARE: process.env.GLOBAL_MIDDLEWARE || null,
ALLOW_SIGNUP: process.env.ALLOW_SIGNUP || true,
},

View File

@ -7,13 +7,13 @@
<script lang="ts">
import { defineComponent, onMounted, ref } from "@nuxtjs/composition-api";
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
import { useApi } from "~/composables/use-api";
import { useApiSingleton } from "~/composables/use-api";
import { Recipe } from "~/types/api-types/admin";
export default defineComponent({
components: { RecipeCardSection },
setup() {
const api = useApi();
const api = useApiSingleton();
const recipes = ref<Recipe[] | null>([]);
onMounted(async () => {

View File

@ -1,16 +1,265 @@
<template>
<div></div>
<v-container>
<v-card v-if="skeleton" :color="`white ${false ? 'darken-2' : 'lighten-4'}`" class="pa-3">
<v-skeleton-loader class="mx-auto" height="700px" type="card"></v-skeleton-loader>
</v-card>
<v-card v-else-if="recipe">
<v-img
:key="imageKey"
:height="hideImage ? '50' : imageHeight"
:src="api.recipes.recipeImage(recipe.slug)"
class="d-print-none"
@error="hideImage = true"
>
<RecipeTimeCard
:class="true ? undefined : 'force-bottom'"
:prep-time="recipe.prepTime"
:total-time="recipe.totalTime"
:perform-time="recipe.performTime"
/>
</v-img>
<RecipeActionMenu
v-model="form"
:slug="recipe.slug"
:name="recipe.name"
:logged-in="$auth.loggedIn"
:open="form"
class="ml-auto"
@close="form = false"
@json="jsonEditor = !jsonEditor"
@edit="
jsonEditor = false;
form = true;
"
@save="updateRecipe(recipe.slug, recipe)"
@delete="deleteRecipe(recipe.slug)"
/>
<div>
<v-card-text>
<div v-if="form" class="d-flex justify-start align-center">
<RecipeImageUploadBtn class="my-1" :slug="recipe.slug" @upload="uploadImage" @refresh="$emit('upload')" />
<RecipeSettingsMenu class="my-1 mx-1" :value="recipe.settings" @upload="null" />
</div>
<!-- Recipe Title Section -->
<template v-if="!form">
<v-card-title class="pa-0 ma-0 headline">
{{ recipe.name }}
</v-card-title>
<VueMarkdown :source="recipe.description"> </VueMarkdown>
</template>
<template v-else>
<v-text-field
v-model="recipe.name"
class="my-3"
:label="$t('recipe.recipe-name')"
:rules="[validators.required]"
>
</v-text-field>
<div class="d-flex flex-wrap">
<v-text-field v-model="recipe.totalTime" class="mx-2" :label="$t('recipe.total-time')"></v-text-field>
<v-text-field v-model="recipe.prepTime" class="mx-2" :label="$t('recipe.prep-time')"></v-text-field>
<v-text-field v-model="recipe.performTime" class="mx-2" :label="$t('recipe.perform-time')"></v-text-field>
</div>
<v-textarea v-model="recipe.description" auto-grow min-height="100" :label="$t('recipe.description')">
</v-textarea>
</template>
<div class="d-flex justify-space-between align-center">
<v-btn
v-if="recipe.recipeYield"
dense
small
:hover="false"
type="label"
:ripple="false"
elevation="0"
color="secondary darken-1"
class="rounded-sm static"
>
{{ recipe.recipeYield }}
</v-btn>
<RecipeRating :key="recipe.slug" :value="recipe.rating" :name="recipe.name" :slug="recipe.slug" />
</div>
<v-row>
<v-col cols="12" sm="12" md="4" lg="4">
<RecipeIngredients :value="recipe.recipeIngredient" :edit="form" />
<div v-if="$vuetify.breakpoint.mdAndUp">
<v-card v-if="recipe.recipeCategory.length > 0" class="mt-2">
<v-card-title class="py-2">
{{ $t("recipe.categories") }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text>
<RecipeChips :items="recipe.recipeCategory" />
</v-card-text>
</v-card>
<v-card v-if="recipe.tags.length > 0" class="mt-2">
<v-card-title class="py-2">
{{ $t("tag.tags") }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text>
<RecipeChips :items="recipe.tags" :is-category="false" />
</v-card-text>
</v-card>
<RecipeNutrition v-if="true || form" v-model="recipe.nutrition" class="mt-10" :edit="form" />
<RecipeAssets
v-if="recipe.settings.showAssets || form"
v-model="recipe.assets"
:edit="form"
:slug="recipe.slug"
/>
</div>
</v-col>
<v-divider v-if="$vuetify.breakpoint.mdAndUp" class="my-divider" :vertical="true"></v-divider>
<v-col cols="12" sm="12" md="8" lg="8">
<RecipeInstructions v-model="recipe.recipeInstructions" :edit="form" />
<RecipeNotes v-model="recipe.notes" :edit="form" />
</v-col>
</v-row>
</v-card-text>
</div>
</v-card>
</v-container>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
<script lang="ts">
import { defineComponent, ref, useRoute, useRouter } from "@nuxtjs/composition-api";
// @ts-ignore
import VueMarkdown from "@adapttive/vue-markdown";
import { useApiSingleton } from "~/composables/use-api";
import { validators } from "~/composables/use-validators";
import { useRecipeContext } from "~/composables/use-recipe-context";
import RecipeActionMenu from "~/components/Domain/Recipe/RecipeActionMenu.vue";
import RecipeChips from "~/components/Domain/Recipe/RecipeChips.vue";
import RecipeIngredients from "~/components/Domain/Recipe/RecipeIngredients.vue";
import RecipeRating from "~/components/Domain/Recipe/RecipeRating.vue";
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
import RecipeAssets from "~/components/Domain/Recipe/RecipeAssets.vue";
import RecipeNutrition from "~/components/Domain/Recipe/RecipeNutrition.vue";
import RecipeInstructions from "~/components/Domain/Recipe/RecipeInstructions.vue";
import RecipeNotes from "~/components/Domain/Recipe/RecipeNotes.vue";
import RecipeImageUploadBtn from "~/components/Domain/Recipe/RecipeImageUploadBtn.vue";
import RecipeSettingsMenu from "~/components/Domain/Recipe/RecipeSettingsMenu.vue";
import { Recipe } from "~/types/api-types/admin";
export default defineComponent({
components: {
RecipeActionMenu,
RecipeAssets,
RecipeChips,
RecipeIngredients,
RecipeInstructions,
RecipeNotes,
RecipeNutrition,
RecipeRating,
RecipeTimeCard,
RecipeImageUploadBtn,
RecipeSettingsMenu,
VueMarkdown,
},
setup() {
return {}
}
})
const route = useRoute();
const router = useRouter();
const slug = route.value.params.slug;
const api = useApiSingleton();
const { getBySlug, loading } = useRecipeContext();
const recipe = getBySlug(slug);
const form = ref<boolean>(false);
async function updateRecipe(slug: string, recipe: Recipe) {
const { data } = await api.recipes.updateOne(slug, recipe);
form.value = false;
if (data?.slug) {
router.push("/recipe/" + data.slug);
}
}
async function deleteRecipe(slug: string) {
const { data } = await api.recipes.deleteOne(slug);
if (data?.slug) {
router.push("/");
}
}
return {
recipe,
api,
form,
loading,
deleteRecipe,
updateRecipe,
validators,
};
},
data() {
return {
imageKey: 1,
hideImage: false,
loadFailed: false,
skeleton: false,
jsonEditor: false,
jsonEditorOptions: {
mode: "code",
search: false,
mainMenuBar: false,
},
};
},
computed: {
imageHeight() {
// @ts-ignore
return this.$vuetify.breakpoint.xs ? "200" : "400";
},
},
methods: {
printPage() {
window.print();
},
// validateRecipe() {
// if (this.jsonEditor) {
// return true;
// } else {
// return this.$refs.recipeEditor.validateRecipe();
// }
// },
// async saveImage(overrideSuccessMsg = false) {
// if (this.fileObject) {
// const newVersion = await api.recipes.updateImage(this.recipeDetails.slug, this.fileObject, overrideSuccessMsg);
// if (newVersion) {
// this.recipeDetails.image = newVersion.data.version;
// this.imageKey += 1;
// }
// }
// },
// async saveRecipe() {
// if (this.validateRecipe()) {
// const slug = await this.api.recipes.updateOne(this.recipeDetails);
// if (!slug) return;
// if (this.fileObject) {
// this.saveImage(true);
// }
// this.form = false;
// if (slug !== this.recipe.slug) {
// this.$router.push(`/recipe/${slug}`);
// }
// window.URL.revokeObjectURL(this.api.recipes.(this.recipe.slug));
// }
// },
},
});
</script>
<style scoped>
</style>
<style scoped></style>

View File

@ -1,16 +1,38 @@
<template>
<div></div>
</template>
<v-container>
<RecipeCardSection
:icon="$globals.icons.primary"
:title="$t('page.all-recipes')"
:recipes="recipes"
@sort="assignSorted"
></RecipeCardSection>
</v-container>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
export default defineComponent({
setup() {
return {}
}
})
</script>
<style scoped>
</style>
import { defineComponent, onMounted, ref } from "@nuxtjs/composition-api";
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
import { useApiSingleton } from "~/composables/use-api";
import { Recipe } from "~/types/api-types/admin";
export default defineComponent({
components: { RecipeCardSection },
setup() {
const api = useApiSingleton();
const recipes = ref<Recipe[] | null>([]);
onMounted(async () => {
const { data } = await api.recipes.getAll();
recipes.value = data;
});
return { api, recipes };
},
methods: {
assignSorted(val: Array<Recipe>) {
this.recipes = val;
},
},
});
</script>

View File

@ -178,7 +178,7 @@
</v-btn>
</v-form>
</v-card-text>
<v-btn v-if="allowSignup" class="mx-auto" text to="/user/sign-up"> Sign Up </v-btn>
<v-btn v-if="$config.ALLOW_SIGNUP" class="mx-auto" text to="/user/sign-up"> Sign Up </v-btn>
<v-btn v-else class="mx-auto" text disabled> Invite Only </v-btn>
</v-card>
<!-- <v-col class="fill-height"> </v-col> -->

View File

@ -20,6 +20,7 @@ import {
mdiEmail,
mdiLock,
mdiEye,
mdiDrag,
mdiEyeOff,
mdiCalendarMinus,
mdiCalendar,
@ -38,7 +39,6 @@ import {
mdiFilePdfBox,
mdiFileImage,
mdiCodeJson,
mdiArrowUpDown,
mdiCog,
mdiSort,
mdiOrderAlphabeticalAscending,
@ -103,7 +103,7 @@ const icons = {
alertCircle: mdiAlertCircle,
api: mdiApi,
arrowLeftBold: mdiArrowLeftBold,
arrowUpDown: mdiArrowUpDown,
arrowUpDown: mdiDrag,
backupRestore: mdiBackupRestore,
bellAlert: mdiBellAlert,
broom: mdiBroom,

3
frontend/template.env Normal file
View File

@ -0,0 +1,3 @@
GLOBAL_MIDDLEWARE=null # null or 'auth'
BASE_URL = ""
ALLOW_SIGNUP=true

View File

@ -62,8 +62,8 @@ export interface RecipeCategoryResponse {
}
export interface Recipe {
id?: number;
name?: string;
slug?: string;
name: string;
slug: string;
image?: unknown;
description?: string;
recipeCategory?: string[];

View File

@ -49,8 +49,8 @@ export interface Nutrition {
}
export interface Recipe {
id?: number;
name?: string;
slug?: string;
name: string;
slug: string;
image?: unknown;
description?: string;
recipeCategory?: string[];

View File

@ -7,10 +7,10 @@ interface RequestResponse<T> {
}
export interface ApiRequestInstance {
get<T>(url: string, data?: object): Promise<RequestResponse<T>>;
post<T>(url: string, data: object): Promise<RequestResponse<T>>;
put<T>(url: string, data: object): Promise<RequestResponse<T>>;
patch<T>(url: string, data: object): Promise<RequestResponse<T>>;
delete<T>(url: string, data: object): Promise<RequestResponse<T>>;
get<T>(url: string, data?: T | object): Promise<RequestResponse<T>>;
post<T>(url: string, data: T | object): Promise<RequestResponse<T>>;
put<T>(url: string, data: T | object): Promise<RequestResponse<T>>;
patch<T>(url: string, data: T | object): Promise<RequestResponse<T>>;
delete<T>(url: string, data?: T | object): Promise<RequestResponse<T>>;
}

8
frontend/types/ts-shim.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
declare module "*.vue" {
import Vue from "vue"
export default Vue
}
interface VForm extends HTMLFormElement {
validate(): boolean;
}