mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-24 01:12:54 -04:00
feat(frontend): 🚧 CRUD Functionality
This commit is contained in:
parent
00a8fdda41
commit
afcad2f701
@ -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");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -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 {
|
||||
|
@ -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() {
|
||||
|
@ -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],
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
@ -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>
|
@ -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);
|
@ -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>
|
||||
|
||||
|
@ -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 };
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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");
|
||||
},
|
||||
|
@ -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,
|
||||
},
|
@ -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,
|
@ -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,
|
@ -48,7 +48,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
loggedIn() {
|
||||
return this.$store.getters.getIsLoggedIn;
|
||||
return this.$auth.loggedIn;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
@ -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;
|
@ -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>
|
||||
|
71
frontend/components/global/AppButtonCopy.vue
Normal file
71
frontend/components/global/AppButtonCopy.vue
Normal 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>
|
89
frontend/components/global/AppButtonUpload.vue
Normal file
89
frontend/components/global/AppButtonUpload.vue
Normal 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>
|
@ -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 {
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
|
||||
|
38
frontend/composables/use-recipe-context.ts
Normal file
38
frontend/composables/use-recipe-context.ts
Normal 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}
|
||||
};
|
@ -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",
|
||||
}
|
||||
|
@ -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 {};
|
||||
},
|
||||
|
@ -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,
|
||||
},
|
||||
|
||||
|
@ -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 () => {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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> -->
|
||||
|
@ -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
3
frontend/template.env
Normal file
@ -0,0 +1,3 @@
|
||||
GLOBAL_MIDDLEWARE=null # null or 'auth'
|
||||
BASE_URL = ""
|
||||
ALLOW_SIGNUP=true
|
@ -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[];
|
||||
|
@ -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[];
|
||||
|
@ -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
8
frontend/types/ts-shim.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
declare module "*.vue" {
|
||||
import Vue from "vue"
|
||||
export default Vue
|
||||
}
|
||||
|
||||
interface VForm extends HTMLFormElement {
|
||||
validate(): boolean;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user