Use composition API for more components, enable more type checking (#914)

* Activate more linting rules from eslint and typescript

* Properly add VForm as type information

* Fix usage of native types

* Fix more linting issues

* Rename vuetify types file, add VTooltip

* Fix some more typing problems

* Use composition API for more components

* Convert RecipeRating

* Convert RecipeNutrition

* Convert more components to composition API

* Fix globals plugin for type checking

* Add missing icon types

* Fix vuetify types in Nuxt context

* Use composition API for RecipeActionMenu

* Convert error.vue to composition API

* Convert RecipeContextMenu to composition API

* Use more composition API and type checking in recipe/create

* Convert AppButtonUpload to composition API

* Fix some type checking in RecipeContextMenu

* Remove unused components BaseAutoForm and BaseColorPicker

* Convert RecipeCategoryTagDialog to composition API

* Convert RecipeCardSection to composition API

* Convert RecipeCategoryTagSelector to composition API

* Properly import vuetify type definitions

* Convert BaseButton to composition API

* Convert AutoForm to composition API

* Remove unused requests API file

* Remove static routes from recipe API

* Fix more type errors

* Convert AppHeader to composition API, fixing some search bar focus problems

* Convert RecipeDialogSearch to composition API

* Update API types from pydantic models, handle undefined values

* Improve more typing problems

* Add types to other plugins

* Properly type the CRUD API access

* Fix typing of static image routes

* Fix more typing stuff

* Fix some more typing problems

* Turn off more rules
This commit is contained in:
Philipp Fischbeck 2022-01-09 07:15:23 +01:00 committed by GitHub
parent d5ab5ec66f
commit 86c99b10a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
114 changed files with 2218 additions and 2033 deletions

View File

@ -2,7 +2,7 @@ from pathlib import Path
from jinja2 import Template from jinja2 import Template
template = """// This Code is auto generated by gen_global_componenets.py template = """// This Code is auto generated by gen_global_components.py
{% for name in global %} import {{ name }} from "@/components/global/{{ name }}.vue"; {% for name in global %} import {{ name }} from "@/components/global/{{ name }}.vue";
{% endfor %} {% endfor %}
{% for name in layout %} import {{ name }} from "@/components/layout/{{ name }}.vue"; {% for name in layout %} import {{ name }} from "@/components/layout/{{ name }}.vue";

View File

@ -5,7 +5,7 @@ from pydantic2ts import generate_typescript_defs
CWD = Path(__file__).parent CWD = Path(__file__).parent
PROJECT_DIR = Path(__file__).parent.parent.parent PROJECT_DIR = Path(__file__).parent.parent.parent
SCHEMA_PATH = Path("/Users/hayden/Projects/Vue/mealie/mealie/schema/") SCHEMA_PATH = PROJECT_DIR / "mealie" / "schema"
TYPES_DIR = CWD / "output" / "types" / "api-types" TYPES_DIR = CWD / "output" / "types" / "api-types"

View File

@ -4,11 +4,26 @@ module.exports = {
browser: true, browser: true,
node: true, node: true,
}, },
parser: "vue-eslint-parser",
parserOptions: { parserOptions: {
parser: "@typescript-eslint/parser", parser: "@typescript-eslint/parser",
requireConfigFile: false, requireConfigFile: false,
tsConfigRootDir: __dirname,
project: ["./tsconfig.json"],
extraFileExtensions: [".vue"],
}, },
extends: ["@nuxtjs/eslint-config-typescript", "plugin:nuxt/recommended", "prettier"], extends: [
"@nuxtjs/eslint-config-typescript",
"plugin:nuxt/recommended",
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
// "plugin:prettier/recommended",
"prettier",
],
// Re-add once we use nuxt bridge
// See https://v3.nuxtjs.org/getting-started/bridge#update-nuxtconfig
ignorePatterns: ["nuxt.config.js"],
plugins: ["prettier"], plugins: ["prettier"],
// add your custom rules here // add your custom rules here
rules: { rules: {
@ -27,5 +42,13 @@ module.exports = {
allowModifiers: true, allowModifiers: true,
}, },
], ],
// TODO Gradually activate all rules
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-explicit-any": "off",
}, },
}; };

View File

@ -10,16 +10,6 @@ export interface CrudAPIInterface {
// Methods // Methods
} }
export interface CrudAPIMethodsInterface {
// CRUD Methods
getAll(): any;
createOne(): any;
getOne(): any;
updateOne(): any;
patchOne(): any;
deleteOne(): any;
}
export abstract class BaseAPI { export abstract class BaseAPI {
requests: ApiRequestInstance; requests: ApiRequestInstance;
@ -50,8 +40,8 @@ export abstract class BaseCRUDAPI<T, U> extends BaseAPI implements CrudAPIInterf
return await this.requests.put<T>(this.itemRoute(itemId), payload); return await this.requests.put<T>(this.itemRoute(itemId), payload);
} }
async patchOne(itemId: string, payload: T) { async patchOne(itemId: string, payload: Partial<T>) {
return await this.requests.patch(this.itemRoute(itemId), payload); return await this.requests.patch<T>(this.itemRoute(itemId), payload);
} }
async deleteOne(itemId: string | number) { async deleteOne(itemId: string | number) {

View File

@ -49,6 +49,7 @@ export class AdminGroupsApi extends BaseCRUDAPI<GroupRead, GroupCreate> {
itemRoute = routes.adminUsersId; itemRoute = routes.adminUsersId;
async updateOne(id: number, payload: AdminGroupUpdate) { async updateOne(id: number, payload: AdminGroupUpdate) {
return await this.requests.put<GroupRead>(this.itemRoute(id), payload); // TODO: This should probably be a patch request, which isn't offered by the API currently
return await this.requests.put<GroupRead, AdminGroupUpdate>(this.itemRoute(id), payload);
} }
} }

View File

@ -59,10 +59,10 @@ export interface Invitation {
} }
export interface SetPermissions { export interface SetPermissions {
userId: number; userId: string;
canInvite: boolean; canInvite?: boolean;
canManage: boolean; canManage?: boolean;
canOrganize: boolean; canOrganize?: boolean;
} }
export class GroupAPI extends BaseCRUDAPI<GroupInDB, CreateGroup> { export class GroupAPI extends BaseCRUDAPI<GroupInDB, CreateGroup> {
@ -87,7 +87,8 @@ export class GroupAPI extends BaseCRUDAPI<GroupInDB, CreateGroup> {
} }
async setPreferences(payload: UpdatePreferences) { async setPreferences(payload: UpdatePreferences) {
return await this.requests.put<Preferences>(routes.preferences, payload); // TODO: This should probably be a patch request, which isn't offered by the API currently
return await this.requests.put<Preferences, UpdatePreferences>(routes.preferences, payload);
} }
async createInvitation(payload: CreateInvitation) { async createInvitation(payload: CreateInvitation) {
@ -99,6 +100,7 @@ export class GroupAPI extends BaseCRUDAPI<GroupInDB, CreateGroup> {
} }
async setMemberPermissions(payload: SetPermissions) { async setMemberPermissions(payload: SetPermissions) {
return await this.requests.put<UserOut>(routes.permissions, payload); // TODO: This should probably be a patch request, which isn't offered by the API currently
return await this.requests.put<Permissions, SetPermissions>(routes.permissions, payload);
} }
} }

View File

@ -6,6 +6,7 @@ interface BasePayload {
type exportType = "json"; type exportType = "json";
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface RecipeBulkDelete extends BasePayload {} interface RecipeBulkDelete extends BasePayload {}
interface RecipeBulkExport extends BasePayload { interface RecipeBulkExport extends BasePayload {

View File

@ -80,30 +80,13 @@ export class RecipeAPI extends BaseCRUDAPI<Recipe, CreateRecipe> {
} }
async createOneByUrl(url: string) { async createOneByUrl(url: string) {
return await this.requests.post(routes.recipesCreateUrl, { url }); return await this.requests.post<string>(routes.recipesCreateUrl, { url });
} }
async createManyByUrl(payload: BulkCreatePayload) { async createManyByUrl(payload: BulkCreatePayload) {
return await this.requests.post(routes.recipesCreateUrlBulk, payload); return await this.requests.post(routes.recipesCreateUrlBulk, payload);
} }
// 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}`;
}
recipeSmallImage(recipeSlug: string, version = null, key = null) {
return `/api/media/recipes/${recipeSlug}/images/min-original.webp?&rnd=${key}&version=${version}`;
}
recipeTinyImage(recipeSlug: string, version = null, key = null) {
return `/api/media/recipes/${recipeSlug}/images/tiny-original.webp?&rnd=${key}&version=${version}`;
}
recipeAssetPath(recipeSlug: string, assetName: string) {
return `/api/media/recipes/${recipeSlug}/assets/${assetName}`;
}
async parseIngredients(parser: Parser, ingredients: Array<string>) { async parseIngredients(parser: Parser, ingredients: Array<string>) {
parser = parser || "nlp"; parser = parser || "nlp";
return await this.requests.post<ParsedIngredient[]>(routes.recipesParseIngredients, { parser, ingredients }); return await this.requests.post<ParsedIngredient[]>(routes.recipesParseIngredients, { parser, ingredients });

View File

@ -1,5 +1,6 @@
import { Category } from "../categories"; import { Category } from "../categories";
import { Tag } from "../tags"; import { Tag } from "../tags";
import { CreateIngredientFood, CreateIngredientUnit, IngredientFood, IngredientUnit } from "~/types/api-types/recipe";
export type Parser = "nlp" | "brute"; export type Parser = "nlp" | "brute";
@ -12,26 +13,14 @@ export interface Confidence {
food?: number; food?: number;
} }
export interface Unit {
name: string;
description: string;
fraction: boolean;
abbreviation: string;
}
export interface Food {
name: string;
description?: string;
}
export interface Ingredient { export interface Ingredient {
referenceId: string; title?: string;
title: string; note?: string;
note: string; unit?: IngredientUnit | CreateIngredientUnit;
unit: Unit | null; food?: IngredientFood | CreateIngredientFood;
food: Food | null; disableAmount?: boolean;
disableAmount: boolean; quantity?: number;
quantity: number; referenceId?: string;
} }
export interface ParsedIngredient { export interface ParsedIngredient {

View File

@ -2,6 +2,6 @@ import { BaseAPI } from "../_base";
export class UploadFile extends BaseAPI { export class UploadFile extends BaseAPI {
file(url: string, fileObject: any) { file(url: string, fileObject: any) {
return this.requests.post(url, fileObject); return this.requests.post<string>(url, fileObject);
} }
} }

View File

@ -11,10 +11,10 @@ export class UtilsAPI extends BaseAPI {
} }
// @ts-ignore // @ts-ignore
const token: String = response.data.fileToken; const token: string = response.data.fileToken;
const tokenURL = prefix + "/utils/download?token=" + token; const tokenURL = prefix + "/utils/download?token=" + token;
window.open(tokenURL, "_blank"); window.open(tokenURL, "_blank");
return await response; return response;
} }
} }

View File

@ -1,47 +0,0 @@
import axios, { AxiosResponse } from "axios";
interface RequestResponse<T> {
response: AxiosResponse<T> | null;
data: T | null;
error: any;
}
const request = {
async safe<T>(funcCall: any, url: string, data: object = {}): Promise<RequestResponse<T>> {
const response = await funcCall(url, data).catch(function (error: object) {
console.log(error);
// Insert Generic Error Handling Here
return { response: null, error, data: null };
});
return { response, error: null, data: response.data };
},
};
export const requests = {
async get<T>(url: string, params = {}): Promise<RequestResponse<T>> {
let error = null;
const response = await axios.get<T>(url, { ...params }).catch((e) => {
error = e;
});
if (response != null) {
return { response, error, data: response?.data };
}
return { response: null, error, data: null };
},
async post<T>(url: string, data: object) {
return await request.safe<T>(axios.post, url, data);
},
async put<T>(url: string, data: object) {
return await request.safe<T>(axios.put, url, data);
},
async patch<T>(url: string, data: object) {
return await request.safe<T>(axios.patch, url, data);
},
async delete<T>(url: string) {
return await request.safe<T>(axios.delete, url);
},
};

View File

@ -22,50 +22,58 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import { defineComponent, onMounted, useContext } from "@nuxtjs/composition-api";
const UPDATE_EVENT = "input"; const UPDATE_EVENT = "input";
export default { export default defineComponent({
props: { props: {
importBackup: { importBackup: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}, },
data() { setup(_, context) {
return { const { i18n } = useContext();
options: {
recipes: { const options = {
value: true, recipes: {
text: this.$t("general.recipes"), value: true,
}, text: i18n.t("general.recipes"),
users: {
value: true,
text: this.$t("user.users"),
},
groups: {
value: true,
text: this.$t("group.groups"),
},
}, },
forceImport: false, users: {
}; value: true,
}, text: i18n.t("user.users"),
mounted() { },
this.emitValue(); groups: {
}, value: true,
methods: { text: i18n.t("group.groups"),
emitValue() { },
this.$emit(UPDATE_EVENT, { }
recipes: this.options.recipes.value, const forceImport = false;
function emitValue() {
context.emit(UPDATE_EVENT, {
recipes: options.recipes.value,
settings: false, settings: false,
themes: false, themes: false,
pages: false, pages: false,
users: this.options.users.value, users: options.users.value,
groups: this.options.groups.value, groups: options.groups.value,
notifications: false, notifications: false,
forceImport: this.forceImport, forceImport,
}); });
}, }
onMounted(() => {
emitValue();
});
return {
options,
forceImport,
emitValue,
};
}, },
}; });
</script> </script>

View File

@ -92,7 +92,8 @@
</v-toolbar> </v-toolbar>
</template> </template>
<script> <script lang="ts">
import {defineComponent, ref, useContext} from "@nuxtjs/composition-api";
import RecipeContextMenu from "./RecipeContextMenu.vue"; import RecipeContextMenu from "./RecipeContextMenu.vue";
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue"; import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
@ -101,7 +102,7 @@ const DELETE_EVENT = "delete";
const CLOSE_EVENT = "close"; const CLOSE_EVENT = "close";
const JSON_EVENT = "json"; const JSON_EVENT = "json";
export default { export default defineComponent({
components: { RecipeContextMenu, RecipeFavoriteBadge }, components: { RecipeContextMenu, RecipeFavoriteBadge },
props: { props: {
slug: { slug: {
@ -129,69 +130,70 @@ export default {
default: false, default: false,
}, },
}, },
data() { setup(_, context) {
return { const deleteDialog = ref(false);
deleteDialog: false,
edit: false,
};
},
computed: { const { i18n, $globals } = useContext();
editorButtons() { const editorButtons = [
return [ {
{ text: i18n.t("general.delete"),
text: this.$t("general.delete"), icon: $globals.icons.delete,
icon: this.$globals.icons.delete, event: DELETE_EVENT,
event: DELETE_EVENT, color: "error",
color: "error", },
}, {
{ text: i18n.t("general.json"),
text: this.$t("general.json"), icon: $globals.icons.codeBraces,
icon: this.$globals.icons.codeBraces, event: JSON_EVENT,
event: JSON_EVENT, color: "accent",
color: "accent", },
}, {
{ text: i18n.t("general.close"),
text: this.$t("general.close"), icon: $globals.icons.close,
icon: this.$globals.icons.close, event: CLOSE_EVENT,
event: CLOSE_EVENT, color: "",
color: "", },
}, {
{ text: i18n.t("general.save"),
text: this.$t("general.save"), icon: $globals.icons.save,
icon: this.$globals.icons.save, event: SAVE_EVENT,
event: SAVE_EVENT, color: "success",
color: "success", },
}, ];
];
}, function emitHandler(event: string) {
},
methods: {
emitHandler(event) {
switch (event) { switch (event) {
case CLOSE_EVENT: case CLOSE_EVENT:
this.$emit(CLOSE_EVENT); context.emit(CLOSE_EVENT);
this.$emit("input", false); context.emit("input", false);
break; break;
case SAVE_EVENT: case SAVE_EVENT:
this.$emit(SAVE_EVENT); context.emit(SAVE_EVENT);
break; break;
case JSON_EVENT: case JSON_EVENT:
this.$emit(JSON_EVENT); context.emit(JSON_EVENT);
break; break;
case DELETE_EVENT: case DELETE_EVENT:
this.deleteDialog = true; deleteDialog.value = true;
break; break;
default: default:
break; break;
} }
}, }
emitDelete() {
this.$emit(DELETE_EVENT); function emitDelete() {
this.$emit("input", false); context.emit(DELETE_EVENT);
}, context.emit("input", false);
}
return {
deleteDialog,
editorButtons,
emitHandler,
emitDelete,
}
}, },
}; });
</script> </script>
<style scoped> <style scoped>

View File

@ -75,7 +75,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, toRefs, useContext } from "@nuxtjs/composition-api"; import { defineComponent, reactive, toRefs, useContext } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api"; import { useStaticRoutes, useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast"; import { alert } from "~/composables/use-toast";
const BASE_URL = window.location.origin; const BASE_URL = window.location.origin;
@ -107,7 +107,6 @@ export default defineComponent({
}, },
}); });
// @ts-ignore
const { $globals, i18n } = useContext(); const { $globals, i18n } = useContext();
const iconOptions = [ const iconOptions = [
@ -142,15 +141,16 @@ export default defineComponent({
return iconOptions.find((item) => item.name === icon) || iconOptions[0]; return iconOptions.find((item) => item.name === icon) || iconOptions[0];
} }
const { recipeAssetPath } = useStaticRoutes();
function assetURL(assetName: string) { function assetURL(assetName: string) {
return api.recipes.recipeAssetPath(props.slug, assetName); return recipeAssetPath(props.slug, assetName);
} }
function assetEmbed(name: string) { function assetEmbed(name: string) {
return `<img src="${BASE_URL}${assetURL(name)}" height="100%" width="100%"> </img>`; return `<img src="${BASE_URL}${assetURL(name)}" height="100%" width="100%"> </img>`;
} }
function setFileObject(fileObject: any) { function setFileObject(fileObject: File) {
state.fileObject = fileObject; state.fileObject = fileObject;
} }

View File

@ -51,13 +51,15 @@
</v-lazy> </v-lazy>
</template> </template>
<script> <script lang="ts">
import RecipeFavoriteBadge from "./RecipeFavoriteBadge"; import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import RecipeChips from "./RecipeChips"; import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
import RecipeContextMenu from "./RecipeContextMenu"; import RecipeChips from "./RecipeChips.vue";
import RecipeCardImage from "./RecipeCardImage"; import RecipeContextMenu from "./RecipeContextMenu.vue";
import RecipeRating from "./RecipeRating"; import RecipeCardImage from "./RecipeCardImage.vue";
export default { import RecipeRating from "./RecipeRating.vue";
export default defineComponent({
components: { RecipeFavoriteBadge, RecipeChips, RecipeContextMenu, RecipeRating, RecipeCardImage }, components: { RecipeFavoriteBadge, RecipeChips, RecipeContextMenu, RecipeRating, RecipeCardImage },
props: { props: {
name: { name: {
@ -99,17 +101,17 @@ export default {
default: 200, default: 200,
}, },
}, },
data() { setup() {
const { $auth } = useContext();
const loggedIn = computed(() => {
return $auth.loggedIn;
});
return { return {
fallBackImage: false, loggedIn,
}; };
}, },
computed: { });
loggedIn() {
return this.$store.getters.getIsLoggedIn;
},
},
};
</script> </script>
<style> <style>

View File

@ -17,9 +17,11 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import {computed, defineComponent, ref, watch} from "@nuxtjs/composition-api";
import { useStaticRoutes, useUserApi } from "~/composables/api"; import { useStaticRoutes, useUserApi } from "~/composables/api";
export default {
export default defineComponent({
props: { props: {
tiny: { tiny: {
type: Boolean, type: Boolean,
@ -50,44 +52,42 @@ export default {
default: 200, default: 200,
}, },
}, },
setup() { setup(props) {
const api = useUserApi(); const api = useUserApi();
const { recipeImage, recipeSmallImage, recipeTinyImage } = useStaticRoutes(); const { recipeImage, recipeSmallImage, recipeTinyImage } = useStaticRoutes();
return { api, recipeImage, recipeSmallImage, recipeTinyImage }; const fallBackImage = ref(false);
}, const imageSize = computed(() => {
data() { if (props.tiny) return "tiny";
if (props.small) return "small";
if (props.large) return "large";
return "large";
})
watch(() => props.slug, () => {
fallBackImage.value = false;
});
function getImage(slug: string) {
switch (imageSize.value) {
case "tiny":
return recipeTinyImage(slug, props.imageVersion);
case "small":
return recipeSmallImage(slug, props.imageVersion);
case "large":
return recipeImage(slug, props.imageVersion);
}
}
return { return {
fallBackImage: false, api,
fallBackImage,
imageSize,
getImage,
}; };
}, },
computed: { });
imageSize() {
if (this.tiny) return "tiny";
if (this.small) return "small";
if (this.large) return "large";
return "large";
},
},
watch: {
slug() {
this.fallBackImage = false;
},
},
methods: {
getImage(slug) {
switch (this.imageSize) {
case "tiny":
return this.recipeTinyImage(slug, this.imageVersion);
case "small":
return this.recipeSmallImage(slug, this.imageVersion);
case "large":
return this.recipeImage(slug, this.imageVersion);
}
},
},
};
</script> </script>
<style scoped> <style scoped>

View File

@ -10,15 +10,7 @@
<v-list-item three-line> <v-list-item three-line>
<slot name="avatar"> <slot name="avatar">
<v-list-item-avatar tile size="125" class="v-mobile-img rounded-sm my-0 ml-n4"> <v-list-item-avatar tile size="125" class="v-mobile-img rounded-sm my-0 ml-n4">
<v-img <RecipeCardImage :icon-size="100" :height="125" :slug="slug" small :image-version="image"></RecipeCardImage>
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-avatar>
</slot> </slot>
<v-list-item-content> <v-list-item-content>
@ -61,15 +53,17 @@
</v-expand-transition> </v-expand-transition>
</template> </template>
<script> <script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"; import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import RecipeFavoriteBadge from "./RecipeFavoriteBadge"; import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
import RecipeContextMenu from "./RecipeContextMenu"; import RecipeContextMenu from "./RecipeContextMenu.vue";
import { useUserApi } from "~/composables/api"; import RecipeCardImage from "./RecipeCardImage.vue";
export default defineComponent({ export default defineComponent({
components: { components: {
RecipeFavoriteBadge, RecipeFavoriteBadge,
RecipeContextMenu, RecipeContextMenu,
RecipeCardImage,
}, },
props: { props: {
name: { name: {
@ -89,8 +83,9 @@ export default defineComponent({
default: 0, default: 0,
}, },
image: { image: {
type: [String, null], type: String,
default: "", required: false,
default: "abc123",
}, },
route: { route: {
type: Boolean, type: Boolean,
@ -102,24 +97,14 @@ export default defineComponent({
}, },
}, },
setup() { setup() {
const api = useUserApi(); const { $auth } = useContext();
const loggedIn = computed(() => {
return $auth.loggedIn;
});
return { api };
},
data() {
return { return {
fallBackImage: false, loggedIn,
}; }
},
computed: {
loggedIn() {
return this.$store.getters.getIsLoggedIn;
},
},
methods: {
getImage(slug) {
return this.api.recipes.recipeSmallImage(slug, this.image);
},
}, },
}); });
</script> </script>

View File

@ -102,13 +102,16 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import RecipeCard from "./RecipeCard"; import { computed, defineComponent, reactive, toRefs, useContext, useRouter } from "@nuxtjs/composition-api";
import RecipeCardMobile from "./RecipeCardMobile"; import RecipeCard from "./RecipeCard.vue";
import RecipeCardMobile from "./RecipeCardMobile.vue";
import { useSorter } from "~/composables/recipes"; import { useSorter } from "~/composables/recipes";
import {Recipe} from "~/types/api-types/recipe";
const SORT_EVENT = "sort"; const SORT_EVENT = "sort";
export default { export default defineComponent({
components: { components: {
RecipeCard, RecipeCard,
RecipeCardMobile, RecipeCardMobile,
@ -126,100 +129,90 @@ export default {
type: String, type: String,
default: null, default: null,
}, },
hardLimit: {
type: [String, Number],
default: 99999,
},
mobileCards: { mobileCards: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
singleColumn: { singleColumn: {
type: Boolean, type: Boolean,
defualt: false, default: false,
}, },
recipes: { recipes: {
type: Array, type: Array as () => Recipe[],
default: () => [], default: () => [],
}, },
}, },
setup() { setup(props, context) {
const utils = useSorter(); const utils = useSorter();
return { utils }; const EVENTS = {
}, az: "az",
data() { rating: "rating",
return { created: "created",
sortLoading: false, updated: "updated",
loading: false, shuffle: "shuffle",
EVENTS: {
az: "az",
rating: "rating",
created: "created",
updated: "updated",
shuffle: "shuffle",
},
}; };
},
computed: {
viewScale() {
if (this.mobileCards) return true;
switch (this.$vuetify.breakpoint.name) {
case "xs":
return true;
case "sm":
return true;
default:
return false;
}
},
effectiveHardLimit() {
return Math.min(this.hardLimit, this.recipes.length);
},
displayTitleIcon() {
return this.icon || this.$globals.icons.tags;
},
},
methods: { const { $globals, $vuetify } = useContext();
async setLoader() { const viewScale = computed(() => {
this.loading = true; return props.mobileCards || $vuetify.breakpoint.smAndDown;
// eslint-disable-next-line promise/param-names });
await new Promise((r) => setTimeout(r, 1000));
this.loading = false; const displayTitleIcon = computed(() => {
}, return props.icon || $globals.icons.tags;
navigateRandom() { });
const recipe = this.recipes[Math.floor(Math.random() * this.recipes.length)];
this.$router.push(`/recipe/${recipe.slug}`); const state = reactive({
}, sortLoading: false,
sortRecipes(sortType) { })
this.sortLoading = true;
const sortTarget = [...this.recipes]; const router = useRouter();
function navigateRandom() {
if (props.recipes.length > 0) {
const recipe = props.recipes[Math.floor(Math.random() * props.recipes.length)];
if (recipe.slug !== undefined) {
router.push(`/recipe/${recipe.slug}`);
}
}
}
function sortRecipes(sortType: string) {
state.sortLoading = true;
const sortTarget = [...props.recipes];
switch (sortType) { switch (sortType) {
case this.EVENTS.az: case EVENTS.az:
this.utils.sortAToZ(sortTarget); utils.sortAToZ(sortTarget);
break; break;
case this.EVENTS.rating: case EVENTS.rating:
this.utils.sortByRating(sortTarget); utils.sortByRating(sortTarget);
break; break;
case this.EVENTS.created: case EVENTS.created:
this.utils.sortByCreated(sortTarget); utils.sortByCreated(sortTarget);
break; break;
case this.EVENTS.updated: case EVENTS.updated:
this.utils.sortByUpdated(sortTarget); utils.sortByUpdated(sortTarget);
break; break;
case this.EVENTS.shuffle: case EVENTS.shuffle:
this.utils.shuffle(sortTarget); utils.shuffle(sortTarget);
break; break;
default: default:
console.log("Unknown Event", sortType); console.log("Unknown Event", sortType);
return; return;
} }
this.$emit(SORT_EVENT, sortTarget); context.emit(SORT_EVENT, sortTarget);
this.sortLoading = false; state.sortLoading = false;
}, }
return {
...toRefs(state),
EVENTS,
viewScale,
displayTitleIcon,
navigateRandom,
sortRecipes,
};
}, },
}; });
</script> </script>
<style> <style>

View File

@ -40,16 +40,14 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"; import { computed, defineComponent, reactive, toRefs, watch } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
const CREATED_ITEM_EVENT = "created-item"; const CREATED_ITEM_EVENT = "created-item";
export default defineComponent({ export default defineComponent({
props: { props: {
buttonText: {
type: String,
default: "Add",
},
value: { value: {
type: String, type: String,
default: "", default: "",
@ -63,55 +61,49 @@ export default defineComponent({
default: true, default: true,
}, },
}, },
setup() { setup(props, context) {
const api = useUserApi(); const title = computed(() => props.tagDialog ? "Create a Tag" : "Create a Category");
const inputLabel = computed(() => props.tagDialog ? "Tag Name" : "Category Name");
return { api }; const rules = {
}, required: (val: string) => !!val || "A Name is Required",
data() { };
return {
const state = reactive({
dialog: false, dialog: false,
itemName: "", itemName: "",
rules: { });
required: (val) => !!val || "A Name is Required",
},
};
},
computed: { watch(() => state.dialog, (val: boolean) => {
title() { if (!val) state.itemName = "";
return this.tagDialog ? "Create a Tag" : "Create a Category"; });
},
inputLabel() {
return this.tagDialog ? "Tag Name" : "Category Name";
},
},
watch: {
dialog(val) {
if (!val) this.itemName = "";
},
},
methods: { const api = useUserApi();
open() { async function select() {
this.dialog = true;
},
async select() {
const newItem = await (async () => { const newItem = await (async () => {
if (this.tagDialog) { if (props.tagDialog) {
const { data } = await this.api.tags.createOne({ name: this.itemName }); const { data } = await api.tags.createOne({ name: state.itemName });
return data; return data;
} else { } else {
const { data } = await this.api.categories.createOne({ name: this.itemName }); const { data } = await api.categories.createOne({ name: state.itemName });
return data; return data;
} }
})(); })();
console.log(newItem); console.log(newItem);
this.$emit(CREATED_ITEM_EVENT, newItem); context.emit(CREATED_ITEM_EVENT, newItem);
this.dialog = false; state.dialog = false;
}, }
return {
...toRefs(state),
title,
inputLabel,
rules,
select,
};
}, },
}); });
</script> </script>

View File

@ -42,18 +42,22 @@
</v-autocomplete> </v-autocomplete>
</template> </template>
<script> <script lang="ts">
import { computed, defineComponent, onMounted, reactive, toRefs, useContext, watch } from "@nuxtjs/composition-api";
import RecipeCategoryTagDialog from "./RecipeCategoryTagDialog.vue"; import RecipeCategoryTagDialog from "./RecipeCategoryTagDialog.vue";
import { useUserApi } from "~/composables/api";
import { useTags, useCategories } from "~/composables/recipes"; import { useTags, useCategories } from "~/composables/recipes";
import { Category } from "~/api/class-interfaces/categories";
import { Tag } from "~/api/class-interfaces/tags";
const MOUNTED_EVENT = "mounted"; const MOUNTED_EVENT = "mounted";
export default {
export default defineComponent({
components: { components: {
RecipeCategoryTagDialog, RecipeCategoryTagDialog,
}, },
props: { props: {
value: { value: {
type: Array, type: Array as () => (Category | Tag | string)[],
required: true, required: true,
}, },
solo: { solo: {
@ -90,74 +94,74 @@ export default {
}, },
}, },
setup() { setup(props, context) {
const api = useUserApi();
const { allTags, useAsyncGetAll: getAllTags } = useTags(); const { allTags, useAsyncGetAll: getAllTags } = useTags();
const { allCategories, useAsyncGetAll: getAllCategories } = useCategories(); const { allCategories, useAsyncGetAll: getAllCategories } = useCategories();
getAllCategories(); getAllCategories();
getAllTags(); getAllTags();
return { api, allTags, allCategories, getAllCategories, getAllTags }; const state = reactive({
}, selected: props.value,
});
watch(() => props.value, (val) => {
state.selected = val;
});
data() { const { i18n } = useContext();
return { const inputLabel = computed(() => {
selected: [], if (!props.showLabel) return null;
}; return props.tagSelector ? i18n.t("tag.tags") : i18n.t("recipe.categories");
}, });
computed: { const activeItems = computed(() => {
inputLabel() { let itemObjects: Tag[] | Category[] | null;
if (!this.showLabel) return null; if (props.tagSelector) itemObjects = allTags.value;
return this.tagSelector ? this.$t("tag.tags") : this.$t("recipe.categories");
},
activeItems() {
let ItemObjects = [];
if (this.tagSelector) ItemObjects = this.allTags;
else { else {
ItemObjects = this.allCategories; itemObjects = allCategories.value;
} }
if (this.returnObject) return ItemObjects; if (props.returnObject) return itemObjects;
else { else {
return ItemObjects.map((x) => x.name); return itemObjects?.map((x: Tag | Category) => x.name);
} }
}, });
flat() {
if (this.selected) { const flat = computed(() => {
return this.selected.length > 0 && this.solo; if (state.selected) {
return state.selected.length > 0 && props.solo;
} }
return false; return false;
}, });
},
watch: { function emitChange() {
value(val) { context.emit("input", state.selected);
this.selected = val; }
},
}, // TODO Is this needed?
mounted() { onMounted(() => {
this.$emit(MOUNTED_EVENT); context.emit(MOUNTED_EVENT);
this.setInit(this.value); });
},
methods: { function removeByIndex(index: number) {
emitChange() { state.selected.splice(index, 1);
this.$emit("input", this.selected); }
},
setInit(val) { function pushToItem(createdItem: Tag | Category) {
this.selected = val;
},
removeByIndex(index) {
this.selected.splice(index, 1);
},
pushToItem(createdItem) {
createdItem = this.returnObject ? createdItem : createdItem.name;
// TODO: Remove excessive get calls // TODO: Remove excessive get calls
this.getAllCategories(); getAllCategories();
this.getAllTags(); getAllTags();
this.selected.push(createdItem); state.selected.push(createdItem);
}, }
return {
...toRefs(state),
inputLabel,
activeItems,
flat,
emitChange,
removeByIndex,
pushToItem,
};
}, },
}; });
</script> </script>

View File

@ -27,7 +27,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, toRefs, useContext, computed } from "@nuxtjs/composition-api"; import { defineComponent, reactive, toRefs, useContext, computed, useMeta } from "@nuxtjs/composition-api";
type ItemType = "tags" | "categories" | "tools"; type ItemType = "tags" | "categories" | "tools";
@ -54,7 +54,6 @@ export default defineComponent({
}, },
}, },
setup(props) { setup(props) {
// @ts-ignore
const { i18n, $globals } = useContext(); const { i18n, $globals } = useContext();
const state = reactive({ const state = reactive({
@ -77,8 +76,12 @@ export default defineComponent({
break; break;
} }
useMeta(() => ({
title: state.headline,
}));
const itemsSorted = computed(() => { const itemsSorted = computed(() => {
const byLetter: { [key: string]: Array<any> } = {}; const byLetter: { [key: string]: Array<GenericItem> } = {};
if (!props.items) return byLetter; if (!props.items) return byLetter;
@ -99,10 +102,7 @@ export default defineComponent({
itemsSorted, itemsSorted,
}; };
}, },
head() { // Needed for useMeta
return { head: {},
title: this.headline as string,
}
},
}); });
</script> </script>

View File

@ -16,8 +16,10 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
export default { import {computed, defineComponent} from "@nuxtjs/composition-api";
export default defineComponent({
props: { props: {
truncate: { truncate: {
type: Boolean, type: Boolean,
@ -48,39 +50,23 @@ export default {
default: null, default: null,
}, },
}, },
computed: { setup(props) {
allCategories() { const urlParam = computed(() => props.isCategory ? "categories" : "tags");
return this.$store.getters.getAllCategories || [];
},
allTags() {
return this.$store.getters.getAllTags || [];
},
urlParam() {
return this.isCategory ? "categories" : "tags";
},
},
methods: {
getSlug(name) {
if (!name) return;
if (this.isCategory) { function truncateText(text: string, length = 20, clamp = "...") {
const matches = this.allCategories.filter((x) => x.name === name); if (!props.truncate) return text;
if (matches.length > 0) return matches[0].slug;
} else {
const matches = this.allTags.filter((x) => x.name === name);
if (matches.length > 0) return matches[0].slug;
}
},
truncateText(text, length = 20, clamp) {
if (!this.truncate) return text;
clamp = clamp || "...";
const node = document.createElement("div"); const node = document.createElement("div");
node.innerHTML = text; node.innerHTML = text;
const content = node.textContent; const content = node.textContent || "";
return content.length > length ? content.slice(0, length) + clamp : content; return content.length > length ? content.slice(0, length) + clamp : content;
}, }
return {
urlParam,
truncateText,
}
}, },
}; });
</script> </script>
<style></style> <style></style>

View File

@ -167,7 +167,6 @@ export default defineComponent({
pickerMenu: false, pickerMenu: false,
}); });
// @ts-ignore
const { i18n, $globals } = useContext(); const { i18n, $globals } = useContext();
// =========================================================================== // ===========================================================================
@ -262,14 +261,12 @@ export default defineComponent({
} }
// Note: Print is handled as an event in the parent component // Note: Print is handled as an event in the parent component
const eventHandlers: { [key: string]: Function } = { const eventHandlers: { [key: string]: () => void } = {
// @ts-ignore - Doens't know about open()
delete: () => { delete: () => {
state.recipeDeleteDialog = true; state.recipeDeleteDialog = true;
}, },
edit: () => router.push(`/recipe/${props.slug}` + "?edit=true"), edit: () => router.push(`/recipe/${props.slug}` + "?edit=true"),
download: handleDownloadEvent, download: handleDownloadEvent,
// @ts-ignore - Doens't know about open()
mealplanner: () => { mealplanner: () => {
state.mealplannerDialog = true; state.mealplannerDialog = true;
}, },

View File

@ -49,13 +49,13 @@ import { UserOut } from "~/types/api-types/user";
const INPUT_EVENT = "input"; const INPUT_EVENT = "input";
interface ShowHeaders { interface ShowHeaders {
id: Boolean; id: boolean;
owner: Boolean; owner: boolean;
tags: Boolean; tags: boolean;
categories: Boolean; categories: boolean;
tools: Boolean; tools: boolean;
recipeYield: Boolean; recipeYield: boolean;
dateAdded: Boolean; dateAdded: boolean;
} }
export default defineComponent({ export default defineComponent({
@ -129,7 +129,7 @@ export default defineComponent({
// ============ // ============
// Group Members // Group Members
const api = useUserApi(); const api = useUserApi();
const members = ref<UserOut[] | null[]>([]); const members = ref<UserOut[]>([]);
async function refreshMembers() { async function refreshMembers() {
const { data } = await api.groups.fetchMembers(); const { data } = await api.groups.fetchMembers();
@ -142,10 +142,9 @@ export default defineComponent({
refreshMembers(); refreshMembers();
}); });
function getMember(id: number) { function getMember(id: string) {
if (members.value[0]) { if (members.value[0]) {
// @ts-ignore return members.value.find((m) => m.id === id)?.username;
return members.value.find((m) => m.id === id).username;
} }
return "None"; return "None";

View File

@ -54,7 +54,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, toRefs, reactive, ref, watch } from "@nuxtjs/composition-api"; import { defineComponent, toRefs, reactive, ref, watch, useRoute } from "@nuxtjs/composition-api";
import RecipeCardMobile from "./RecipeCardMobile.vue"; import RecipeCardMobile from "./RecipeCardMobile.vue";
import { useRecipes, allRecipes, useRecipeSearch } from "~/composables/recipes"; import { useRecipes, allRecipes, useRecipeSearch } from "~/composables/recipes";
import { RecipeSummary } from "~/types/api-types/recipe"; import { RecipeSummary } from "~/types/api-types/recipe";
@ -74,7 +74,7 @@ export default defineComponent({
}); });
// =========================================================================== // ===========================================================================
// Dialong State Management // Dialog State Management
const dialog = ref(false); const dialog = ref(false);
// Reset or Grab Recipes on Change // Reset or Grab Recipes on Change
@ -89,6 +89,53 @@ export default defineComponent({
} }
}); });
// ===========================================================================
// Event Handlers
function selectRecipe() {
const recipeCards = document.getElementsByClassName("arrow-nav");
if (recipeCards) {
if (state.selectedIndex < 0) {
state.selectedIndex = -1;
document.getElementById("arrow-search")?.focus();
return;
}
if (state.selectedIndex >= recipeCards.length) {
state.selectedIndex = recipeCards.length - 1;
}
(recipeCards[state.selectedIndex] as HTMLElement).focus();
}
}
function onUpDown(e: KeyboardEvent) {
if (e.key === "Enter") {
console.log(document.activeElement);
// (document.activeElement as HTMLElement).click();
} else if (e.key === "ArrowUp") {
e.preventDefault();
state.selectedIndex--;
} else if (e.key === "ArrowDown") {
e.preventDefault();
state.selectedIndex++;
} else {
return;
}
selectRecipe();
}
watch(dialog, (val) => {
if (!val) {
document.removeEventListener("keyup", onUpDown);
} else {
document.addEventListener("keyup", onUpDown);
}
});
const route = useRoute();
watch(route, close);
function open() { function open() {
dialog.value = true; dialog.value = true;
} }
@ -110,56 +157,6 @@ export default defineComponent({
return { allRecipes, refreshRecipes, ...toRefs(state), dialog, open, close, handleSelect, search, results }; return { allRecipes, refreshRecipes, ...toRefs(state), dialog, open, close, handleSelect, search, results };
}, },
data() {
return {};
},
computed: {},
watch: {
$route() {
this.dialog = false;
},
dialog() {
if (!this.dialog) {
document.removeEventListener("keyup", this.onUpDown);
} else {
document.addEventListener("keyup", this.onUpDown);
}
},
},
methods: {
onUpDown(e: KeyboardEvent) {
if (e.key === "Enter") {
console.log(document.activeElement);
// (document.activeElement as HTMLElement).click();
} else if (e.key === "ArrowUp") {
e.preventDefault();
this.selectedIndex--;
} else if (e.key === "ArrowDown") {
e.preventDefault();
this.selectedIndex++;
} else {
return;
}
this.selectRecipe();
},
selectRecipe() {
const recipeCards = document.getElementsByClassName("arrow-nav");
if (recipeCards) {
if (this.selectedIndex < 0) {
this.selectedIndex = -1;
document.getElementById("arrow-search")?.focus();
return;
}
if (this.selectedIndex >= recipeCards.length) {
this.selectedIndex = recipeCards.length - 1;
}
(recipeCards[this.selectedIndex] as HTMLElement).focus();
}
},
},
}); });
</script> </script>

View File

@ -20,9 +20,10 @@
</v-tooltip> </v-tooltip>
</template> </template>
<script> <script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"; import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import {UserOut} from "~/types/api-types/user";
export default defineComponent({ export default defineComponent({
props: { props: {
slug: { slug: {
@ -38,28 +39,26 @@ export default defineComponent({
default: false, default: false,
}, },
}, },
setup() { setup(props) {
const api = useUserApi(); const api = useUserApi();
const { $auth } = useContext();
return { api }; // TODO Setup the correct type for $auth.user
}, // See https://github.com/nuxt-community/auth-module/issues/1097
computed: { const user = computed(() => $auth.user as unknown as UserOut);
user() { const isFavorite = computed(() => user.value?.favoriteRecipes?.includes(props.slug));
return this.$auth.user;
}, async function toggleFavorite() {
isFavorite() { console.log("Favorited?");
return this.$auth.user.favoriteRecipes.includes(this.slug); if (!isFavorite.value) {
}, await api.users.addFavorite(user.value?.id, props.slug);
},
methods: {
async toggleFavorite() {
if (!this.isFavorite) {
await this.api.users.addFavorite(this.$auth.user.id, this.slug);
} else { } else {
await this.api.users.removeFavorite(this.$auth.user.id, this.slug); await api.users.removeFavorite(user.value?.id, props.slug);
} }
this.$auth.fetchUser(); $auth.fetchUser();
}, };
return { isFavorite, toggleFavorite };
}, },
}); });
</script> </script>

View File

@ -25,7 +25,7 @@
</v-card-title> </v-card-title>
<v-card-text class="mt-n5"> <v-card-text class="mt-n5">
<div> <div>
<v-text-field v-model="url" :label="$t('general.url')" class="pt-5" clearable :messages="getMessages()"> <v-text-field v-model="url" :label="$t('general.url')" class="pt-5" clearable :messages="messages">
<template #append-outer> <template #append-outer>
<v-btn class="ml-2" color="primary" :loading="loading" :disabled="!slug" @click="getImageFromURL"> <v-btn class="ml-2" color="primary" :loading="loading" :disabled="!slug" @click="getImageFromURL">
{{ $t("general.get") }} {{ $t("general.get") }}
@ -39,11 +39,13 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"; import { defineComponent, reactive, toRefs, useContext } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
const REFRESH_EVENT = "refresh"; const REFRESH_EVENT = "refresh";
const UPLOAD_EVENT = "upload"; const UPLOAD_EVENT = "upload";
export default defineComponent({ export default defineComponent({
props: { props: {
slug: { slug: {
@ -51,32 +53,37 @@ export default defineComponent({
required: true, required: true,
}, },
}, },
setup() { setup(props, context) {
const api = useUserApi(); const state = reactive({
url: "",
loading: false,
menu: false,
})
return { api }; function uploadImage(fileObject: File) {
}, context.emit(UPLOAD_EVENT, fileObject);
data: () => ({ state.menu = false;
url: "", }
loading: false,
menu: false, const api = useUserApi();
}), async function getImageFromURL() {
methods: { state.loading = true;
uploadImage(fileObject) { if (await api.recipes.updateImagebyURL(props.slug, state.url)) {
this.$emit(UPLOAD_EVENT, fileObject); context.emit(REFRESH_EVENT);
this.menu = false;
},
async getImageFromURL() {
this.loading = true;
if (await this.api.recipes.updateImagebyURL(this.slug, this.url)) {
this.$emit(REFRESH_EVENT);
} }
this.loading = false; state.loading = false;
this.menu = false; state.menu = false;
}, }
getMessages() {
return this.slug ? [""] : [this.$i18n.t("recipe.save-recipe-before-use")]; const { i18n } = useContext();
}, const messages = props.slug ? [""] : [i18n.t("recipe.save-recipe-before-use")];
return {
...toRefs(state),
uploadImage,
getImageFromURL,
messages,
};
}, },
}); });
</script> </script>

View File

@ -105,11 +105,12 @@
import { defineComponent, reactive, ref, toRefs } from "@nuxtjs/composition-api"; import { defineComponent, reactive, ref, toRefs } from "@nuxtjs/composition-api";
import { useFoods, useUnits } from "~/composables/recipes"; import { useFoods, useUnits } from "~/composables/recipes";
import { validators } from "~/composables/use-validators"; import { validators } from "~/composables/use-validators";
import { RecipeIngredient } from "~/types/api-types/recipe";
export default defineComponent({ export default defineComponent({
props: { props: {
value: { value: {
type: Object, type: Object as () => RecipeIngredient,
required: true, required: true,
}, },
disableAmount: { disableAmount: {
@ -157,14 +158,14 @@ export default defineComponent({
} }
function handleUnitEnter() { function handleUnitEnter() {
if (value.unit === null || !value.unit.name.includes(unitSearch.value)) { if (value.unit === undefined || !value.unit.name.includes(unitSearch.value)) {
console.log("Creating"); console.log("Creating");
createAssignUnit(); createAssignUnit();
} }
} }
function handleFoodEnter() { function handleFoodEnter() {
if (value.food === null || !value.food.name.includes(foodSearch.value)) { if (value.food === undefined || !value.food.name.includes(foodSearch.value)) {
console.log("Creating"); console.log("Creating");
createAssignFood(); createAssignFood();
} }

View File

@ -23,17 +23,20 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import { computed, defineComponent } from "@nuxtjs/composition-api"; import { computed, defineComponent, reactive, toRefs } from "@nuxtjs/composition-api";
// @ts-ignore
import VueMarkdown from "@adapttive/vue-markdown"; import VueMarkdown from "@adapttive/vue-markdown";
import { parseIngredientText } from "~/composables/recipes"; import { parseIngredientText } from "~/composables/recipes";
import { RecipeIngredient } from "~/types/api-types/recipe";
export default defineComponent({ export default defineComponent({
components: { components: {
VueMarkdown, VueMarkdown,
}, },
props: { props: {
value: { value: {
type: Array, type: Array as () => RecipeIngredient[],
default: () => [], default: () => [],
}, },
disableAmount: { disableAmount: {
@ -46,6 +49,15 @@ export default defineComponent({
}, },
}, },
setup(props) { setup(props) {
function validateTitle(title?: string) {
return !(title === undefined || title === "");
}
const state = reactive({
checked: props.value.map(() => false),
showTitleEditor: computed(() => props.value.map((x) => validateTitle(x.title))),
});
const ingredientCopyText = computed(() => { const ingredientCopyText = computed(() => {
return props.value return props.value
.map((ingredient) => { .map((ingredient) => {
@ -54,41 +66,18 @@ export default defineComponent({
.join("\n"); .join("\n");
}); });
return { parseIngredientText, ingredientCopyText }; function toggleChecked(index: number) {
}, // TODO Find a better way to do this - $set is not available, and
data() { // direct array modifications are not propagated for some reason
return { state.checked.splice(index, 1, !state.checked[index]);
drag: false, }
checked: [],
showTitleEditor: [],
};
},
watch: {
value: {
handler() {
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));
},
methods: {
toggleChecked(index) {
this.$set(this.checked, index, !this.checked[index]);
},
validateTitle(title) { return {
return !(title === null || title === ""); ...toRefs(state),
}, parseIngredientText,
toggleShowTitle(index) { ingredientCopyText,
const newVal = !this.showTitleEditor[index]; toggleChecked,
if (!newVal) { };
this.value[index].title = "";
}
this.$set(this.showTitleEditor, index, newVal);
},
}, },
}); });
</script> </script>

View File

@ -153,7 +153,7 @@ import draggable from "vuedraggable";
// @ts-ignore // @ts-ignore
import VueMarkdown from "@adapttive/vue-markdown"; import VueMarkdown from "@adapttive/vue-markdown";
import { ref, toRefs, reactive, defineComponent, watch, onMounted } from "@nuxtjs/composition-api"; import { ref, toRefs, reactive, defineComponent, watch, onMounted } from "@nuxtjs/composition-api";
import { RecipeStep, IngredientToStepRef, RecipeIngredient } from "~/types/api-types/recipe"; import { RecipeStep, IngredientReferences, RecipeIngredient } from "~/types/api-types/recipe";
import { parseIngredientText } from "~/composables/recipes"; import { parseIngredientText } from "~/composables/recipes";
import { uuid4 } from "~/composables/use-utils"; import { uuid4 } from "~/composables/use-utils";
@ -227,14 +227,18 @@ export default defineComponent({
state.disabledSteps = []; state.disabledSteps = [];
v.forEach((element) => { v.forEach((element) => {
showTitleEditor.value[element.id] = validateTitle(element.title); if (element.id !== undefined) {
showTitleEditor.value[element.id] = validateTitle(element.title);
}
}); });
}); });
// Eliminate state with an eager call to watcher? // Eliminate state with an eager call to watcher?
onMounted(() => { onMounted(() => {
props.value.forEach((element) => { props.value.forEach((element) => {
showTitleEditor.value[element.id] = validateTitle(element.title); if (element.id !== undefined) {
showTitleEditor.value[element.id] = validateTitle(element.title);
}
}); });
}); });
@ -268,23 +272,23 @@ export default defineComponent({
// =============================================================== // ===============================================================
// Ingredient Linker // Ingredient Linker
const activeRefs = ref<String[]>([]); const activeRefs = ref<string[]>([]);
const activeIndex = ref(0); const activeIndex = ref(0);
const activeText = ref(""); const activeText = ref("");
function openDialog(idx: number, refs: IngredientToStepRef[], text: string) { function openDialog(idx: number, refs: IngredientReferences[], text: string) {
setUsedIngredients(); setUsedIngredients();
activeText.value = text; activeText.value = text;
activeIndex.value = idx; activeIndex.value = idx;
state.dialog = true; state.dialog = true;
activeRefs.value = refs.map((ref) => ref.referenceId); activeRefs.value = refs.map((ref) => ref.referenceId ?? "");
} }
function setIngredientIds() { function setIngredientIds() {
const instruction = props.value[activeIndex.value]; const instruction = props.value[activeIndex.value];
instruction.ingredientReferences = activeRefs.value.map((ref) => { instruction.ingredientReferences = activeRefs.value.map((ref) => {
return { return {
referenceId: ref as string, referenceId: ref,
}; };
}); });
state.dialog = false; state.dialog = false;
@ -294,17 +298,19 @@ export default defineComponent({
const usedRefs: { [key: string]: boolean } = {}; const usedRefs: { [key: string]: boolean } = {};
props.value.forEach((element) => { props.value.forEach((element) => {
element.ingredientReferences.forEach((ref) => { element.ingredientReferences?.forEach((ref) => {
usedRefs[ref.referenceId] = true; if (ref.referenceId !== undefined) {
usedRefs[ref.referenceId] = true;
}
}); });
}); });
state.usedIngredients = props.ingredients.filter((ing) => { state.usedIngredients = props.ingredients.filter((ing) => {
return ing.referenceId in usedRefs; return ing.referenceId !== undefined && ing.referenceId in usedRefs;
}); });
state.unusedIngredients = props.ingredients.filter((ing) => { state.unusedIngredients = props.ingredients.filter((ing) => {
return !(ing.referenceId in usedRefs); return !(ing.referenceId !== undefined && ing.referenceId in usedRefs);
}); });
} }
@ -343,6 +349,10 @@ export default defineComponent({
props.ingredients.forEach((ingredient) => { props.ingredients.forEach((ingredient) => {
const searchText = parseIngredientText(ingredient, props.disableAmount); const searchText = parseIngredientText(ingredient, props.disableAmount);
if (ingredient.referenceId === undefined) {
return;
}
if (searchText.toLowerCase().includes(" " + word) && !activeRefs.value.includes(ingredient.referenceId)) { if (searchText.toLowerCase().includes(" " + word) && !activeRefs.value.includes(ingredient.referenceId)) {
console.info("Word Matched", `'${word}'`, ingredient.note); console.info("Word Matched", `'${word}'`, ingredient.note);
activeRefs.value.push(ingredient.referenceId); activeRefs.value.push(ingredient.referenceId);
@ -351,7 +361,7 @@ export default defineComponent({
}); });
} }
function getIngredientByRefId(refId: String) { function getIngredientByRefId(refId: string) {
const ing = props.ingredients.find((ing) => ing.referenceId === refId) || ""; const ing = props.ingredients.find((ing) => ing.referenceId === refId) || "";
if (ing === "") { if (ing === "") {
return ""; return "";

View File

@ -31,9 +31,12 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
// @ts-ignore
import VueMarkdown from "@adapttive/vue-markdown"; import VueMarkdown from "@adapttive/vue-markdown";
export default { import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
components: { components: {
VueMarkdown, VueMarkdown,
}, },
@ -48,15 +51,21 @@ export default {
default: true, default: true,
}, },
}, },
methods: { setup(props) {
addNote() { function addNote() {
this.value.push({ title: "", text: "" }); props.value.push({ title: "", text: "" });
}, }
removeByIndex(list, index) {
function removeByIndex(list: unknown[], index: number) {
list.splice(index, 1); list.splice(index, 1);
}, }
return {
addNote,
removeByIndex,
};
}, },
}; });
</script> </script>
<style></style> <style></style>

View File

@ -33,11 +33,14 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
export default { import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import { Nutrition } from "~/types/api-types/recipe";
export default defineComponent({
props: { props: {
value: { value: {
type: Object, type: Object as () => Nutrition,
required: true, required: true,
}, },
edit: { edit: {
@ -45,59 +48,59 @@ export default {
default: true, default: true,
}, },
}, },
data() { setup(props, context) {
return { const { i18n } = useContext();
labels: { const labels = {
calories: { calories: {
label: this.$t("recipe.calories"), label: i18n.t("recipe.calories"),
suffix: this.$t("recipe.calories-suffix"), suffix: i18n.t("recipe.calories-suffix"),
}, },
fatContent: { fatContent: {
label: this.$t("recipe.fat-content"), label: i18n.t("recipe.fat-content"),
suffix: this.$t("recipe.grams"), suffix: i18n.t("recipe.grams"),
}, },
fiberContent: { fiberContent: {
label: this.$t("recipe.fiber-content"), label: i18n.t("recipe.fiber-content"),
suffix: this.$t("recipe.grams"), suffix: i18n.t("recipe.grams"),
}, },
proteinContent: { proteinContent: {
label: this.$t("recipe.protein-content"), label: i18n.t("recipe.protein-content"),
suffix: this.$t("recipe.grams"), suffix: i18n.t("recipe.grams"),
}, },
sodiumContent: { sodiumContent: {
label: this.$t("recipe.sodium-content"), label: i18n.t("recipe.sodium-content"),
suffix: this.$t("recipe.milligrams"), suffix: i18n.t("recipe.milligrams"),
}, },
sugarContent: { sugarContent: {
label: this.$t("recipe.sugar-content"), label: i18n.t("recipe.sugar-content"),
suffix: this.$t("recipe.grams"), suffix: i18n.t("recipe.grams"),
}, },
carbohydrateContent: { carbohydrateContent: {
label: this.$t("recipe.carbohydrate-content"), label: i18n.t("recipe.carbohydrate-content"),
suffix: this.$t("recipe.grams"), suffix: i18n.t("recipe.grams"),
},
}, },
}; };
}, const valueNotNull = computed(() => {
computed: { Object.values(props.value).forEach((valueProperty) => {
showViewer() {
return !this.edit && this.valueNotNull;
},
valueNotNull() {
for (const property in this.value) {
const valueProperty = this.value[property];
if (valueProperty && valueProperty !== "") return true; if (valueProperty && valueProperty !== "") return true;
} });
return false; return false;
}, });
},
methods: { const showViewer = computed(() => !props.edit && valueNotNull.value);
updateValue(key, value) {
this.$emit("input", { ...this.value, [key]: value }); function updateValue(key: number | string, event: Event) {
}, context.emit("input", { ...props.value, [key]: event });
}
return {
labels,
valueNotNull,
showViewer,
updateValue
}
}, },
}; });
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -16,8 +16,8 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"; import { computed, defineComponent, ref, useContext } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
export default defineComponent({ export default defineComponent({
props: { props: {
@ -25,6 +25,7 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
// TODO Remove name prop?
name: { name: {
type: String, type: String,
default: "", default: "",
@ -42,36 +43,26 @@ export default defineComponent({
default: false, default: false,
}, },
}, },
setup() { setup(props, context) {
const api = useUserApi(); const { $auth } = useContext();
const loggedIn = computed(() => {
return $auth.loggedIn;
});
return { api }; const rating = ref(props.value);
},
data() { const api = useUserApi();
return { function updateRating(val: number) {
rating: 0, if (props.emitOnly) {
}; context.emit("input", val);
},
computed: {
loggedIn() {
return this.$auth.loggedIn;
},
},
mounted() {
this.rating = this.value;
},
methods: {
updateRating(val) {
if (this.emitOnly) {
this.$emit("input", val);
return; return;
} }
this.api.recipes.patchOne(this.slug, { api.recipes.patchOne(props.slug, {
name: this.name,
slug: this.slug,
rating: val, rating: val,
}); });
}, }
return { loggedIn, rating, updateRating };
}, },
}); });
</script> </script>

View File

@ -34,9 +34,10 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
export default { import { defineComponent, useContext } from "@nuxtjs/composition-api";
components: {},
export default defineComponent({
props: { props: {
value: { value: {
type: Object, type: Object,
@ -47,23 +48,23 @@ export default {
required: false, required: false,
}, },
}, },
setup() {
const { i18n } = useContext();
const labels = {
public: i18n.t("recipe.public-recipe"),
showNutrition: i18n.t("recipe.show-nutrition-values"),
showAssets: i18n.t("asset.show-assets"),
landscapeView: i18n.t("recipe.landscape-view-coming-soon"),
disableComments: i18n.t("recipe.disable-comments"),
disableAmount: i18n.t("recipe.disable-amount"),
locked: "Locked",
};
computed: { return {
labels() { labels,
return { }
public: this.$t("recipe.public-recipe"),
showNutrition: this.$t("recipe.show-nutrition-values"),
showAssets: this.$t("asset.show-assets"),
landscapeView: this.$t("recipe.landscape-view-coming-soon"),
disableComments: this.$t("recipe.disable-comments"),
disableAmount: this.$t("recipe.disable-amount"),
locked: "Locked",
};
},
}, },
});
methods: {},
};
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -10,8 +10,10 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
export default { import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
export default defineComponent({
props: { props: {
prepTime: { prepTime: {
type: String, type: String,
@ -26,29 +28,39 @@ export default {
default: null, default: null,
}, },
}, },
computed: { setup(props) {
showCards() { const { i18n } = useContext();
return [this.prepTime, this.totalTime, this.performTime].some((x) => !this.isEmpty(x));
}, function isEmpty(str: string | null) {
allTimes() {
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;
},
validatePrepTime() {
return !this.isEmpty(this.prepTime) ? { name: this.$t("recipe.prep-time"), value: this.prepTime } : null;
},
validatePerformTime() {
return !this.isEmpty(this.performTime) ? { name: this.$t("recipe.perform-time"), value: this.performTime } : null;
},
},
methods: {
isEmpty(str) {
return !str || str.length === 0; return !str || str.length === 0;
}, }
const showCards = computed(() => {
return [props.prepTime, props.totalTime, props.performTime].some((x) => !isEmpty(x));
});
const validateTotalTime = computed(() => {
return !isEmpty(props.totalTime) ? { name: i18n.t("recipe.total-time"), value: props.totalTime } : null;
});
const validatePrepTime = computed(() => {
return !isEmpty(props.prepTime) ? { name: i18n.t("recipe.prep-time"), value: props.prepTime } : null;
});
const validatePerformTime = computed(() => {
return !isEmpty(props.performTime) ? { name: i18n.t("recipe.perform-time"), value: props.performTime } : null;
});
const allTimes = computed(() => {
return [validateTotalTime.value, validatePrepTime.value, validatePerformTime.value].filter((x) => x !== null);
});
return {
showCards,
allTimes,
}
}, },
}; });
</script> </script>
<style scoped> <style scoped>

View File

@ -9,6 +9,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, toRefs, reactive, useContext, computed } from "@nuxtjs/composition-api"; import { defineComponent, toRefs, reactive, useContext, computed } from "@nuxtjs/composition-api";
import { UserOut } from "~/types/api-types/user";
export default defineComponent({ export default defineComponent({
props: { props: {
@ -33,7 +34,9 @@ export default defineComponent({
const { $auth } = useContext(); const { $auth } = useContext();
const imageURL = computed(() => { const imageURL = computed(() => {
const key = $auth?.user?.cacheKey || ""; // TODO Setup correct user type for $auth.user
const user = $auth.user as unknown as (UserOut | null);
const key = user?.cacheKey ?? "";
return `/api/media/users/${props.userId}/profile.webp?cacheKey=${key}`; return `/api/media/users/${props.userId}/profile.webp?cacheKey=${key}`;
}); });

View File

@ -48,8 +48,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api"; import { defineComponent, onBeforeUnmount, onMounted, ref } from "@nuxtjs/composition-api";
import RecipeDialogSearch from "~/components/Domain/Recipe/RecipeDialogSearch.vue"; import RecipeDialogSearch from "~/components/Domain/Recipe/RecipeDialogSearch.vue";
export default defineComponent({ export default defineComponent({
components: { RecipeDialogSearch }, components: { RecipeDialogSearch },
props: { props: {
@ -59,36 +60,32 @@ export default defineComponent({
}, },
}, },
setup() { setup() {
const domSearchDialog = ref(null); const domSearchDialog = ref<InstanceType<typeof RecipeDialogSearch> | null>(null);
function activateSearch() { function activateSearch() {
// @ts-ignore domSearchDialog.value?.open();
domSearchDialog?.value?.open();
} }
function handleKeyEvent(e: KeyboardEvent) {
const activeTag = document.activeElement?.tagName;
if (e.key === "/" && activeTag !== "INPUT" && activeTag !== "TEXTAREA") {
e.preventDefault();
activateSearch();
}
}
onMounted(() => {
document.addEventListener("keydown", handleKeyEvent);
});
onBeforeUnmount(() => {
document.removeEventListener("keydown", handleKeyEvent);
});
return { return {
activateSearch, activateSearch,
domSearchDialog, domSearchDialog,
}; };
}, },
mounted() {
document.addEventListener("keyup", this.handleKeyEvent);
},
beforeUnmount() {
document.removeEventListener("keyup", this.handleKeyEvent);
},
methods: {
handleKeyEvent(e: any) {
if (
e.key === "/" &&
// @ts-ignore
!document.activeElement.id.startsWith("input")
) {
e.preventDefault();
this.activateSearch();
}
},
},
}); });
</script> </script>

View File

@ -32,16 +32,14 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import { computed, defineComponent } from "@nuxtjs/composition-api";
import { toastAlert, toastLoading } from "~/composables/use-toast"; import { toastAlert, toastLoading } from "~/composables/use-toast";
export default { export default defineComponent({
setup() { setup() {
return { toastAlert, toastLoading }; const icon = computed(() => {
}, switch (toastAlert.color) {
computed: {
icon() {
switch (this.toastAlert.color) {
case "error": case "error":
return "mdi-alert"; return "mdi-alert";
case "success": case "success":
@ -51,7 +49,9 @@ export default {
default: default:
return "mdi-alert"; return "mdi-alert";
} }
}, });
return { icon, toastAlert, toastLoading };
}, },
}; });
</script> </script>

View File

@ -34,8 +34,11 @@
</v-tooltip> </v-tooltip>
</template> </template>
<script> <script lang="ts">
export default { import { defineComponent, ref } from "@nuxtjs/composition-api";
import { VTooltip } from "~/types/vuetify";
export default defineComponent({
props: { props: {
copyText: { copyText: {
type: String, type: String,
@ -54,29 +57,33 @@ export default {
default: "", default: "",
}, },
}, },
data() { setup(props) {
return { const show = ref(false);
show: false, const copyToolTip = ref<VTooltip | null>(null);
};
},
methods: { function toggleBlur() {
toggleBlur() { copyToolTip.value?.deactivate();
this.$refs.copyToolTip.deactivate(); }
},
textToClipboard() { function textToClipboard() {
this.show = true; show.value = true;
const copyText = this.copyText; const copyText = props.copyText;
navigator.clipboard.writeText(copyText).then( navigator.clipboard.writeText(copyText).then(
() => console.log(`Copied\n${copyText}`), () => console.log(`Copied\n${copyText}`),
() => console.log(`Copied Failed\n${copyText}`) () => console.log(`Copied Failed\n${copyText}`)
); );
setTimeout(() => { setTimeout(() => {
this.toggleBlur(); toggleBlur();
}, 500); }, 500);
}, }
return {
show,
copyToolTip,
textToClipboard,
}
}, },
}; });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -10,10 +10,13 @@
</v-form> </v-form>
</template> </template>
<script> <script lang="ts">
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
const UPLOAD_EVENT = "uploaded"; const UPLOAD_EVENT = "uploaded";
export default {
export default defineComponent({
props: { props: {
small: { small: {
type: Boolean, type: Boolean,
@ -48,65 +51,70 @@ export default {
default: "", default: "",
}, },
}, },
setup() { setup(props, context) {
const file = ref<File | null>(null);
const uploader = ref<HTMLInputElement | null>(null);
const isSelecting = ref(false);
const { i18n, $globals } = useContext();
const effIcon = props.icon ? props.icon : $globals.icons.upload;
const defaultText = i18n.t("general.upload");
const api = useUserApi(); const api = useUserApi();
async function upload() {
if (file.value != null) {
isSelecting.value = true;
return { api }; if (!props.post) {
}, context.emit(UPLOAD_EVENT, file.value);
data: () => ({ isSelecting.value = false;
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; return;
} }
const formData = new FormData(); const formData = new FormData();
formData.append(this.fileName, this.file); formData.append(props.fileName, file.value);
const response = await this.api.upload.file(this.url, formData); const response = await api.upload.file(props.url, formData);
if (response) { if (response) {
this.$emit(UPLOAD_EVENT, response); context.emit(UPLOAD_EVENT, response);
} }
this.isSelecting = false; isSelecting.value = false;
} }
}, }
onButtonClick() {
this.isSelecting = true; function onFileChanged(e: Event) {
const target = e.target as HTMLInputElement;
if (target.files !== null && target.files.length > 0 && file.value !== null) {
file.value = target.files[0];
upload();
}
}
function onButtonClick() {
isSelecting.value = true;
window.addEventListener( window.addEventListener(
"focus", "focus",
() => { () => {
this.isSelecting = false; isSelecting.value = false;
}, },
{ once: true } { once: true }
); );
uploader.value?.click();
}
this.$refs.uploader.click(); return {
}, file,
onFileChanged(e) { uploader,
this.file = e.target.files[0]; isSelecting,
this.upload(); effIcon,
}, defaultText,
onFileChanged,
onButtonClick,
};
}, },
}; });
</script> </script>
<style></style> <style></style>

View File

@ -20,8 +20,10 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
export default { import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
export default defineComponent({
props: { props: {
loading: { loading: {
type: Boolean, type: Boolean,
@ -40,15 +42,15 @@ export default {
default: false, default: false,
}, },
}, },
computed: { setup(props) {
size() { const size = computed(() => {
if (this.small) { if (props.small) {
return { return {
width: 2, width: 2,
icon: 30, icon: 30,
size: 50, size: 50,
}; };
} else if (this.large) { } else if (props.large) {
return { return {
width: 4, width: 4,
icon: 120, icon: 120,
@ -60,10 +62,15 @@ export default {
icon: 75, icon: 75,
size: 125, size: 125,
}; };
}, });
waitingText() {
return this.$t("general.loading-recipes"); const { i18n } = useContext();
}, const waitingText = i18n.t("general.loading-recipes");
return {
size,
waitingText,
};
}, },
}; });
</script> </script>

View File

@ -137,14 +137,15 @@
</v-card> </v-card>
</template> </template>
<script> <script lang="ts">
import { ref } from "@nuxtjs/composition-api"; import { computed, defineComponent } from "@nuxtjs/composition-api";
import { validators } from "@/composables/use-validators"; import { validators } from "@/composables/use-validators";
import { fieldTypes } from "@/composables/forms"; import { fieldTypes } from "@/composables/forms";
import { AutoFormItems } from "~/types/auto-forms";
const BLUR_EVENT = "blur"; const BLUR_EVENT = "blur";
export default { export default defineComponent({
name: "AutoForm", name: "AutoForm",
props: { props: {
value: { value: {
@ -157,7 +158,7 @@ export default {
}, },
items: { items: {
default: null, default: null,
type: Array, type: Array as () => AutoFormItems,
}, },
width: { width: {
type: [Number, String], type: [Number, String],
@ -165,7 +166,7 @@ export default {
}, },
globalRules: { globalRules: {
default: null, default: null,
type: Array, type: Array as () => string[],
}, },
color: { color: {
default: null, default: null,
@ -176,94 +177,53 @@ export default {
type: Boolean, type: Boolean,
}, },
}, },
setup() { setup(props, context) {
const menu = ref({}); function rulesByKey(keys?: string[] | null) {
if (keys === undefined || keys === null) {
return [];
}
return { const list = [] as ((v: string) => (boolean | string))[];
menu, keys.forEach((key) => {
fieldTypes, if (key in validators) {
validators, list.push(validators[key]);
};
},
computed: {
defaultRules() {
return this.rulesByKey(this.globalRules);
},
},
watch: {
items: {
immediate: true,
handler(val) {
// Initialize Value Object to Obtain all keys
if (!val) {
return;
} }
for (let i = 0; i < val.length; i++) { });
try { return list;
if (this.value[val[i].varName]) { }
continue;
}
} catch {}
if (val[i].type === "text" || val[i].type === "textarea") { const defaultRules = computed(() => rulesByKey(props.globalRules));
this.$set(this.value, val[i].varName, "");
} else if (val[i].type === "select") {
if (!val[i].options[0]) {
continue;
}
this.$set(this.value, val[i].varName, val[i].options[0].value); function removeByIndex(list: never[], index: number) {
} else if (val[i].type === "list") {
this.$set(this.value, val[i].varName, []);
} else if (val[i].type === "object") {
this.$set(this.value, val[i].varName, {});
} else if (val[i].type === "color") {
this.$set(this.value, val[i].varName, "");
this.$set(this.menu, val[i].varName, false);
}
}
},
},
},
methods: {
removeByIndex(list, index) {
// Removes the item at the index // Removes the item at the index
list.splice(index, 1); list.splice(index, 1);
}, }
getTemplate(item) {
const obj = {}; function getTemplate(item: AutoFormItems) {
const obj = {} as { [key: string]: string };
item.forEach((field) => { item.forEach((field) => {
obj[field.varName] = ""; obj[field.varName] = "";
}); });
return obj; return obj;
}, }
rulesByKey(keys) {
const list = [];
if (keys === undefined) { function emitBlur() {
return list; context.emit(BLUR_EVENT, props.value);
} }
if (keys === null) {
return list;
}
if (keys === list) {
return list;
}
keys.forEach((key) => { return {
if (key in this.validators) { rulesByKey,
list.push(this.validators[key]); defaultRules,
} removeByIndex,
}); getTemplate,
return list; emitBlur,
}, fieldTypes,
emitBlur() { validators,
this.$emit(BLUR_EVENT, this.value); };
},
}, },
}; });
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -1,269 +0,0 @@
<template>
<v-card :color="color" :dark="dark" flat :width="width" class="my-2">
<v-row>
<v-col v-for="(inputField, index) in items" :key="index" class="py-0" cols="12" sm="12">
<v-divider v-if="inputField.section" class="my-2" />
<v-card-title v-if="inputField.section" class="pl-0">
{{ inputField.section }}
</v-card-title>
<v-card-text v-if="inputField.sectionDetails" class="pl-0 mt-0 pt-0">
{{ inputField.sectionDetails }}
</v-card-text>
<!-- Check Box -->
<v-checkbox
v-if="inputField.type === fieldTypes.BOOLEAN"
v-model="value[inputField.varName]"
class="my-0 py-0"
:label="inputField.label"
:name="inputField.varName"
:hint="inputField.hint || ''"
@change="emitBlur"
/>
<!-- Text Field -->
<v-text-field
v-else-if="inputField.type === fieldTypes.TEXT"
v-model="value[inputField.varName]"
:readonly="inputField.fixed && updateMode"
filled
rounded
class="rounded-lg"
dense
:label="inputField.label"
:name="inputField.varName"
:hint="inputField.hint || ''"
:rules="[...rulesByKey(inputField.rules), ...defaultRules]"
lazy-validation
@blur="emitBlur"
/>
<!-- Text Area -->
<v-textarea
v-else-if="inputField.type === fieldTypes.TEXT_AREA"
v-model="value[inputField.varName]"
:readonly="inputField.fixed && updateMode"
filled
rounded
class="rounded-lg"
rows="3"
auto-grow
dense
:label="inputField.label"
:name="inputField.varName"
:hint="inputField.hint || ''"
:rules="[...rulesByKey(inputField.rules), ...defaultRules]"
lazy-validation
@blur="emitBlur"
/>
<!-- Option Select -->
<v-select
v-else-if="inputField.type === fieldTypes.SELECT"
v-model="value[inputField.varName]"
:readonly="inputField.fixed && updateMode"
filled
rounded
class="rounded-lg"
:prepend-icon="inputField.icons ? value[inputField.varName] : null"
:label="inputField.label"
:name="inputField.varName"
:items="inputField.options"
:return-object="false"
lazy-validation
@blur="emitBlur"
>
<template #item="{ item }">
<v-list-item-content>
<v-list-item-title>{{ item.text }}</v-list-item-title>
<v-list-item-subtitle>{{ item.description }}</v-list-item-subtitle>
</v-list-item-content>
</template>
</v-select>
<!-- Color Picker -->
<div v-else-if="inputField.type === fieldTypes.COLOR" class="d-flex" style="width: 100%">
<v-menu offset-y>
<template #activator="{ on }">
<v-btn class="my-2 ml-auto" style="min-width: 200px" :color="value[inputField.varName]" dark v-on="on">
{{ inputField.label }}
</v-btn>
</template>
<v-color-picker
v-model="value[inputField.varName]"
value="#7417BE"
hide-canvas
hide-inputs
show-swatches
class="mx-auto"
@input="emitBlur"
/>
</v-menu>
</div>
<div v-else-if="inputField.type === fieldTypes.OBJECT">
<base-auto-form
v-model="value[inputField.varName]"
:color="color"
:items="inputField.items"
@blur="emitBlur"
/>
</div>
<!-- List Type -->
<div v-else-if="inputField.type === fieldTypes.LIST">
<div v-for="(item, idx) in value[inputField.varName]" :key="idx">
<p>
{{ inputField.label }} {{ idx + 1 }}
<span>
<BaseButton class="ml-5" x-small delete @click="removeByIndex(value[inputField.varName], idx)" />
</span>
</p>
<v-divider class="mb-5 mx-2" />
<base-auto-form
v-model="value[inputField.varName][idx]"
:color="color"
:items="inputField.items"
@blur="emitBlur"
/>
</div>
<v-card-actions>
<v-spacer />
<BaseButton small @click="value[inputField.varName].push(getTemplate(inputField.items))"> New </BaseButton>
</v-card-actions>
</div>
</v-col>
</v-row>
</v-card>
</template>
<script>
import { ref } from "@nuxtjs/composition-api";
import { validators } from "@/composables/use-validators";
import { fieldTypes } from "@/composables/forms";
const BLUR_EVENT = "blur";
export default {
name: "BaseAutoForm",
props: {
value: {
default: null,
type: [Object, Array],
},
updateMode: {
default: false,
type: Boolean,
},
items: {
default: null,
type: Array,
},
width: {
type: [Number, String],
default: "max",
},
globalRules: {
default: null,
type: Array,
},
color: {
default: null,
type: String,
},
dark: {
default: false,
type: Boolean,
},
},
setup() {
const menu = ref({});
return {
menu,
fieldTypes,
validators,
};
},
computed: {
defaultRules() {
return this.rulesByKey(this.globalRules);
},
},
watch: {
items: {
immediate: true,
handler(val) {
// Initialize Value Object to Obtain all keys
if (!val) {
return;
}
for (let i = 0; i < val.length; i++) {
try {
if (this.value[val[i].varName]) {
continue;
}
} catch {}
if (val[i].type === "text" || val[i].type === "textarea") {
this.$set(this.value, val[i].varName, "");
} else if (val[i].type === "select") {
if (!val[i].options[0]) {
continue;
}
this.$set(this.value, val[i].varName, val[i].options[0].value);
} else if (val[i].type === "list") {
this.$set(this.value, val[i].varName, []);
} else if (val[i].type === "object") {
this.$set(this.value, val[i].varName, {});
} else if (val[i].type === "color") {
this.$set(this.value, val[i].varName, "");
this.$set(this.menu, val[i].varName, false);
}
}
},
},
},
methods: {
removeByIndex(list, index) {
// Removes the item at the index
list.splice(index, 1);
},
getTemplate(item) {
const obj = {};
item.forEach((field) => {
obj[field.varName] = "";
});
return obj;
},
rulesByKey(keys) {
const list = [];
if (keys === undefined) {
return list;
}
if (keys === null) {
return list;
}
if (keys === list) {
return list;
}
keys.forEach((key) => {
if (key in this.validators) {
list.push(this.validators[key]);
}
});
return list;
},
emitBlur() {
this.$emit(BLUR_EVENT, this.value);
},
},
};
</script>
<style lang="scss" scoped></style>

View File

@ -28,9 +28,11 @@
</v-btn> </v-btn>
</template> </template>
<script> <script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
export default {
export default defineComponent({
name: "BaseButton", name: "BaseButton",
props: { props: {
// Types // Types
@ -106,103 +108,99 @@ export default {
default: false, default: false,
}, },
}, },
setup() { setup(props) {
const api = useUserApi(); const { $globals } = useContext();
const buttonOptions = {
return { api }; create: {
}, text: "Create",
data() { icon: $globals.icons.createAlt,
return { color: "success",
buttonOptions: {
create: {
text: "Create",
icon: this.$globals.icons.createAlt,
color: "success",
},
update: {
text: "Update",
icon: this.$globals.icons.edit,
color: "success",
},
save: {
text: "Save",
icon: this.$globals.icons.save,
color: "success",
},
edit: {
text: "Edit",
icon: this.$globals.icons.edit,
color: "info",
},
delete: {
text: "Delete",
icon: this.$globals.icons.delete,
color: "error",
},
cancel: {
text: "Cancel",
icon: this.$globals.icons.close,
color: "grey",
},
download: {
text: "Download",
icon: this.$globals.icons.download,
color: "info",
},
}, },
buttonStyles: { update: {
defaults: { text: "Update",
text: false, icon: $globals.icons.edit,
outlined: false, color: "success",
}, },
secondary: { save: {
text: false, text: "Save",
outlined: true, icon: $globals.icons.save,
}, color: "success",
minor: { },
outlined: false, edit: {
text: true, text: "Edit",
}, icon: $globals.icons.edit,
color: "info",
},
delete: {
text: "Delete",
icon: $globals.icons.delete,
color: "error",
},
cancel: {
text: "Cancel",
icon: $globals.icons.close,
color: "grey",
},
download: {
text: "Download",
icon: $globals.icons.download,
color: "info",
}, },
}; };
},
computed: { const btnAttrs = computed(() => {
btnAttrs() { if (props.delete) {
if (this.delete) { return buttonOptions.delete;
return this.buttonOptions.delete; } else if (props.update) {
} else if (this.update) { return buttonOptions.update;
return this.buttonOptions.update; } else if (props.edit) {
} else if (this.edit) { return buttonOptions.edit;
return this.buttonOptions.edit; } else if (props.cancel) {
} else if (this.cancel) { return buttonOptions.cancel;
this.setMinor(); } else if (props.save) {
return this.buttonOptions.cancel; return buttonOptions.save;
} else if (this.save) { } else if (props.download) {
return this.buttonOptions.save; return buttonOptions.download;
} else if (this.download) {
return this.buttonOptions.download;
} }
return this.buttonOptions.create; return buttonOptions.create;
}, });
btnStyle() {
if (this.secondary) { const buttonStyles = {
return this.buttonStyles.secondary; defaults: {
} else if (this.minor) { text: false,
return this.buttonStyles.minor; outlined: false,
},
secondary: {
text: false,
outlined: true,
},
minor: {
text: true,
outlined: false,
},
};
const btnStyle = computed(() => {
if (props.secondary) {
return buttonStyles.secondary;
} else if (props.minor || props.cancel) {
return buttonStyles.minor;
} }
return this.buttonStyles.defaults; return buttonStyles.defaults;
}, });
const api = useUserApi();
function downloadFile() {
api.utils.download(props.downloadUrl);
}
return {
btnAttrs,
btnStyle,
downloadFile,
};
}, },
methods: { });
setMinor() {
this.buttonStyles.defaults = this.buttonStyles.minor;
},
setSecondary() {
this.buttonStyles.defaults = this.buttonStyles.secondary;
},
downloadFile() {
this.api.utils.download(this.downloadUrl);
},
},
};
</script> </script>

View File

@ -23,8 +23,10 @@
</v-card> </v-card>
</template> </template>
<script> <script lang="ts">
export default { import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
props: { props: {
title: { title: {
type: String, type: String,
@ -39,5 +41,5 @@ export default {
default: false, default: false,
}, },
}, },
}; });
</script> </script>

View File

@ -1,70 +0,0 @@
<template>
<div>
<div class="text-center">
<h3>{{ buttonText }}</h3>
</div>
<v-text-field v-model="color" hide-details class="ma-0 pa-0" solo>
<template #append>
<v-menu v-model="menu" top nudge-bottom="105" nudge-left="16" :close-on-content-click="false">
<template #activator="{ on }">
<div :style="swatchStyle" swatches-max-height="300" v-on="on" />
</template>
<v-card>
<v-card-text class="pa-0">
<v-color-picker v-model="color" flat mode="hexa" show-swatches />
</v-card-text>
</v-card>
</v-menu>
</template>
</v-text-field>
</div>
</template>
<script>
export default {
props: {
buttonText: {
type: String,
default: "Choose a color",
},
value: {
type: String,
default: "#ff0000",
},
},
data() {
return {
dialog: false,
swatches: false,
color: this.value || "#1976D2",
mask: "!#XXXXXXXX",
menu: false,
};
},
computed: {
swatchStyle() {
const { value, menu } = this;
return {
backgroundColor: value,
cursor: "pointer",
height: "30px",
width: "30px",
borderRadius: menu ? "50%" : "4px",
transition: "border-radius 200ms ease-in-out",
};
},
},
watch: {
color() {
this.updateColor();
},
},
methods: {
updateColor() {
this.$emit("input", this.color);
},
},
};
</script>
<style></style>

View File

@ -110,7 +110,7 @@ export default defineComponent({
}, },
}, },
setup(props, context) { setup(props, context) {
const dialog = computed<Boolean>({ const dialog = computed<boolean>({
get() { get() {
return props.value; return props.value;
}, },
@ -129,12 +129,9 @@ export default defineComponent({
}; };
}, },
computed: { computed: {
determineClose(): Boolean { determineClose(): boolean {
return this.submitted && !this.loading && !this.keepOpen; return this.submitted && !this.loading && !this.keepOpen;
}, },
displayicon(): Boolean {
return this.icon || this.$globals.icons.user;
},
}, },
watch: { watch: {
determineClose() { determineClose() {

View File

@ -2,8 +2,10 @@
<v-divider :width="width" :class="color" :style="`border-width: ${thickness} !important`" /> <v-divider :width="width" :class="color" :style="`border-width: ${thickness} !important`" />
</template> </template>
<script> <script lang="ts">
export default { import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
props: { props: {
width: { width: {
type: String, type: String,
@ -18,5 +20,5 @@ export default {
default: "accent", default: "accent",
}, },
}, },
}; });
</script> </script>

View File

@ -17,13 +17,15 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
export default { import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
props: { props: {
divider: { divider: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}, },
}; });
</script> </script>

View File

@ -1,4 +1,4 @@
w<template> <template>
<v-card v-bind="$attrs" :class="classes" class="v-card--material pa-3"> <v-card v-bind="$attrs" :class="classes" class="v-card--material pa-3">
<div class="d-flex grow flex-wrap"> <div class="d-flex grow flex-wrap">
<slot name="avatar"> <slot name="avatar">
@ -40,8 +40,10 @@ w<template>
</v-card> </v-card>
</template> </template>
<script> <script lang="ts">
export default { import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
export default defineComponent({
name: "MaterialCard", name: "MaterialCard",
props: { props: {
@ -70,22 +72,25 @@ export default {
default: "", default: "",
}, },
}, },
setup() {
const { $vuetify } = useContext();
computed: { const hasHeading = computed(() => false);
classes() { const hasAltHeading = computed(() => false);
const classes = computed(() => {
return { return {
"v-card--material--has-heading": this.hasHeading, "v-card--material--has-heading": hasHeading,
"mt-3": this.$vuetify.breakpoint.name === "xs" || this.$vuetify.breakpoint.name === "sm", "mt-3": $vuetify.breakpoint.name === "xs" || $vuetify.breakpoint.name === "sm",
}; };
}, });
hasHeading() {
return false; return {
}, hasHeading,
hasAltHeading() { hasAltHeading,
return false; classes,
}, };
}, },
}; });
</script> </script>
<style lang="sass"> <style lang="sass">

View File

@ -8,10 +8,12 @@
></VJsoneditor> ></VJsoneditor>
</template> </template>
<script> <script lang="ts">
// @ts-ignore
import VJsoneditor from "v-jsoneditor"; import VJsoneditor from "v-jsoneditor";
import { defineComponent } from "@nuxtjs/composition-api";
export default { export default defineComponent({
components: { VJsoneditor }, components: { VJsoneditor },
props: { props: {
value: { value: {
@ -23,6 +25,6 @@ export default {
default: () => ({}), default: () => ({}),
}, },
}, },
}; });
</script> </script>

View File

@ -47,7 +47,7 @@ export default defineComponent({
{ text: "Delete", value: "actions" }, { text: "Delete", value: "actions" },
]; ];
function handleRowClick(item: any) { function handleRowClick(item: ReportSummary) {
router.push("/user/group/data/reports/" + item.id); router.push("/user/group/data/reports/" + item.id);
} }

View File

@ -2,30 +2,26 @@ import { AxiosResponse } from "axios";
import { useContext } from "@nuxtjs/composition-api"; import { useContext } from "@nuxtjs/composition-api";
import { NuxtAxiosInstance } from "@nuxtjs/axios"; import { NuxtAxiosInstance } from "@nuxtjs/axios";
import { AdminAPI, Api } from "~/api"; import { AdminAPI, Api } from "~/api";
import { ApiRequestInstance } from "~/types/api"; import { ApiRequestInstance, RequestResponse } from "~/types/api";
interface RequestResponse<T> {
response: AxiosResponse<T> | null;
data: T | null;
error: any;
}
const request = { const request = {
async safe<T>(funcCall: any, url: string, data: object = {}): Promise<RequestResponse<T>> { async safe<T, U>(funcCall: (url: string, data: U) => Promise<AxiosResponse<T>>, url: string, data: U): Promise<RequestResponse<T>> {
const response = await funcCall(url, data).catch(function (error: object) { let error = null;
console.log(error); const response = await funcCall(url, data).catch(function (e) {
console.log(e);
// Insert Generic Error Handling Here // Insert Generic Error Handling Here
return { response: null, error, data: null }; error = e;
return null;
}); });
return { response, error: null, data: response.data }; return { response, error, data: response?.data ?? null };
}, },
}; };
function getRequests(axoisInstance: NuxtAxiosInstance): ApiRequestInstance { function getRequests(axiosInstance: NuxtAxiosInstance): ApiRequestInstance {
const requests = { return {
async get<T>(url: string, params = {}): Promise<RequestResponse<T>> { async get<T>(url: string, params = {}): Promise<RequestResponse<T>> {
let error = null; let error = null;
const response = await axoisInstance.get<T>(url, params).catch((e) => { const response = await axiosInstance.get<T>(url, params).catch((e) => {
error = e; error = e;
}); });
if (response != null) { if (response != null) {
@ -34,23 +30,26 @@ function getRequests(axoisInstance: NuxtAxiosInstance): ApiRequestInstance {
return { response: null, error, data: null }; return { response: null, error, data: null };
}, },
async post<T>(url: string, data: object) { async post<T, U>(url: string, data: U) {
return await request.safe<T>(axoisInstance.post, url, data); // eslint-disable-next-line @typescript-eslint/unbound-method
return await request.safe<T, U>(axiosInstance.post, url, data);
}, },
async put<T>(url: string, data: object) { async put<T, U = T>(url: string, data: U) {
return await request.safe<T>(axoisInstance.put, url, data); // eslint-disable-next-line @typescript-eslint/unbound-method
return await request.safe<T, U>(axiosInstance.put, url, data);
}, },
async patch<T>(url: string, data: object) { async patch<T, U = Partial<T>>(url: string, data: U) {
return await request.safe<T>(axoisInstance.patch, url, data); // eslint-disable-next-line @typescript-eslint/unbound-method
return await request.safe<T, U>(axiosInstance.patch, url, data);
}, },
async delete<T>(url: string) { async delete<T>(url: string) {
return await request.safe<T>(axoisInstance.delete, url); // eslint-disable-next-line @typescript-eslint/unbound-method
return await request.safe<T, undefined>(axiosInstance.delete, url, undefined);
}, },
}; };
return requests;
} }
export const useAdminApi = function (): AdminAPI { export const useAdminApi = function (): AdminAPI {

View File

@ -5,20 +5,20 @@ export const useStaticRoutes = () => {
const { $config, req } = useContext(); const { $config, req } = useContext();
const serverBase = detectServerBaseUrl(req); const serverBase = detectServerBaseUrl(req);
const prefix = `${$config.SUB_PATH}/api`.replace("//", "/"); const prefix = `${$config.SUB_PATH as string}/api`.replace("//", "/");
const fullBase = serverBase + prefix; const fullBase = serverBase + prefix;
// Methods to Generate reference urls for assets/images * // Methods to Generate reference urls for assets/images *
function recipeImage(recipeSlug: string, version = null, key = null) { function recipeImage(recipeSlug: string, version = "", key = 1) {
return `${fullBase}/media/recipes/${recipeSlug}/images/original.webp?&rnd=${key}&version=${version}`; return `${fullBase}/media/recipes/${recipeSlug}/images/original.webp?&rnd=${key}&version=${version}`;
} }
function recipeSmallImage(recipeSlug: string, version = null, key = null) { function recipeSmallImage(recipeSlug: string, version = "", key = 1) {
return `${fullBase}/media/recipes/${recipeSlug}/images/min-original.webp?&rnd=${key}&version=${version}`; return `${fullBase}/media/recipes/${recipeSlug}/images/min-original.webp?&rnd=${key}&version=${version}`;
} }
function recipeTinyImage(recipeSlug: string, version = null, key = null) { function recipeTinyImage(recipeSlug: string, version = "", key = 1) {
return `${fullBase}/media/recipes/${recipeSlug}/images/tiny-original.webp?&rnd=${key}&version=${version}`; return `${fullBase}/media/recipes/${recipeSlug}/images/tiny-original.webp?&rnd=${key}&version=${version}`;
} }

View File

@ -1,7 +1,7 @@
/* frac.js (C) 2012-present SheetJS -- http://sheetjs.com */ /* frac.js (C) 2012-present SheetJS -- http://sheetjs.com */
/* https://developer.aliyun.com/mirror/npm/package/frac/v/0.3.0 Apache license */ /* https://developer.aliyun.com/mirror/npm/package/frac/v/0.3.0 Apache license */
function frac(x: number, D: number, mixed: Boolean) { function frac(x: number, D: number, mixed: boolean) {
let n1 = Math.floor(x); let n1 = Math.floor(x);
let d1 = 1; let d1 = 1;
let n2 = n1 + 1; let n2 = n1 + 1;
@ -33,7 +33,7 @@ function frac(x: number, D: number, mixed: Boolean) {
const q = Math.floor(n1 / d1); const q = Math.floor(n1 / d1);
return [q, n1 - q * d1, d1]; return [q, n1 - q * d1, d1];
} }
function cont(x: number, D: number, mixed: Boolean) { function cont(x: number, D: number, mixed: boolean) {
const sgn = x < 0 ? -1 : 1; const sgn = x < 0 ? -1 : 1;
let B = x * sgn; let B = x * sgn;
let P_2 = 0; let P_2 = 0;

View File

@ -2,6 +2,7 @@ import { useAsync, ref, reactive, Ref } from "@nuxtjs/composition-api";
import { useAsyncKey } from "../use-utils"; import { useAsyncKey } from "../use-utils";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { Food } from "~/api/class-interfaces/recipe-foods"; import { Food } from "~/api/class-interfaces/recipe-foods";
import { VForm} from "~/types/vuetify";
let foodStore: Ref<Food[] | null> | null = null; let foodStore: Ref<Food[] | null> | null = null;

View File

@ -3,30 +3,28 @@ import { RecipeIngredient } from "~/types/api-types/recipe";
const { frac } = useFraction(); const { frac } = useFraction();
export function parseIngredientText(ingredient: RecipeIngredient, disableAmount: boolean, scale: number = 1): string { export function parseIngredientText(ingredient: RecipeIngredient, disableAmount: boolean, scale = 1): string {
if (disableAmount) { if (disableAmount) {
return ingredient.note; return ingredient.note || "";
} }
const { quantity, food, unit, note } = ingredient; const { quantity, food, unit, note } = ingredient;
const validQuantity = quantity !== null && quantity !== undefined && quantity !== 0;
let returnQty = ""; let returnQty = "";
if (unit?.fraction) { if (quantity !== undefined && quantity !== 0) {
const fraction = frac(quantity * scale, 10, true); if (unit?.fraction) {
if (fraction[0] !== undefined && fraction[0] > 0) { const fraction = frac(quantity * scale, 10, true);
returnQty += fraction[0]; if (fraction[0] !== undefined && fraction[0] > 0) {
} returnQty += fraction[0];
}
if (fraction[1] > 0) { if (fraction[1] > 0) {
returnQty += ` <sup>${fraction[1]}</sup>&frasl;<sub>${fraction[2]}</sub>`; returnQty += ` <sup>${fraction[1]}</sup>&frasl;<sub>${fraction[2]}</sub>`;
}
} else {
returnQty = (quantity * scale).toString();
} }
} else if (validQuantity) {
returnQty = (quantity * scale).toString();
} else {
returnQty = "";
} }
return `${returnQty} ${unit?.name || " "} ${food?.name || " "} ${note}`.replace(/ {2,}/g, " "); return `${returnQty} ${unit?.name || " "} ${food?.name || " "} ${note || " "}`.replace(/ {2,}/g, " ");
} }

View File

@ -15,7 +15,7 @@ export const useRecipeMeta = (recipe: Ref<Recipe>) => {
{ {
hid: "og:desc", hid: "og:desc",
property: "og:description", property: "og:description",
content: recipe?.value?.description, content: recipe?.value?.description ?? "",
}, },
{ {
hid: "og-image", hid: "og-image",
@ -25,12 +25,12 @@ export const useRecipeMeta = (recipe: Ref<Recipe>) => {
{ {
hid: "twitter:title", hid: "twitter:title",
property: "twitter:title", property: "twitter:title",
content: recipe?.value?.name, content: recipe?.value?.name ?? "",
}, },
{ {
hid: "twitter:desc", hid: "twitter:desc",
property: "twitter:description", property: "twitter:description",
content: recipe?.value?.description, content: recipe?.value?.description ?? "",
}, },
{ hid: "t-type", name: "twitter:card", content: "summary_large_image" }, { hid: "t-type", name: "twitter:card", content: "summary_large_image" },
], ],

View File

@ -1,6 +1,7 @@
import { reactive, ref, useAsync } from "@nuxtjs/composition-api"; import { reactive, ref, useAsync } from "@nuxtjs/composition-api";
import { useAsyncKey } from "../use-utils"; import { useAsyncKey } from "../use-utils";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { VForm } from "~/types/vuetify";
export const useTools = function (eager = true) { export const useTools = function (eager = true) {
const workingToolData = reactive({ const workingToolData = reactive({

View File

@ -2,6 +2,7 @@ import { useAsync, ref, reactive, Ref } from "@nuxtjs/composition-api";
import { useAsyncKey } from "../use-utils"; import { useAsyncKey } from "../use-utils";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { Unit } from "~/api/class-interfaces/recipe-units"; import { Unit } from "~/api/class-interfaces/recipe-units";
import { VForm } from "~/types/vuetify";
let unitStore: Ref<Unit[] | null> | null = null; let unitStore: Ref<Unit[] | null> | null = null;

View File

@ -2,7 +2,7 @@ import { ref, onMounted } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { Recipe } from "~/types/api-types/recipe"; import { Recipe } from "~/types/api-types/recipe";
export const useRecipe = function (slug: string, eager: boolean = true) { export const useRecipe = function (slug: string, eager = true) {
const api = useUserApi(); const api = useUserApi();
const loading = ref(false); const loading = ref(false);

View File

@ -9,7 +9,7 @@ export const recentRecipes = ref<Recipe[] | null>([]);
const rand = (n: number) => Math.floor(Math.random() * n); const rand = (n: number) => Math.floor(Math.random() * n);
function swap(t: Array<any>, i: number, j: number) { function swap(t: Array<unknown>, i: number, j: number) {
const q = t[i]; const q = t[i];
t[i] = t[j]; t[i] = t[j];
t[j] = q; t[j] = q;
@ -19,19 +19,19 @@ function swap(t: Array<any>, i: number, j: number) {
export const useSorter = () => { export const useSorter = () => {
function sortAToZ(list: Array<Recipe>) { function sortAToZ(list: Array<Recipe>) {
list.sort((a, b) => { list.sort((a, b) => {
const textA = a.name.toUpperCase(); const textA = a.name?.toUpperCase() ?? "";
const textB = b.name.toUpperCase(); const textB = b.name?.toUpperCase() ?? "";
return textA < textB ? -1 : textA > textB ? 1 : 0; return textA < textB ? -1 : textA > textB ? 1 : 0;
}); });
} }
function sortByCreated(list: Array<Recipe>) { function sortByCreated(list: Array<Recipe>) {
list.sort((a, b) => (a.dateAdded > b.dateAdded ? -1 : 1)); list.sort((a, b) => ((a.dateAdded ?? "") > (b.dateAdded ?? "") ? -1 : 1));
} }
function sortByUpdated(list: Array<Recipe>) { function sortByUpdated(list: Array<Recipe>) {
list.sort((a, b) => (a.dateUpdated > b.dateUpdated ? -1 : 1)); list.sort((a, b) => ((a.dateUpdated ?? "") > (b.dateUpdated ?? "") ? -1 : 1));
} }
function sortByRating(list: Array<Recipe>) { function sortByRating(list: Array<Recipe>) {
list.sort((a, b) => (a.rating > b.rating ? -1 : 1)); list.sort((a, b) => ((a.rating ?? 0) > (b.rating ?? 0) ? -1 : 1));
} }
function randomRecipe(list: Array<Recipe>): Recipe { function randomRecipe(list: Array<Recipe>): Recipe {

View File

@ -60,8 +60,7 @@ export const useCookbooks = function () {
async createOne() { async createOne() {
loading.value = true; loading.value = true;
const { data } = await api.cookbooks.createOne({ const { data } = await api.cookbooks.createOne({
// @ts-ignore. I"m thinking this will always be defined. name: "Cookbook " + String((cookbookStore?.value?.length ?? 0) + 1),
name: "Cookbook " + String(cookbookStore?.value?.length + 1 || 1),
}); });
if (data && cookbookStore?.value) { if (data && cookbookStore?.value) {
cookbookStore.value.push(data); cookbookStore.value.push(data);

View File

@ -40,7 +40,7 @@ export const useMealplans = function (range: Ref<DateRange>) {
loading.value = false; loading.value = false;
return units; return units;
}, },
async refreshAll() { async refreshAll(this: void) {
loading.value = true; loading.value = true;
const query = { const query = {
start: format(range.value.start, "yyyy-MM-dd"), start: format(range.value.start, "yyyy-MM-dd"),

View File

@ -1,8 +1,10 @@
import { IncomingMessage } from "connect";
export const useAsyncKey = function () { export const useAsyncKey = function () {
return String(Date.now()); return String(Date.now());
}; };
export function detectServerBaseUrl(req: any) { export function detectServerBaseUrl(req?: IncomingMessage | null) {
if (!req || req === undefined) { if (!req || req === undefined) {
return ""; return "";
} }
@ -10,26 +12,27 @@ export function detectServerBaseUrl(req: any) {
const url = new URL(req.headers.referer); const url = new URL(req.headers.referer);
return `${url.protocol}//${url.host}`; return `${url.protocol}//${url.host}`;
} else if (req.headers.host) { } else if (req.headers.host) {
const protocol = req.connection.encrypted ? "https" : "http:"; // TODO Socket.encrypted doesn't exist. What is needed here?
// @ts-ignore
const protocol = req.socket.encrypted ? "https:" : "http:";
return `${protocol}//${req.headers.host}`; return `${protocol}//${req.headers.host}`;
} else if (req.connection.remoteAddress) { } else if (req.socket.remoteAddress) {
const protocol = req.connection.encrypted ? "https" : "http:"; // @ts-ignore
return `${protocol}//${req.connection.localAddress}:${req.connection.localPort}`; const protocol = req.socket.encrypted ? "https:" : "http:";
return `${protocol}//${req.socket.localAddress}:${req.socket.localPort}`;
} }
return ""; return "";
} }
export function uuid4() { export function uuid4() {
// @ts-ignore return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) =>
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => (parseInt(c) ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (parseInt(c) / 4)))).toString(16)
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
); );
} }
// https://stackoverflow.com/questions/28876300/deep-copying-array-of-nested-objects-in-javascript // https://stackoverflow.com/questions/28876300/deep-copying-array-of-nested-objects-in-javascript
const toString = Object.prototype.toString; export function deepCopy<T>(obj: T): T {
export function deepCopy(obj: any) {
let rv; let rv;
switch (typeof obj) { switch (typeof obj) {
@ -38,19 +41,19 @@ export function deepCopy(obj: any) {
// null => null // null => null
rv = null; rv = null;
} else { } else {
switch (toString.call(obj)) { switch (Object.prototype.toString.call(obj)) {
case "[object Array]": case "[object Array]":
// It's an array, create a new array with // It's an array, create a new array with
// deep copies of the entries // deep copies of the entries
rv = obj.map(deepCopy); rv = (obj as unknown as Array<unknown>).map(deepCopy);
break; break;
case "[object Date]": case "[object Date]":
// Clone the date // Clone the date
rv = new Date(obj); rv = new Date(obj as unknown as Date);
break; break;
case "[object RegExp]": case "[object RegExp]":
// Clone the RegExp // Clone the RegExp
rv = new RegExp(obj); rv = new RegExp(obj as unknown as RegExp);
break; break;
// ...probably a few others // ...probably a few others
default: default:
@ -70,5 +73,5 @@ export function deepCopy(obj: any) {
rv = obj; rv = obj;
break; break;
} }
return rv; return rv as T;
} }

View File

@ -3,11 +3,12 @@ const EMAIL_REGEX =
const URL_REGEX = /[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/; const URL_REGEX = /[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
export const validators = { export const validators: {[key: string]: (v: string) => boolean | string} = {
required: (v: string) => !!v || "This Field is Required", required: (v: string) => !!v || "This Field is Required",
email: (v: string) => !v || EMAIL_REGEX.test(v) || "Email Must Be Valid", 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", url: (v: string) => !v || URL_REGEX.test(v) || "Must Be A Valid URL",
minLength: (min: number) => (v: string) => !v || v.length >= min || `Must Be At Least ${min} Characters`, // TODO These appear to be unused?
maxLength: (max: number) => (v: string) => !v || v.length <= max || `Must Be At Most ${max} Characters`, // minLength: (min: number) => (v: string) => !v || v.length >= min || `Must Be At Least ${min} Characters`,
// maxLength: (max: number) => (v: string) => !v || v.length <= max || `Must Be At Most ${max} Characters`,
}; };

View File

@ -37,7 +37,6 @@ export default defineComponent({
middleware: "auth", middleware: "auth",
auth: true, auth: true,
setup() { setup() {
// @ts-ignore - $globals not found in type definition
const { $globals, i18n, $vuetify } = useContext(); const { $globals, i18n, $vuetify } = useContext();
const sidebar = ref<boolean | null>(null); const sidebar = ref<boolean | null>(null);
@ -124,4 +123,3 @@ export default defineComponent({

View File

@ -74,7 +74,6 @@ export default defineComponent({
middleware: "auth", middleware: "auth",
setup() { setup() {
const { cookbooks } = useCookbooks(); const { cookbooks } = useCookbooks();
// @ts-ignore
const { $globals, $auth, $vuetify } = useContext(); const { $globals, $auth, $vuetify } = useContext();
const isAdmin = computed(() => $auth.user?.admin); const isAdmin = computed(() => $auth.user?.admin);
@ -87,7 +86,7 @@ export default defineComponent({
console.log("toggleDark"); console.log("toggleDark");
} }
const sidebar = ref<Boolean | null>(null); const sidebar = ref<boolean | null>(null);
onMounted(() => { onMounted(() => {
sidebar.value = !$vuetify.breakpoint.md; sidebar.value = !$vuetify.breakpoint.md;

View File

@ -27,8 +27,10 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
export default { import { defineComponent, useContext, useMeta } from "@nuxtjs/composition-api";
export default defineComponent({
layout: "basic", layout: "basic",
props: { props: {
error: { error: {
@ -36,28 +38,23 @@ export default {
default: null, default: null,
}, },
}, },
data() { setup(props) {
useMeta({ title: props.error.statusCode === 404 ? "404 Not Found" : "An error occurred" });
const { $globals, i18n } = useContext();
const buttons = [
{ icon: $globals.icons.home, to: "/", text: i18n.t("general.home") },
{ icon: $globals.icons.primary, to: "/recipes/all", text: i18n.t("page.all-recipes") },
{ icon: $globals.icons.search, to: "/search", text: i18n.t("search.search") },
];
return { return {
pageNotFound: "404 Not Found", buttons,
otherError: "An error occurred", }
};
}, },
head() { // Needed for useMeta
const title = this.error.statusCode === 404 ? this.pageNotFound : this.otherError; head: {},
return { });
title,
};
},
computed: {
buttons() {
return [
{ icon: this.$globals.icons.home, to: "/", text: this.$t("general.home") },
{ icon: this.$globals.icons.primary, to: "/recipes/all", text: this.$t("page.all-recipes") },
{ icon: this.$globals.icons.search, to: "/search", text: this.$t("search.search") },
];
},
},
};
</script> </script>
<style scoped> <style scoped>

View File

@ -7,11 +7,11 @@
"build": "nuxt build", "build": "nuxt build",
"start": "nuxt start", "start": "nuxt start",
"generate": "nuxt generate", "generate": "nuxt generate",
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .", "lint:js": "eslint --ext \".ts,.js,.vue\" --ignore-path .gitignore .",
"lint": "yarn lint:js" "lint": "yarn lint:js"
}, },
"lint-staged": { "lint-staged": {
"*.{js,vue}": "eslint" "*.{ts,js,vue}": "eslint"
}, },
"dependencies": { "dependencies": {
"@adapttive/vue-markdown": "^4.0.1", "@adapttive/vue-markdown": "^4.0.1",

View File

@ -31,6 +31,7 @@ import { useGroups } from "~/composables/use-groups";
import { alert } from "~/composables/use-toast"; import { alert } from "~/composables/use-toast";
import { useUserForm } from "~/composables/use-users"; import { useUserForm } from "~/composables/use-users";
import { validators } from "~/composables/use-validators"; import { validators } from "~/composables/use-validators";
import { VForm } from "~/types/vuetify";
export default defineComponent({ export default defineComponent({
components: { components: {

View File

@ -126,7 +126,7 @@ export default defineComponent({
const router = useRouter(); const router = useRouter();
function handleRowClick(item: Group) { function handleRowClick(item: Group) {
router.push("/admin/manage/groups/" + item.id); router.push(`/admin/manage/groups/${item.id}`);
} }
return { ...toRefs(state), groups, refreshAllGroups, deleteGroup, createGroup, openDialog, handleRowClick }; return { ...toRefs(state), groups, refreshAllGroups, deleteGroup, createGroup, openDialog, handleRowClick };

View File

@ -44,6 +44,7 @@ import { useGroups } from "~/composables/use-groups";
import { alert } from "~/composables/use-toast"; import { alert } from "~/composables/use-toast";
import { useUserForm } from "~/composables/use-users"; import { useUserForm } from "~/composables/use-users";
import { validators } from "~/composables/use-validators"; import { validators } from "~/composables/use-validators";
import { VForm } from "~/types/vuetify";
export default defineComponent({ export default defineComponent({
layout: "admin", layout: "admin",

View File

@ -39,6 +39,7 @@ import { useAdminApi } from "~/composables/api";
import { useGroups } from "~/composables/use-groups"; import { useGroups } from "~/composables/use-groups";
import { useUserForm } from "~/composables/use-users"; import { useUserForm } from "~/composables/use-users";
import { validators } from "~/composables/use-validators"; import { validators } from "~/composables/use-validators";
import { VForm } from "~/types/vuetify";
export default defineComponent({ export default defineComponent({
layout: "admin", layout: "admin",

View File

@ -77,7 +77,7 @@ export default defineComponent({
const { loading, deleteUser } = useUser(refreshAllUsers); const { loading, deleteUser } = useUser(refreshAllUsers);
function handleRowClick(item: UserOut) { function handleRowClick(item: UserOut) {
router.push("/admin/manage/users/" + item.id); router.push(`/admin/manage/users/${item.id}`);
} }
// ========================================================== // ==========================================================

View File

@ -199,7 +199,7 @@ export default defineComponent({
return false; return false;
}); });
function getColor(booly: boolean | any, warning = false) { function getColor(booly: unknown, warning = false) {
const falsey = warning ? "warning" : "error"; const falsey = warning ? "warning" : "error";
return booly ? "success" : falsey; return booly ? "success" : falsey;
} }
@ -207,7 +207,6 @@ export default defineComponent({
// ============================================================ // ============================================================
// General About Info // General About Info
// @ts-ignore
const { $globals, i18n } = useContext(); const { $globals, i18n } = useContext();
// @ts-ignore // @ts-ignore

View File

@ -151,7 +151,6 @@ import { useNotifications } from "@/composables/use-notifications";
export default defineComponent({ export default defineComponent({
layout: "admin", layout: "admin",
setup() { setup() {
// @ts-ignore -> Ignore missing $globals
const { i18n } = useContext(); const { i18n } = useContext();
const state = reactive({ const state = reactive({

View File

@ -209,7 +209,7 @@ export default defineComponent({
const loggingIn = ref(false); const loggingIn = ref(false);
const allowSignup = computed(() => context.env.ALLOW_SIGNUP); const allowSignup = computed(() => context.env.ALLOW_SIGNUP as boolean);
async function authenticate() { async function authenticate() {
loggingIn.value = true; loggingIn.value = true;
@ -221,7 +221,11 @@ export default defineComponent({
try { try {
await $auth.loginWith("local", { data: formData }); await $auth.loginWith("local", { data: formData });
} catch (error) { } catch (error) {
if (error.response.status === 401) { // TODO Check if error is an AxiosError, but isAxiosError is not working right now
// See https://github.com/nuxt-community/axios-module/issues/550
// Import $axios from useContext()
// if ($axios.isAxiosError(error) && error.response?.status === 401) {
if (error.response?.status === 401) {
alert.error("Invalid Credentials"); alert.error("Invalid Credentials");
} else { } else {
alert.error("Something Went Wrong!"); alert.error("Something Went Wrong!");

View File

@ -210,6 +210,7 @@ import { useMealplans, planTypeOptions } from "~/composables/use-group-mealplan"
import { useRecipes, allRecipes } from "~/composables/recipes"; import { useRecipes, allRecipes } from "~/composables/recipes";
import RecipeCardImage from "~/components/Domain/Recipe/RecipeCardImage.vue"; import RecipeCardImage from "~/components/Domain/Recipe/RecipeCardImage.vue";
import RecipeCard from "~/components/Domain/Recipe/RecipeCard.vue"; import RecipeCard from "~/components/Domain/Recipe/RecipeCard.vue";
import { PlanEntryType } from "~/types/api-types/meal-plan";
export default defineComponent({ export default defineComponent({
components: { components: {
@ -238,7 +239,7 @@ export default defineComponent({
useRecipes(true, true); useRecipes(true, true);
function filterMealByDate(date: Date) { function filterMealByDate(date: Date) {
if (!mealplans.value) return; if (!mealplans.value) return [];
return mealplans.value.filter((meal) => { return mealplans.value.filter((meal) => {
const mealDate = parseISO(meal.date); const mealDate = parseISO(meal.date);
return isSameDay(mealDate, date); return isSameDay(mealDate, date);
@ -263,14 +264,12 @@ export default defineComponent({
// The drop was cancelled, unsure if anything needs to be done? // The drop was cancelled, unsure if anything needs to be done?
console.log("Cancel Move Event"); console.log("Cancel Move Event");
} else { } else {
// A Meal was moved, set the new date value and make a update request and refresh the meals // A Meal was moved, set the new date value and make an update request and refresh the meals
const fromMealsByIndex = evt.from.getAttribute("data-index"); const fromMealsByIndex = parseInt(evt.from.getAttribute("data-index") ?? "");
const toMealsByIndex = evt.to.getAttribute("data-index"); const toMealsByIndex = parseInt(evt.to.getAttribute("data-index") ?? "");
if (fromMealsByIndex) { if (!isNaN(fromMealsByIndex) && !isNaN(toMealsByIndex)) {
// @ts-ignore
const mealData = mealsByDate.value[fromMealsByIndex].meals[evt.oldIndex as number]; const mealData = mealsByDate.value[fromMealsByIndex].meals[evt.oldIndex as number];
// @ts-ignore
const destDate = mealsByDate.value[toMealsByIndex].date; const destDate = mealsByDate.value[toMealsByIndex].date;
mealData.date = format(destDate, "yyyy-MM-dd"); mealData.date = format(destDate, "yyyy-MM-dd");
@ -282,13 +281,12 @@ export default defineComponent({
const mealsByDate = computed(() => { const mealsByDate = computed(() => {
return days.value.map((day) => { return days.value.map((day) => {
return { date: day, meals: filterMealByDate(day as any) }; return { date: day, meals: filterMealByDate(day) };
}); });
}); });
const days = computed(() => { const days = computed(() => {
return Array.from(Array(8).keys()).map( return Array.from(Array(8).keys()).map(
// @ts-ignore
(i) => new Date(weekRange.value.start.getTime() + i * 24 * 60 * 60 * 1000) (i) => new Date(weekRange.value.start.getTime() + i * 24 * 60 * 60 * 1000)
); );
}); });
@ -304,7 +302,7 @@ export default defineComponent({
watch(dialog, () => { watch(dialog, () => {
if (dialog.note) { if (dialog.note) {
newMeal.recipeId = null; newMeal.recipeId = undefined;
} }
newMeal.title = ""; newMeal.title = "";
newMeal.text = ""; newMeal.text = "";
@ -314,13 +312,12 @@ export default defineComponent({
date: "", date: "",
title: "", title: "",
text: "", text: "",
recipeId: null as Number | null, recipeId: undefined as number | undefined,
entryType: "dinner", entryType: "dinner" as PlanEntryType,
}); });
function openDialog(date: Date) { function openDialog(date: Date) {
newMeal.date = format(date, "yyyy-MM-dd"); newMeal.date = format(date, "yyyy-MM-dd");
// @ts-ignore
state.createMealDialog = true; state.createMealDialog = true;
} }
@ -329,21 +326,20 @@ export default defineComponent({
newMeal.title = ""; newMeal.title = "";
newMeal.text = ""; newMeal.text = "";
newMeal.entryType = "dinner"; newMeal.entryType = "dinner";
newMeal.recipeId = null; newMeal.recipeId = undefined;
} }
async function randomMeal(date: Date) { async function randomMeal(date: Date) {
// TODO: Refactor to use API call to get random recipe // TODO: Refactor to use API call to get random recipe
// @ts-ignore const randomRecipe = allRecipes.value?.[Math.floor(Math.random() * allRecipes.value.length)];
const randomRecipe = allRecipes.value[Math.floor(Math.random() * allRecipes.value.length)]; if (!randomRecipe) return;
newMeal.date = format(date, "yyyy-MM-dd"); newMeal.date = format(date, "yyyy-MM-dd");
newMeal.recipeId = randomRecipe.id || null; newMeal.recipeId = randomRecipe.id;
console.log(newMeal.recipeId, randomRecipe.id); console.log(newMeal.recipeId, randomRecipe.id);
// @ts-ignore
await actions.createOne({ ...newMeal }); await actions.createOne({ ...newMeal });
resetDialog(); resetDialog();
} }

View File

@ -94,12 +94,12 @@ export default defineComponent({
const { recipeImage } = useStaticRoutes(); const { recipeImage } = useStaticRoutes();
function getIngredientByRefId(refId: String) { function getIngredientByRefId(refId: string) {
if (!recipe.value) { if (!recipe.value) {
return; return;
} }
const ing = recipe?.value.recipeIngredient.find((ing) => ing.referenceId === refId) || ""; const ing = recipe?.value.recipeIngredient?.find((ing) => ing.referenceId === refId) || "";
if (ing === "") { if (ing === "") {
return ""; return "";
} }

View File

@ -34,7 +34,7 @@
:key="imageKey" :key="imageKey"
:max-width="enableLandscape ? null : '50%'" :max-width="enableLandscape ? null : '50%'"
:min-height="hideImage ? '50' : imageHeight" :min-height="hideImage ? '50' : imageHeight"
:src="recipeImage(recipe.slug, imageKey)" :src="recipeImage(recipe.slug, '', imageKey)"
class="d-print-none" class="d-print-none"
@error="hideImage = true" @error="hideImage = true"
> >
@ -561,7 +561,6 @@ export default defineComponent({
const { recipeImage } = useStaticRoutes(); const { recipeImage } = useStaticRoutes();
// @ts-ignore
const { $vuetify } = useContext(); const { $vuetify } = useContext();
// =========================================================================== // ===========================================================================
@ -623,7 +622,7 @@ export default defineComponent({
}); });
async function uploadImage(fileObject: File) { async function uploadImage(fileObject: File) {
if (!recipe.value) { if (!recipe.value || !recipe.value.slug) {
return; return;
} }
const newVersion = await api.recipes.updateImage(recipe.value.slug, fileObject); const newVersion = await api.recipes.updateImage(recipe.value.slug, fileObject);
@ -656,8 +655,8 @@ export default defineComponent({
referenceId: uuid4(), referenceId: uuid4(),
title: "", title: "",
note: x, note: x,
unit: null, unit: undefined,
food: null, food: undefined,
disableAmount: true, disableAmount: true,
quantity: 1, quantity: 1,
}; };
@ -671,8 +670,8 @@ export default defineComponent({
referenceId: uuid4(), referenceId: uuid4(),
title: "", title: "",
note: "", note: "",
unit: null, unit: undefined,
food: null, food: undefined,
disableAmount: true, disableAmount: true,
quantity: 1, quantity: 1,
}); });
@ -762,7 +761,6 @@ export default defineComponent({
head: {}, head: {},
computed: { computed: {
imageHeight() { imageHeight() {
// @ts-ignore
return this.$vuetify.breakpoint.xs ? "200" : "400"; return this.$vuetify.breakpoint.xs ? "200" : "400";
}, },
}, },

View File

@ -83,17 +83,18 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, useRoute, useRouter } from "@nuxtjs/composition-api"; import { defineComponent, ref, useRoute, useRouter } from "@nuxtjs/composition-api";
import { until, invoke } from "@vueuse/core"; import { invoke, until } from "@vueuse/core";
import { Food, ParsedIngredient, Parser } from "~/api/class-interfaces/recipes/types"; import { ParsedIngredient, Parser } from "~/api/class-interfaces/recipes/types";
import { CreateIngredientFood, CreateIngredientUnit, IngredientFood, IngredientUnit } from "~/types/api-types/recipe";
import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientEditor.vue"; import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientEditor.vue";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { useRecipe, useFoods, useUnits } from "~/composables/recipes"; import { useFoods, useRecipe, useUnits } from "~/composables/recipes";
import { RecipeIngredientUnit } from "~/types/api-types/recipe";
interface Error { interface Error {
ingredientIndex: number; ingredientIndex: number;
unitError: Boolean; unitError: boolean;
unitErrorMessage: string; unitErrorMessage: string;
foodError: Boolean; foodError: boolean;
foodErrorMessage: string; foodErrorMessage: string;
} }
@ -125,10 +126,10 @@ export default defineComponent({
const parsedIng = ref<ParsedIngredient[]>([]); const parsedIng = ref<ParsedIngredient[]>([]);
async function fetchParsed() { async function fetchParsed() {
if (!recipe.value) { if (!recipe.value || !recipe.value.recipeIngredient) {
return; return;
} }
const raw = recipe.value.recipeIngredient.map((ing) => ing.note); const raw = recipe.value.recipeIngredient.map((ing) => ing.note ?? "");
const { data } = await api.recipes.parseIngredients(parser.value, raw); const { data } = await api.recipes.parseIngredients(parser.value, raw);
if (data) { if (data) {
@ -187,7 +188,7 @@ export default defineComponent({
const errors = ref<Error[]>([]); const errors = ref<Error[]>([]);
function checkForUnit(unit: RecipeIngredientUnit | null) { function checkForUnit(unit?: IngredientUnit | CreateIngredientUnit) {
if (!unit) { if (!unit) {
return false; return false;
} }
@ -197,7 +198,7 @@ export default defineComponent({
return false; return false;
} }
function checkForFood(food: Food | null) { function checkForFood(food?: IngredientFood | CreateIngredientFood) {
if (!food) { if (!food) {
return false; return false;
} }
@ -207,7 +208,7 @@ export default defineComponent({
return false; return false;
} }
async function createFood(food: Food, index: number) { async function createFood(food: CreateIngredientFood, index: number) {
workingFoodData.name = food.name; workingFoodData.name = food.name;
await actions.createOne(); await actions.createOne();
errors.value[index].foodError = false; errors.value[index].foodError = false;
@ -227,16 +228,14 @@ export default defineComponent({
return ing; return ing;
} }
// Get food from foods // Get food from foods
const food = foods.value.find((f) => f.name === ing.food?.name); ing.food = foods.value.find((f) => f.name === ing.food?.name);
ing.food = food || null;
// Get unit from units // Get unit from units
const unit = units.value.find((u) => u.name === ing.unit?.name); ing.unit = units.value.find((u) => u.name === ing.unit?.name);
ing.unit = unit || null;
return ing; return ing;
}); });
if (!recipe.value) { if (!recipe.value || !recipe.value.slug) {
return; return;
} }

View File

@ -314,7 +314,17 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, toRefs, ref, useRouter, useContext } from "@nuxtjs/composition-api"; import {
defineComponent,
reactive,
toRefs,
ref,
useRouter,
useContext,
computed,
useRoute
} from "@nuxtjs/composition-api";
import { AxiosResponse } from "axios";
// @ts-ignore No Types for v-jsoneditor // @ts-ignore No Types for v-jsoneditor
import VJsoneditor from "v-jsoneditor"; import VJsoneditor from "v-jsoneditor";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
@ -322,6 +332,8 @@ import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategory
import { validators } from "~/composables/use-validators"; import { validators } from "~/composables/use-validators";
import { Recipe } from "~/types/api-types/recipe"; import { Recipe } from "~/types/api-types/recipe";
import { alert } from "~/composables/use-toast"; import { alert } from "~/composables/use-toast";
import { VForm} from "~/types/vuetify";
export default defineComponent({ export default defineComponent({
components: { VJsoneditor, RecipeCategoryTagSelector }, components: { VJsoneditor, RecipeCategoryTagSelector },
setup() { setup() {
@ -330,7 +342,6 @@ export default defineComponent({
loading: false, loading: false,
}); });
// @ts-ignore - $globals not found in type definition
const { $globals } = useContext(); const { $globals } = useContext();
const tabs = [ const tabs = [
@ -362,20 +373,39 @@ export default defineComponent({
]; ];
const api = useUserApi(); const api = useUserApi();
const route = useRoute();
const router = useRouter(); const router = useRouter();
function handleResponse(response: any, edit: Boolean = false) { function handleResponse(response: AxiosResponse<string> | null, edit = false) {
if (response?.status !== 201) { if (response?.status !== 201) {
state.error = true; state.error = true;
state.loading = false; state.loading = false;
return; return;
} }
router.push(`/recipe/${response.data}?edit=${edit}`); router.push(`/recipe/${response.data}?edit=${edit.toString()}`);
} }
const tab = computed({
set(tab: string) {
router.replace({ query: { ...route.value.query, tab } });
},
get() {
return route.value.query.tab as string;
},
});
const recipeUrl = computed({
set(recipe_import_url: string) {
recipe_import_url = recipe_import_url.trim()
router.replace({ query: { ...route.value.query, recipe_import_url } });
},
get() {
return route.value.query.recipe_import_url as string;
},
});
// =================================================== // ===================================================
// Recipe Debug URL Scraper // Recipe Debug URL Scraper
// @ts-ignore
const debugTreeView = ref(false); const debugTreeView = ref(false);
@ -425,6 +455,8 @@ export default defineComponent({
return; return;
} }
const { response } = await api.recipes.createOne({ name }); const { response } = await api.recipes.createOne({ name });
// TODO createOne claims to return a Recipe, but actually the API only returns a string
// @ts-ignore
handleResponse(response, true); handleResponse(response, true);
} }
@ -467,6 +499,8 @@ export default defineComponent({
} }
return { return {
tab,
recipeUrl,
bulkCreate, bulkCreate,
bulkUrls, bulkUrls,
lockBulkImport, lockBulkImport,
@ -490,30 +524,6 @@ export default defineComponent({
title: this.$t("general.create") as string, title: this.$t("general.create") as string,
}; };
}, },
// Computed State is used because of the limitation of vue-composition-api in v2.0
computed: {
tab: {
set(tab) {
// @ts-ignore
this.$router.replace({ query: { ...this.$route.query, tab } });
},
get() {
// @ts-ignore
return this.$route.query.tab;
},
},
recipeUrl: {
set(recipe_import_url) {
// @ts-ignore
recipe_import_url = recipe_import_url.trim()
this.$router.replace({ query: { ...this.$route.query, recipe_import_url } });
},
get() {
// @ts-ignore
return this.$route.query.recipe_import_url;
},
},
},
}); });
</script> </script>

View File

@ -104,6 +104,7 @@ import { validators } from "@/composables/use-validators";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast"; import { alert } from "~/composables/use-toast";
import { useRouterQuery } from "@/composables/use-router"; import { useRouterQuery } from "@/composables/use-router";
import { VForm} from "~/types/vuetify";
export default defineComponent({ export default defineComponent({
layout: "basic", layout: "basic",

View File

@ -325,11 +325,11 @@ export default defineComponent({
if (data) { if (data) {
if (data && data !== undefined) { if (data && data !== undefined) {
console.log("Computed Meta. RefKey="); console.log("Computed Meta. RefKey=");
const imageURL = recipeImage(data.slug); const imageURL = data.slug ? recipeImage(data.slug) : undefined;
title.value = data.name; title.value = data.name;
meta.value = [ meta.value = [
{ hid: "og:title", property: "og:title", content: data.name }, { hid: "og:title", property: "og:title", content: data.name ?? "" },
// @ts-ignore // @ts-ignore
{ {
hid: "og:desc", hid: "og:desc",
@ -339,7 +339,7 @@ export default defineComponent({
{ {
hid: "og-image", hid: "og-image",
property: "og:image", property: "og:image",
content: imageURL, content: imageURL ?? "",
}, },
// @ts-ignore // @ts-ignore
{ {
@ -360,7 +360,6 @@ export default defineComponent({
} }
}); });
// @ts-ignore
const { $vuetify } = useContext(); const { $vuetify } = useContext();
const enableLandscape = computed(() => { const enableLandscape = computed(() => {
@ -400,7 +399,6 @@ export default defineComponent({
head: {}, head: {},
computed: { computed: {
imageHeight() { imageHeight() {
// @ts-ignore
return this.$vuetify.breakpoint.xs ? "200" : "400"; return this.$vuetify.breakpoint.xs ? "200" : "400";
}, },
}, },

View File

@ -79,7 +79,6 @@ const MIGRATIONS = {
export default defineComponent({ export default defineComponent({
setup() { setup() {
// @ts-ignore
const { $globals } = useContext(); const { $globals } = useContext();
const api = useUserApi(); const api = useUserApi();

View File

@ -176,7 +176,6 @@ export default defineComponent({
setup() { setup() {
const { getAllRecipes, refreshRecipes } = useRecipes(true, true); const { getAllRecipes, refreshRecipes } = useRecipes(true, true);
// @ts-ignore
const { $globals } = useContext(); const { $globals } = useContext();
const selected = ref<Recipe[]>([]); const selected = ref<Recipe[]>([]);
@ -272,7 +271,7 @@ export default defineComponent({
async function exportSelected() { async function exportSelected() {
loading.value = true; loading.value = true;
const { data } = await api.bulk.bulkExport({ const { data } = await api.bulk.bulkExport({
recipes: selected.value.map((x: Recipe) => x.slug), recipes: selected.value.map((x: Recipe) => x.slug ?? ""),
exportType: "json", exportType: "json",
}); });
@ -289,7 +288,7 @@ export default defineComponent({
async function tagSelected() { async function tagSelected() {
loading.value = true; loading.value = true;
const recipes = selected.value.map((x: Recipe) => x.slug); const recipes = selected.value.map((x: Recipe) => x.slug ?? "");
await api.bulk.bulkTag({ recipes, tags: toSetTags.value }); await api.bulk.bulkTag({ recipes, tags: toSetTags.value });
await refreshRecipes(); await refreshRecipes();
resetAll(); resetAll();
@ -300,7 +299,7 @@ export default defineComponent({
async function categorizeSelected() { async function categorizeSelected() {
loading.value = true; loading.value = true;
const recipes = selected.value.map((x: Recipe) => x.slug); const recipes = selected.value.map((x: Recipe) => x.slug ?? "");
await api.bulk.bulkCategorize({ recipes, categories: toSetCategories.value }); await api.bulk.bulkCategorize({ recipes, categories: toSetCategories.value });
await refreshRecipes(); await refreshRecipes();
resetAll(); resetAll();
@ -309,7 +308,7 @@ export default defineComponent({
async function deleteSelected() { async function deleteSelected() {
loading.value = true; loading.value = true;
const recipes = selected.value.map((x: Recipe) => x.slug); const recipes = selected.value.map((x: Recipe) => x.slug ?? "");
const { response, data } = await api.bulk.bulkDelete({ recipes }); const { response, data } = await api.bulk.bulkDelete({ recipes });
@ -327,6 +326,7 @@ export default defineComponent({
title: "Tag Recipes", title: "Tag Recipes",
mode: MODES.tag, mode: MODES.tag,
tag: "", tag: "",
// eslint-disable-next-line @typescript-eslint/no-empty-function
callback: () => {}, callback: () => {},
icon: $globals.icons.tags, icon: $globals.icons.tags,
}); });

View File

@ -30,7 +30,7 @@
</v-container> </v-container>
</template> </template>
<script> <script lang="ts">
import { defineComponent, useRoute, reactive, toRefs, onMounted } from "@nuxtjs/composition-api"; import { defineComponent, useRoute, reactive, toRefs, onMounted } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";

View File

@ -66,6 +66,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, useContext, ref } from "@nuxtjs/composition-api"; import { computed, defineComponent, useContext, ref } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { VForm } from "~/types/vuetify";
export default defineComponent({ export default defineComponent({
setup() { setup() {

View File

@ -115,6 +115,7 @@
import { ref, reactive, defineComponent, computed, useContext, watch } from "@nuxtjs/composition-api"; import { ref, reactive, defineComponent, computed, useContext, watch } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import UserAvatar from "~/components/Domain/User/UserAvatar.vue"; import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
import { VForm } from "~/types/vuetify";
export default defineComponent({ export default defineComponent({
components: { components: {

View File

@ -1,11 +1,10 @@
import { Plugin } from "@nuxt/types"
import { useDark } from "@vueuse/core"; import { useDark } from "@vueuse/core";
export default ({ $vuetify }: any) => { const darkModePlugin: Plugin = ({ $vuetify }, _) => {
const isDark = useDark(); const isDark = useDark();
if (isDark.value) { $vuetify.theme.dark = isDark.value;
$vuetify.theme.dark = true;
} else {
$vuetify.theme.dark = false;
}
}; };
export default darkModePlugin;

View File

@ -1,7 +1,27 @@
import { Plugin } from "@nuxt/types"
import { icons } from "~/utils/icons"; import { icons } from "~/utils/icons";
import { Icon } from "~/utils/icons/icon-type";
// eslint-disable-next-line no-empty-pattern interface Globals {
export default ({}, inject: any) => { icons: Icon;
// Inject $hello(msg) in Vue, context and store. }
inject("globals", { icons });
declare module "vue/types/vue" {
interface Vue {
$globals: Globals;
}
}
declare module "@nuxt/types" {
interface Context {
$globals: Globals;
}
}
const globalsPlugin: Plugin = (_, inject) => {
inject("globals", {
icons
});
}; };
export default globalsPlugin

View File

@ -1,7 +1,11 @@
export default ({ $vuetify, $config }: any) => { import { Plugin } from "@nuxt/types"
$vuetify.theme.themes = $config.themes;
const themePlugin: Plugin = ({ $vuetify, $config }) => {
$vuetify.theme.themes = $config.themes as typeof $vuetify.theme.themes
if ($config.useDark) { if ($config.useDark) {
$vuetify.theme.dark = true; $vuetify.theme.dark = true;
} }
}; };
export default themePlugin;

View File

@ -1,7 +1,8 @@
import { Plugin } from "@nuxt/types"
import { NuxtAxiosInstance } from "@nuxtjs/axios"; import { NuxtAxiosInstance } from "@nuxtjs/axios";
import { alert } from "~/composables/use-toast"; import { alert } from "~/composables/use-toast";
export default function ({ $axios }: { $axios: NuxtAxiosInstance }) { const toastPlugin: Plugin = ({ $axios }: { $axios: NuxtAxiosInstance }) => {
$axios.onResponse((response) => { $axios.onResponse((response) => {
if (response.data.message) { if (response.data.message) {
alert.info(response.data.message); alert.info(response.data.message);
@ -13,3 +14,5 @@ export default function ({ $axios }: { $axios: NuxtAxiosInstance }) {
} }
}); });
} }
export default toastPlugin;

View File

@ -16,8 +16,9 @@
"~/*": ["./*"], "~/*": ["./*"],
"@/*": ["./*"] "@/*": ["./*"]
}, },
"types": ["@nuxt/types", "@nuxtjs/axios", "@types/node", "@nuxtjs/i18n", "@nuxtjs/auth-next", "@types/sortablejs"] "types": ["@nuxt/types", "@nuxtjs/axios", "@types/node", "@nuxtjs/i18n", "@nuxtjs/auth-next", "@nuxtjs/vuetify", "@types/sortablejs"]
}, },
"include": ["**/*", ".eslintrc.js"],
"exclude": ["node_modules", ".nuxt", "dist"], "exclude": ["node_modules", ".nuxt", "dist"],
"vueCompilerOptions": { "vueCompilerOptions": {
"experimentalCompatMode": 2 "experimentalCompatMode": 2

View File

@ -5,6 +5,26 @@
/* Do not modify it by hand - just update the pydantic models and then re-run the script /* Do not modify it by hand - just update the pydantic models and then re-run the script
*/ */
export interface AdminAboutInfo {
production: boolean;
version: string;
demoStatus: boolean;
versionLatest: string;
apiPort: number;
apiDocs: boolean;
dbType: string;
dbUrl: string;
defaultGroup: string;
}
export interface AllBackups {
imports: BackupFile[];
templates: string[];
}
export interface BackupFile {
name: string;
date: string;
size: string;
}
export interface AppInfo { export interface AppInfo {
production: boolean; production: boolean;
version: string; version: string;
@ -17,36 +37,31 @@ export interface AppStatistics {
uncategorizedRecipes: number; uncategorizedRecipes: number;
untaggedRecipes: number; untaggedRecipes: number;
} }
export interface BackupJob {
tag?: string;
options: BackupOptions;
templates?: string[];
}
export interface BackupOptions { export interface BackupOptions {
recipes?: boolean; recipes?: boolean;
settings?: boolean; settings?: boolean;
pages?: boolean;
themes?: boolean; themes?: boolean;
groups?: boolean; groups?: boolean;
users?: boolean; users?: boolean;
notifications?: boolean; notifications?: boolean;
} }
export interface CategoryBase { export interface CheckAppConfig {
name: string; emailReady?: boolean;
id: number; ldapReady?: boolean;
slug: string; baseUrlSet?: boolean;
} }
export interface ChowdownURL { export interface ChowdownURL {
url: string; url: string;
} }
export interface Colors { export interface CommentImport {
primary?: string; name: string;
accent?: string; status: boolean;
secondary?: string; exception?: string;
success?: string; }
info?: string; export interface CreateBackup {
warning?: string; tag?: string;
error?: string; options: BackupOptions;
templates?: string[];
} }
export interface CustomPageBase { export interface CustomPageBase {
name: string; name: string;
@ -62,51 +77,87 @@ export interface RecipeCategoryResponse {
} }
export interface Recipe { export interface Recipe {
id?: number; id?: number;
name: string; userId?: string;
slug: string; groupId?: string;
name?: string;
slug?: string;
image?: unknown; image?: unknown;
description?: string;
recipeCategory?: string[];
tags?: string[];
rating?: number;
dateAdded?: string;
dateUpdated?: string;
recipeYield?: string; recipeYield?: string;
recipeIngredient?: RecipeIngredient[];
recipeInstructions?: RecipeStep[];
nutrition?: Nutrition;
tools?: string[];
totalTime?: string; totalTime?: string;
prepTime?: string; prepTime?: string;
cookTime?: string;
performTime?: string; performTime?: string;
description?: string;
recipeCategory?: RecipeTag[];
tags?: RecipeTag[];
tools?: RecipeTool[];
rating?: number;
orgURL?: string;
recipeIngredient?: RecipeIngredient[];
dateAdded?: string;
dateUpdated?: string;
recipeInstructions?: RecipeStep[];
nutrition?: Nutrition;
settings?: RecipeSettings; settings?: RecipeSettings;
assets?: RecipeAsset[]; assets?: RecipeAsset[];
notes?: RecipeNote[]; notes?: RecipeNote[];
orgURL?: string;
extras?: { extras?: {
[k: string]: unknown; [k: string]: unknown;
}; };
comments?: CommentOut[]; comments?: RecipeCommentOut[];
}
export interface RecipeTag {
name: string;
slug: string;
}
export interface RecipeTool {
name: string;
slug: string;
id?: number;
onHand?: boolean;
} }
export interface RecipeIngredient { export interface RecipeIngredient {
title?: string; title?: string;
note?: string; note?: string;
unit?: RecipeIngredientUnit; unit?: IngredientUnit | CreateIngredientUnit;
food?: RecipeIngredientFood; food?: IngredientFood | CreateIngredientFood;
disableAmount?: boolean; disableAmount?: boolean;
quantity?: number; quantity?: number;
referenceId?: string;
} }
export interface RecipeIngredientUnit { export interface IngredientUnit {
name?: string; name: string;
description?: string; description?: string;
fraction?: boolean;
abbreviation?: string;
id: number;
} }
export interface RecipeIngredientFood { export interface CreateIngredientUnit {
name?: string; name: string;
description?: string;
fraction?: boolean;
abbreviation?: string;
}
export interface IngredientFood {
name: string;
description?: string;
id: number;
}
export interface CreateIngredientFood {
name: string;
description?: string; description?: string;
} }
export interface RecipeStep { export interface RecipeStep {
id?: string;
title?: string; title?: string;
text: string; text: string;
ingredientReferences?: IngredientReferences[];
}
/**
* A list of ingredient references.
*/
export interface IngredientReferences {
referenceId?: string;
} }
export interface Nutrition { export interface Nutrition {
calories?: string; calories?: string;
@ -124,6 +175,7 @@ export interface RecipeSettings {
landscapeView?: boolean; landscapeView?: boolean;
disableComments?: boolean; disableComments?: boolean;
disableAmount?: boolean; disableAmount?: boolean;
locked?: boolean;
} }
export interface RecipeAsset { export interface RecipeAsset {
name: string; name: string;
@ -134,12 +186,13 @@ export interface RecipeNote {
title: string; title: string;
text: string; text: string;
} }
export interface CommentOut { export interface RecipeCommentOut {
recipeId: number;
text: string; text: string;
id: number; id: string;
uuid: string; createdAt: string;
recipeSlug: string; updateAt: string;
dateAdded: string; userId: string;
user: UserBase; user: UserBase;
} }
export interface UserBase { export interface UserBase {
@ -159,16 +212,6 @@ export interface CustomPageOut {
categories?: RecipeCategoryResponse[]; categories?: RecipeCategoryResponse[];
id: number; id: number;
} }
export interface DebugInfo {
production: boolean;
version: string;
demoStatus: boolean;
apiPort: number;
apiDocs: boolean;
dbType: string;
dbUrl: string;
defaultGroup: string;
}
export interface GroupImport { export interface GroupImport {
name: string; name: string;
status: boolean; status: boolean;
@ -182,7 +225,6 @@ export interface ImportBase {
export interface ImportJob { export interface ImportJob {
recipes?: boolean; recipes?: boolean;
settings?: boolean; settings?: boolean;
pages?: boolean;
themes?: boolean; themes?: boolean;
groups?: boolean; groups?: boolean;
users?: boolean; users?: boolean;
@ -191,14 +233,6 @@ export interface ImportJob {
force?: boolean; force?: boolean;
rebase?: boolean; rebase?: boolean;
} }
export interface Imports {
imports: LocalBackup[];
templates: string[];
}
export interface LocalBackup {
name: string;
date: string;
}
export interface MigrationFile { export interface MigrationFile {
name: string; name: string;
date: string; date: string;
@ -229,23 +263,6 @@ export interface SettingsImport {
status: boolean; status: boolean;
exception?: string; exception?: string;
} }
export interface SiteSettings {
language?: string;
firstDayOfWeek?: number;
showRecent?: boolean;
cardsPerSection?: number;
categories?: CategoryBase[];
}
export interface SiteTheme {
id?: number;
name?: string;
colors?: Colors;
}
export interface ThemeImport {
name: string;
status: boolean;
exception?: string;
}
export interface UserImport { export interface UserImport {
name: string; name: string;
status: boolean; status: boolean;

View File

@ -0,0 +1,184 @@
/* tslint:disable */
/* eslint-disable */
/**
/* This file was automatically generated from pydantic models by running pydantic2ts.
/* Do not modify it by hand - just update the pydantic models and then re-run the script
*/
export interface CategoryBase {
name: string;
id: number;
slug: string;
}
export interface CreateCookBook {
name: string;
description?: string;
slug?: string;
position?: number;
categories?: CategoryBase[];
}
export interface ReadCookBook {
name: string;
description?: string;
slug?: string;
position?: number;
categories?: CategoryBase[];
id: number;
groupId: string;
}
export interface RecipeCategoryResponse {
name: string;
id: number;
slug: string;
recipes?: Recipe[];
}
export interface Recipe {
id?: number;
userId?: string;
groupId?: string;
name?: string;
slug?: string;
image?: unknown;
recipeYield?: string;
totalTime?: string;
prepTime?: string;
cookTime?: string;
performTime?: string;
description?: string;
recipeCategory?: RecipeTag[];
tags?: RecipeTag[];
tools?: RecipeTool[];
rating?: number;
orgURL?: string;
recipeIngredient?: RecipeIngredient[];
dateAdded?: string;
dateUpdated?: string;
recipeInstructions?: RecipeStep[];
nutrition?: Nutrition;
settings?: RecipeSettings;
assets?: RecipeAsset[];
notes?: RecipeNote[];
extras?: {
[k: string]: unknown;
};
comments?: RecipeCommentOut[];
}
export interface RecipeTag {
name: string;
slug: string;
}
export interface RecipeTool {
name: string;
slug: string;
id?: number;
onHand?: boolean;
}
export interface RecipeIngredient {
title?: string;
note?: string;
unit?: IngredientUnit | CreateIngredientUnit;
food?: IngredientFood | CreateIngredientFood;
disableAmount?: boolean;
quantity?: number;
referenceId?: string;
}
export interface IngredientUnit {
name: string;
description?: string;
fraction?: boolean;
abbreviation?: string;
id: number;
}
export interface CreateIngredientUnit {
name: string;
description?: string;
fraction?: boolean;
abbreviation?: string;
}
export interface IngredientFood {
name: string;
description?: string;
id: number;
}
export interface CreateIngredientFood {
name: string;
description?: string;
}
export interface RecipeStep {
id?: string;
title?: string;
text: string;
ingredientReferences?: IngredientReferences[];
}
/**
* A list of ingredient references.
*/
export interface IngredientReferences {
referenceId?: string;
}
export interface Nutrition {
calories?: string;
fatContent?: string;
proteinContent?: string;
carbohydrateContent?: string;
fiberContent?: string;
sodiumContent?: string;
sugarContent?: string;
}
export interface RecipeSettings {
public?: boolean;
showNutrition?: boolean;
showAssets?: boolean;
landscapeView?: boolean;
disableComments?: boolean;
disableAmount?: boolean;
locked?: boolean;
}
export interface RecipeAsset {
name: string;
icon: string;
fileName?: string;
}
export interface RecipeNote {
title: string;
text: string;
}
export interface RecipeCommentOut {
recipeId: number;
text: string;
id: string;
createdAt: string;
updateAt: string;
userId: string;
user: UserBase;
}
export interface UserBase {
id: number;
username?: string;
admin: boolean;
}
export interface RecipeCookBook {
name: string;
description?: string;
slug?: string;
position?: number;
categories: RecipeCategoryResponse[];
id: number;
groupId: string;
}
export interface SaveCookBook {
name: string;
description?: string;
slug?: string;
position?: number;
categories?: CategoryBase[];
groupId: string;
}
export interface UpdateCookBook {
name: string;
description?: string;
slug?: string;
position?: number;
categories?: CategoryBase[];
id: number;
}

View File

@ -0,0 +1,28 @@
/* tslint:disable */
/* eslint-disable */
/**
/* This file was automatically generated from pydantic models by running pydantic2ts.
/* Do not modify it by hand - just update the pydantic models and then re-run the script
*/
export interface CreateWebhook {
enabled?: boolean;
name?: string;
url?: string;
time?: string;
}
export interface ReadWebhook {
enabled?: boolean;
name?: string;
url?: string;
time?: string;
groupId: string;
id: number;
}
export interface SaveWebhook {
enabled?: boolean;
name?: string;
url?: string;
time?: string;
groupId: string;
}

View File

@ -5,6 +5,15 @@
/* Do not modify it by hand - just update the pydantic models and then re-run the script /* Do not modify it by hand - just update the pydantic models and then re-run the script
*/ */
export type PlanEntryType = "breakfast" | "lunch" | "dinner" | "snack";
export interface CreatePlanEntry {
date: string;
entryType?: PlanEntryType & string;
title?: string;
text?: string;
recipeId?: number;
}
export interface ListItem { export interface ListItem {
title?: string; title?: string;
text?: string; text?: string;
@ -36,9 +45,90 @@ export interface MealPlanOut {
startDate: string; startDate: string;
endDate: string; endDate: string;
planDays: MealDayIn[]; planDays: MealDayIn[];
uid: number; id: number;
shoppingList?: number; shoppingList?: number;
} }
export interface ReadPlanEntry {
date: string;
entryType?: PlanEntryType & string;
title?: string;
text?: string;
recipeId?: number;
id: number;
groupId: string;
recipe?: RecipeSummary;
}
export interface RecipeSummary {
id?: number;
userId?: string;
groupId?: string;
name?: string;
slug?: string;
image?: unknown;
recipeYield?: string;
totalTime?: string;
prepTime?: string;
cookTime?: string;
performTime?: string;
description?: string;
recipeCategory?: RecipeTag[];
tags?: RecipeTag[];
tools?: RecipeTool[];
rating?: number;
orgURL?: string;
recipeIngredient?: RecipeIngredient[];
dateAdded?: string;
dateUpdated?: string;
}
export interface RecipeTag {
name: string;
slug: string;
}
export interface RecipeTool {
name: string;
slug: string;
id?: number;
onHand?: boolean;
}
export interface RecipeIngredient {
title?: string;
note?: string;
unit?: IngredientUnit | CreateIngredientUnit;
food?: IngredientFood | CreateIngredientFood;
disableAmount?: boolean;
quantity?: number;
referenceId?: string;
}
export interface IngredientUnit {
name: string;
description?: string;
fraction?: boolean;
abbreviation?: string;
id: number;
}
export interface CreateIngredientUnit {
name: string;
description?: string;
fraction?: boolean;
abbreviation?: string;
}
export interface IngredientFood {
name: string;
description?: string;
id: number;
}
export interface CreateIngredientFood {
name: string;
description?: string;
}
export interface SavePlanEntry {
date: string;
entryType?: PlanEntryType & string;
title?: string;
text?: string;
recipeId?: number;
groupId: string;
}
export interface ShoppingListIn { export interface ShoppingListIn {
name: string; name: string;
group?: string; group?: string;
@ -50,3 +140,12 @@ export interface ShoppingListOut {
items: ListItem[]; items: ListItem[];
id: number; id: number;
} }
export interface UpdatePlanEntry {
date: string;
entryType?: PlanEntryType & string;
title?: string;
text?: string;
recipeId?: number;
id: number;
groupId: string;
}

Some files were not shown because too many files have changed in this diff Show More