1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-05 13:35:23 +02:00

feat: Migrate to Nuxt 3 framework (#5184)

Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
This commit is contained in:
Hoa (Kyle) Trinh 2025-06-20 00:09:12 +07:00 committed by GitHub
parent 89ab7fac25
commit c24d532608
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
403 changed files with 23959 additions and 19557 deletions

View file

@ -1,19 +1,17 @@
<template>
<div scoped-slot></div>
<div scoped-slot />
</template>
<script lang="ts">
import { defineComponent, useContext } from "@nuxtjs/composition-api";
/**
* Renderless component that only renders if the user is logged in.
* and has advanced options toggled.
*/
export default defineComponent({
export default defineNuxtComponent({
setup(_, ctx) {
const { $auth } = useContext();
const $auth = useMealieAuth();
const r = $auth?.user?.advanced || false;
const r = $auth.user.value?.advanced || false;
return () => {
return r ? ctx.slots.default?.() : null;

View file

@ -2,32 +2,33 @@
<v-tooltip
ref="copyToolTip"
v-model="show"
:color="copied? 'success lighten-1' : 'red lighten-1'"
:color="copied? 'success-lighten-1' : 'red-lighten-1'"
top
:open-on-hover="false"
:open-on-click="true"
close-delay="500"
transition="slide-y-transition"
>
<template #activator="{ on }">
<template #activator="{ props }">
<v-btn
variant="flat"
:icon="icon"
:color="color"
retain-focus-on-click
:class="btnClass"
:disabled="copyText !== '' ? false : true"
@click="
on.click;
textToClipboard();
"
@blur="on.blur"
v-bind="props"
@click="textToClipboard()"
>
<v-icon>{{ $globals.icons.contentCopy }}</v-icon>
{{ icon ? "" : $t("general.copy") }}
</v-btn>
</template>
<span>
<v-icon left dark>
<v-icon
start
dark
>
{{ $globals.icons.clipboardCheck }}
</v-icon>
<slot v-if="!isSupported"> {{ $t("general.your-browser-does-not-support-clipboard") }} </slot>
@ -37,11 +38,9 @@
</template>
<script lang="ts">
import { useClipboard } from "@vueuse/core"
import { defineComponent, ref } from "@nuxtjs/composition-api";
import { VTooltip } from "~/types/vuetify";
import { useClipboard } from "@vueuse/core";
export default defineComponent({
export default defineNuxtComponent({
props: {
copyText: {
type: String,
@ -61,7 +60,7 @@ export default defineComponent({
},
},
setup(props) {
const { copy, copied, isSupported } = useClipboard()
const { copy, copied, isSupported } = useClipboard();
const show = ref(false);
const copyToolTip = ref<VTooltip | null>(null);
@ -73,7 +72,7 @@ export default defineComponent({
if (isSupported.value) {
await copy(props.copyText);
if (copied.value) {
console.log(`Copied\n${props.copyText}`)
console.log(`Copied\n${props.copyText}`);
}
else {
console.warn("Copy failed: ", copied.value);

View file

@ -1,9 +1,24 @@
<template>
<v-form ref="file">
<input ref="uploader" class="d-none" type="file" :accept="accept" @change="onFileChanged" />
<input
ref="uploader"
class="d-none"
type="file"
:accept="accept"
@change="onFileChanged"
>
<slot v-bind="{ isSelecting, onButtonClick }">
<v-btn :loading="isSelecting" :small="small" :color="color" :text="textBtn" :disabled="disabled" @click="onButtonClick">
<v-icon left> {{ effIcon }}</v-icon>
<v-btn
:loading="isSelecting"
:small="small"
:color="color"
:variant="textBtn ? 'text' : 'elevated'"
:disabled="disabled"
@click="onButtonClick"
>
<v-icon start>
{{ effIcon }}
</v-icon>
{{ text ? text : defaultText }}
</v-btn>
</slot>
@ -11,12 +26,11 @@
</template>
<script lang="ts">
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
const UPLOAD_EVENT = "uploaded";
export default defineComponent({
export default defineNuxtComponent({
props: {
small: {
type: Boolean,
@ -57,14 +71,15 @@ export default defineComponent({
disabled: {
type: Boolean,
default: false,
}
},
},
setup(props, context) {
const file = ref<File | null>(null);
const uploader = ref<HTMLInputElement | null>(null);
const isSelecting = ref(false);
const { i18n, $globals } = useContext();
const i18n = useI18n();
const { $globals } = useNuxtApp();
const effIcon = props.icon ? props.icon : $globals.icons.upload;
const defaultText = i18n.t("general.upload");
@ -82,11 +97,15 @@ export default defineComponent({
const formData = new FormData();
formData.append(props.fileName, file.value);
const response = await api.upload.file(props.url, formData);
if (response) {
context.emit(UPLOAD_EVENT, response);
try {
const response = await api.upload.file(props.url, formData);
if (response) {
context.emit(UPLOAD_EVENT, response);
}
}
catch (e) {
console.error(e);
context.emit(UPLOAD_EVENT, null);
}
isSelecting.value = false;
}
@ -107,7 +126,7 @@ export default defineComponent({
() => {
isSelecting.value = false;
},
{ once: true }
{ once: true },
);
uploader.value?.click();
}

View file

@ -1,19 +1,36 @@
<template>
<div class="mx-auto my-3 justify-center" style="display: flex;">
<div
class="mx-auto my-3 justify-center"
style="display: flex;"
>
<div style="display: inline;">
<v-progress-circular :width="size.width" :size="size.size" color="primary lighten-2" indeterminate>
<v-progress-circular
:width="size.width"
:size="size.size"
color="primary-lighten-2"
indeterminate
>
<div class="text-center">
<v-icon :size="size.icon" color="primary lighten-2">
<v-icon
:size="size.icon"
color="primary-lighten-2"
>
{{ $globals.icons.primary }}
</v-icon>
<div v-if="large" class="text-small">
<div
v-if="large"
class="text-small"
>
<slot>
{{ (small || tiny) ? "" : waitingText }}
</slot>
</div>
</div>
</v-progress-circular>
<div v-if="!large" class="text-small">
<div
v-if="!large"
class="text-small"
>
<slot>
{{ (small || tiny) ? "" : waitingTextCalculated }}
</slot>
@ -23,9 +40,7 @@
</template>
<script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
export default defineComponent({
export default defineNuxtComponent({
props: {
loading: {
type: Boolean,
@ -50,7 +65,7 @@ export default defineComponent({
waitingText: {
type: String,
default: undefined,
}
},
},
setup(props) {
const size = computed(() => {
@ -67,7 +82,8 @@ export default defineComponent({
icon: 30,
size: 50,
};
} else if (props.large) {
}
else if (props.large) {
return {
width: 4,
icon: 120,
@ -81,7 +97,7 @@ export default defineComponent({
};
});
const { i18n } = useContext();
const i18n = useI18n();
const waitingTextCalculated = props.waitingText == null ? i18n.t("general.loading-recipes") : props.waitingText;
return {

View file

@ -1,17 +1,25 @@
<template>
<v-toolbar color="transparent" flat>
<BaseButton color="null" rounded secondary @click="$router.go(-1)">
<template #icon> {{ $globals.icons.arrowLeftBold }}</template>
<v-toolbar
color="transparent"
flat
>
<BaseButton
color="null"
rounded
secondary
@click="$router.go(-1)"
>
<template #icon>
{{ $globals.icons.arrowLeftBold }}
</template>
{{ $t('general.back') }}
</BaseButton>
<slot></slot>
<slot />
</v-toolbar>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
export default defineNuxtComponent({
props: {
back: {
type: Boolean,

View file

@ -1,49 +1,64 @@
<template>
<v-card :color="color" :dark="dark" flat :width="width" class="my-2">
<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">
<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">
<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"
v-model="modelValue[inputField.varName]"
:name="inputField.varName"
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
@change="emitBlur"
>
:hint="inputField.hint"
hide-details="auto"
density="comfortable"
@change="emitBlur">
<template #label>
<div>
<v-card-text class="text-body-1 my-0 py-0">
{{ inputField.label }}
</v-card-text>
<v-card-text v-if="inputField.hint" class="text-caption my-0 py-0">
{{ inputField.hint }}
</v-card-text>
</div>
</template>
</v-checkbox>
<span class="ml-4">
{{ inputField.label }}
</span>
</template>
</v-checkbox>
<!-- Text Field -->
<v-text-field
v-else-if="inputField.type === fieldTypes.TEXT || inputField.type === fieldTypes.PASSWORD"
v-model="value[inputField.varName]"
v-model="modelValue[inputField.varName]"
:readonly="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (readonlyFields && readonlyFields.includes(inputField.varName))"
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
filled
:type="inputField.type === fieldTypes.PASSWORD ? 'password' : 'text'"
rounded
class="rounded-lg"
variant="solo-filled"
flat
:autofocus="index === 0"
dense
density="comfortable"
:label="inputField.label"
:name="inputField.varName"
:hint="inputField.hint || ''"
@ -55,15 +70,15 @@
<!-- Text Area -->
<v-textarea
v-else-if="inputField.type === fieldTypes.TEXT_AREA"
v-model="value[inputField.varName]"
v-model="modelValue[inputField.varName]"
:readonly="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (readonlyFields && readonlyFields.includes(inputField.varName))"
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
filled
rounded
variant="solo-filled"
flat
class="rounded-lg"
rows="3"
auto-grow
dense
density="comfortable"
:label="inputField.label"
:name="inputField.varName"
:hint="inputField.hint || ''"
@ -75,42 +90,53 @@
<!-- Option Select -->
<v-select
v-else-if="inputField.type === fieldTypes.SELECT"
v-model="value[inputField.varName]"
v-model="modelValue[inputField.varName]"
:readonly="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (readonlyFields && readonlyFields.includes(inputField.varName))"
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
filled
rounded
variant="solo-filled"
flat
class="rounded-lg"
:prepend-icon="inputField.icons ? value[inputField.varName] : null"
:prepend-icon="inputField.icons ? modelValue[inputField.varName] : null"
:label="inputField.label"
:name="inputField.varName"
:items="inputField.options"
:item-text="inputField.itemText"
:item-title="inputField.itemText"
:item-value="inputField.itemValue"
:return-object="false"
:hint="inputField.hint"
density="comfortable"
persistent-hint
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>
<div>
<v-list-item-title>{{ item.raw.text }}</v-list-item-title>
<v-list-item-subtitle>{{ item.raw.description }}</v-list-item-subtitle>
</div>
</template>
</v-select>
<!-- Color Picker -->
<div v-else-if="inputField.type === fieldTypes.COLOR" class="d-flex" style="width: 100%">
<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">
<template #activator="{ props: templateProps }">
<v-btn
class="my-2 ml-auto"
style="min-width: 200px"
:color="modelValue[inputField.varName]"
dark
v-bind="templateProps"
>
{{ inputField.label }}
</v-btn>
</template>
<v-color-picker
v-model="value[inputField.varName]"
v-model="modelValue[inputField.varName]"
value="#7417BE"
hide-canvas
hide-inputs
@ -122,21 +148,34 @@
</div>
<div v-else-if="inputField.type === fieldTypes.OBJECT">
<auto-form v-model="value[inputField.varName]" :color="color" :items="inputField.items" @blur="emitBlur" />
<auto-form
v-model="modelValue[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">
<div
v-for="(item, idx) in modelValue[inputField.varName]"
:key="idx"
>
<p>
{{ inputField.label }} {{ idx + 1 }}
<span>
<BaseButton class="ml-5" x-small delete @click="removeByIndex(value[inputField.varName], idx)" />
<BaseButton
class="ml-5"
x-small
delete
@click="removeByIndex(modelValue[inputField.varName], idx)"
/>
</span>
</p>
<v-divider class="mb-5 mx-2" />
<auto-form
v-model="value[inputField.varName][idx]"
v-model="modelValue[inputField.varName][idx]"
:color="color"
:items="inputField.items"
@blur="emitBlur"
@ -144,7 +183,10 @@
</div>
<v-card-actions>
<v-spacer />
<BaseButton small @click="value[inputField.varName].push(getTemplate(inputField.items))">
<BaseButton
small
@click="modelValue[inputField.varName].push(getTemplate(inputField.items))"
>
{{ $t("general.new") }}
</BaseButton>
</v-card-actions>
@ -154,111 +196,96 @@
</v-card>
</template>
<script lang="ts">
import { computed, defineComponent } from "@nuxtjs/composition-api";
<script lang="ts" setup>
import { validators } from "@/composables/use-validators";
import { fieldTypes } from "@/composables/forms";
import { AutoFormItems } from "~/types/auto-forms";
import type { AutoFormItems } from "~/types/auto-forms";
const BLUR_EVENT = "blur";
type ValidatorKey = keyof typeof validators;
export default defineComponent({
name: "AutoForm",
props: {
value: {
default: null,
type: [Object, Array],
},
updateMode: {
default: false,
type: Boolean,
},
items: {
default: null,
type: Array as () => AutoFormItems,
},
width: {
type: [Number, String],
default: "max",
},
globalRules: {
default: null,
type: Array as () => string[],
},
color: {
default: null,
type: String,
},
dark: {
default: false,
type: Boolean,
},
disabledFields: {
default: null,
type: Array as () => string[],
},
readonlyFields: {
default: null,
type: Array as () => string[],
},
// Use defineModel for v-model
const modelValue = defineModel<[object, Array<any>]>();
const props = defineProps({
updateMode: {
default: false,
type: Boolean,
},
setup(props, context) {
function rulesByKey(keys?: ValidatorKey[] | null) {
if (keys === undefined || keys === null) {
return [];
}
const list = [] as ((v: string) => boolean | string)[];
keys.forEach((key) => {
const split = key.split(":");
const validatorKey = split[0] as ValidatorKey;
if (validatorKey in validators) {
if (split.length === 1) {
// @ts-ignore- validators[validatorKey] is a function
list.push(validators[validatorKey]);
} else {
// @ts-ignore - validators[validatorKey] is a function
list.push(validators[validatorKey](split[1]));
}
}
});
return list;
}
const defaultRules = computed(() => rulesByKey(props.globalRules as ValidatorKey[]));
function removeByIndex(list: never[], index: number) {
// Removes the item at the index
list.splice(index, 1);
}
function getTemplate(item: AutoFormItems) {
const obj = {} as { [key: string]: string };
item.forEach((field) => {
obj[field.varName] = "";
});
return obj;
}
function emitBlur() {
context.emit(BLUR_EVENT, props.value);
}
return {
rulesByKey,
defaultRules,
removeByIndex,
getTemplate,
emitBlur,
fieldTypes,
validators,
};
items: {
default: null,
type: Array as () => AutoFormItems,
},
width: {
type: [Number, String],
default: "max",
},
globalRules: {
default: null,
type: Array as () => string[],
},
color: {
default: null,
type: String,
},
dark: {
default: false,
type: Boolean,
},
disabledFields: {
default: null,
type: Array as () => string[],
},
readonlyFields: {
default: null,
type: Array as () => string[],
},
});
const emit = defineEmits(["blur", "update:modelValue"]);
function rulesByKey(keys?: ValidatorKey[] | null) {
if (keys === undefined || keys === null) {
return [];
}
const list = [] as ((v: string) => boolean | string)[];
keys.forEach((key) => {
const split = key.split(":");
const validatorKey = split[0] as ValidatorKey;
if (validatorKey in validators) {
if (split.length === 1) {
list.push(validators[validatorKey]);
}
else {
list.push(validators[validatorKey](split[1]));
}
}
});
return list;
}
const defaultRules = computed(() => rulesByKey(props.globalRules as ValidatorKey[]));
function removeByIndex(list: never[], index: number) {
// Removes the item at the index
list.splice(index, 1);
}
function getTemplate(item: AutoFormItems) {
const obj = {} as { [key: string]: string };
item.forEach((field) => {
obj[field.varName] = "";
});
return obj;
}
function emitBlur() {
emit(BLUR_EVENT, modelValue.value);
}
</script>
<style lang="scss" scoped></style>

View file

@ -1,10 +1,16 @@
<template>
<BannerWarning
:title="$tc('banner-experimental.title')"
:description="$tc('banner-experimental.description')"
:title="$t('banner-experimental.title')"
:description="$t('banner-experimental.description')"
>
<template v-if="issue" #default>
<a :href="issue" target="_blank">{{ $t("banner-experimental.issue-link-text") }}</a>
<template
v-if="issue"
#default
>
<a
:href="issue"
target="_blank"
>{{ $t("banner-experimental.issue-link-text") }}</a>
</template>
</BannerWarning>
</template>

View file

@ -1,9 +1,21 @@
<template>
<v-alert border="left" colored-border type="warning" elevation="2" :icon="$globals.icons.alert">
<v-alert
border="start"
border-color
variant="tonal"
type="warning"
elevation="2"
:icon="$globals.icons.alert"
>
<b v-if="title">{{ title }}</b>
<div v-if="description">{{ description }}</div>
<div v-if="$slots.default" class="py-2">
<slot></slot>
<div v-if="description">
{{ description }}
</div>
<div
v-if="$slots.default"
class="py-2"
>
<slot />
</div>
</v-alert>
</template>

View file

@ -1,18 +1,19 @@
<template>
<v-btn
:color="color || btnAttrs.color"
:small="small"
:size="small ? 'small' : 'default'"
:x-small="xSmall"
:loading="loading"
:disabled="disabled"
:outlined="btnStyle.outlined"
:text="btnStyle.text"
:variant="disabled ? 'tonal' : btnStyle.outlined ? 'outlined' : btnStyle.text ? 'text' : 'elevated'"
:to="to"
v-bind="$attrs"
v-on="$listeners"
@click="download ? downloadFile() : undefined"
>
<v-icon v-if="!iconRight" left>
<v-icon
v-if="!iconRight"
start
>
<slot name="icon">
{{ icon || btnAttrs.icon }}
</slot>
@ -20,7 +21,10 @@
<slot name="default">
{{ text || btnAttrs.text }}
</slot>
<v-icon v-if="iconRight" right>
<v-icon
v-if="iconRight"
end
>
<slot name="icon">
{{ icon || btnAttrs.icon }}
</slot>
@ -29,10 +33,9 @@
</template>
<script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
export default defineComponent({
export default defineNuxtComponent({
name: "BaseButton",
props: {
// Types
@ -117,7 +120,8 @@ export default defineComponent({
},
},
setup(props) {
const { $globals, i18n } = useContext();
const i18n = useI18n();
const { $globals } = useNuxtApp();
const buttonOptions = {
create: {
text: i18n.t("general.create"),
@ -159,15 +163,20 @@ export default defineComponent({
const btnAttrs = computed(() => {
if (props.delete) {
return buttonOptions.delete;
} else if (props.update) {
}
else if (props.update) {
return buttonOptions.update;
} else if (props.edit) {
}
else if (props.edit) {
return buttonOptions.edit;
} else if (props.cancel) {
}
else if (props.cancel) {
return buttonOptions.cancel;
} else if (props.save) {
}
else if (props.save) {
return buttonOptions.save;
} else if (props.download) {
}
else if (props.download) {
return buttonOptions.download;
}
return buttonOptions.create;
@ -191,7 +200,8 @@ export default defineComponent({
const btnStyle = computed(() => {
if (props.secondary) {
return buttonStyles.secondary;
} else if (props.minor || props.cancel) {
}
else if (props.minor || props.cancel) {
return buttonStyles.minor;
}
return buttonStyles.defaults;

View file

@ -1,20 +1,44 @@
<template>
<v-item-group>
<template v-for="btn in buttons">
<v-menu v-if="btn.children" :key="'menu-' + btn.event" active-class="pa-0" offset-y top left :style="stretch ? 'width: 100%;' : ''">
<template #activator="{ on, attrs }">
<v-btn tile :large="large" icon v-bind="attrs" v-on="on">
<v-menu
v-if="btn.children"
:key="'menu-' + btn.event"
active-class="pa-0"
offset-y
top
start
:style="stretch ? 'width: 100%;' : ''"
>
<template #activator="{ props }">
<v-btn
tile
:large="large"
icon
variant="plain"
v-bind="props"
>
<v-icon>
{{ btn.icon }}
</v-icon>
</v-btn>
</template>
<v-list dense>
<template v-for="(child, idx) in btn.children">
<v-list-item :key="idx" dense @click="$emit(child.event)">
<v-list density="compact">
<template
v-for="(child, idx) in btn.children"
:key="idx"
>
<v-list-item
density="compact"
@click="$emit(child.event)"
>
<v-list-item-title>{{ child.text }}</v-list-item-title>
</v-list-item>
<v-divider v-if="child.divider" :key="`divider-${idx}`" class="my-1"></v-divider>
<v-divider
v-if="child.divider"
:key="`divider-${idx}`"
class="my-1"
/>
</template>
</v-list>
</v-menu>
@ -23,11 +47,11 @@
:key="'btn-' + btn.event"
open-delay="200"
transition="slide-y-reverse-transition"
dense
density="compact"
bottom
content-class="text-caption"
>
<template #activator="{ on, attrs }">
<template #activator="{ props }">
<v-btn
tile
icon
@ -35,8 +59,8 @@
:large="large"
:disabled="btn.disabled"
:style="stretch ? `width: ${maxButtonWidth};` : ''"
v-bind="attrs"
v-on="on"
variant="plain"
v-bind="props"
@click="$emit(btn.event)"
>
<v-icon> {{ btn.icon }} </v-icon>
@ -49,8 +73,6 @@
</template>
<script lang="ts">
import { computed, defineComponent } from "@nuxtjs/composition-api";
export interface ButtonOption {
icon?: string;
color?: string;
@ -61,7 +83,7 @@ export interface ButtonOption {
divider?: boolean;
}
export default defineComponent({
export default defineNuxtComponent({
props: {
buttons: {
type: Array as () => ButtonOption[],
@ -74,13 +96,13 @@ export default defineComponent({
stretch: {
type: Boolean,
default: false,
}
},
},
setup(props) {
const maxButtonWidth = computed(() => `${100 / props.buttons.length}%`);
return {
maxButtonWidth,
};
}
},
});
</script>

View file

@ -7,25 +7,29 @@
'mt-8': section,
}"
>
<v-card-title class="headline pl-0 py-0">
<v-icon v-if="icon !== ''" left>
<v-card-title class="text-h5 pl-0 py-0" style="font-weight: normal;">
<v-icon
v-if="icon"
start
>
{{ icon }}
</v-icon>
{{ title }}
</v-card-title>
<v-card-text v-if="$slots.default" class="pt-2 pl-0">
<v-card-text
v-if="$slots.default"
class="pt-2 pl-0"
>
<p class="pb-0 mb-0">
<slot />
</p>
</v-card-text>
<v-divider class="mb-3"></v-divider>
<v-divider class="mb-3" />
</v-card>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
export default defineNuxtComponent({
props: {
title: {
type: String,

View file

@ -1,59 +1,77 @@
<template>
<div>
<slot name="activator" v-bind="{ open }" />
<slot
name="activator"
v-bind="{ open }"
/>
<v-dialog
v-model="dialog"
absolute
:width="width"
:max-width="maxWidth"
:max-width="maxWidth ?? undefined"
:content-class="top ? 'top-dialog' : undefined"
:fullscreen="$vuetify.breakpoint.xsOnly"
@keydown.enter="
$emit('submit');
dialog = false;
"
@click:outside="$emit('cancel')"
@keydown.esc="$emit('cancel')"
:fullscreen="$vuetify.display.xs"
@keydown.enter="() => {
emit('submit'); dialog = false;
}"
@click:outside="emit('cancel')"
@keydown.esc="emit('cancel')"
>
<v-card height="100%">
<v-app-bar dark dense :color="color" class="">
<v-icon large left>
<v-toolbar
dark
density="comfortable"
:color="color"
class="px-3 position-relative top-0 left-0 w-100"
>
<v-icon size="large">
{{ icon }}
</v-icon>
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-progress-linear v-if="loading" class="mt-1" indeterminate color="primary"></v-progress-linear>
<v-toolbar-title class="headline">
{{ title }}
</v-toolbar-title>
<v-spacer />
</v-toolbar>
<v-progress-linear
v-if="loading"
class="mt-1"
indeterminate
color="primary"
/>
<div>
<slot v-bind="{ submitEvent }" />
</div>
<v-divider class="mx-2"></v-divider>
<v-divider class="mx-2" />
<v-card-actions>
<slot name="card-actions">
<v-btn
text
variant="text"
color="grey"
@click="
dialog = false;
$emit('cancel');
emit('cancel');
"
>
{{ $t("general.cancel") }}
</v-btn>
<v-spacer></v-spacer>
<v-spacer />
<slot name="custom-card-action"></slot>
<BaseButton v-if="$listeners.delete" delete secondary @click="deleteEvent" />
<slot name="custom-card-action" />
<BaseButton
v-if="$listeners.confirm"
v-if="canDelete"
delete
secondary
@click="deleteEvent"
/>
<BaseButton
v-if="canConfirm"
:color="color"
type="submit"
:disabled="submitDisabled"
@click="
$emit('confirm');
emit('confirm');
dialog = false;
"
>
@ -63,141 +81,127 @@
{{ $t("general.confirm") }}
</BaseButton>
<BaseButton
v-if="$listeners.submit"
v-if="canSubmit"
type="submit"
:disabled="submitDisabled"
@click="submitEvent"
>
{{ submitText }}
<template v-if="submitIcon" #icon>
<template
v-if="submitIcon"
#icon
>
{{ submitIcon }}
</template>
</BaseButton>
</slot>
</v-card-actions>
<div v-if="$slots['below-actions']" class="pb-4">
<slot name="below-actions"> </slot>
<div
v-if="$slots['below-actions']"
class="pb-4"
>
<slot name="below-actions" />
</div>
</v-card>
</v-dialog>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from "@nuxtjs/composition-api";
export default defineComponent({
name: "BaseDialog",
props: {
value: {
type: Boolean,
default: false,
},
color: {
type: String,
default: "primary",
},
title: {
type: String,
default: "Modal Title",
},
icon: {
type: String,
default: null,
},
width: {
type: [Number, String],
default: "500",
},
maxWidth: {
type: [Number, String],
default: null,
},
loading: {
type: Boolean,
default: false,
},
top: {
default: null,
type: Boolean,
},
submitIcon: {
type: String,
default: null,
},
submitText: {
type: String,
default: function () {
return this.$t("general.create");
},
},
submitDisabled: {
type: Boolean,
default: false,
},
keepOpen: {
default: false,
type: Boolean,
},
},
setup(props, context) {
const dialog = computed<boolean>({
get() {
return props.value;
},
set(val) {
context.emit("input", val);
},
});
<script setup lang="ts">
import { useNuxtApp } from "#app";
return {
dialog,
};
},
data() {
return {
submitted: false,
};
},
computed: {
determineClose(): boolean {
return this.submitted && !this.loading && !this.keepOpen;
},
},
watch: {
determineClose() {
this.submitted = false;
this.dialog = false;
},
dialog(val) {
if (val) this.submitted = false;
if (!val) this.$emit("close");
},
},
methods: {
submitEvent() {
this.$emit("submit");
this.submitted = true;
},
deleteEvent() {
this.$emit("delete");
this.submitted = true;
},
open() {
this.dialog = true;
this.logDeprecatedProp("open");
},
close() {
this.dialog = false;
this.logDeprecatedProp("close");
},
logDeprecatedProp(val: string) {
console.warn(
`[BaseDialog] The method '${val}' is deprecated. Please use v-model="value" to manage state instead.`
);
},
},
interface DialogProps {
modelValue: boolean;
color?: string;
title?: string;
icon?: string | null;
width?: number | string;
maxWidth?: number | string | null;
loading?: boolean;
top?: boolean | null;
submitIcon?: string | null;
submitText?: string;
submitDisabled?: boolean;
keepOpen?: boolean;
// actions
canDelete?: boolean;
canConfirm?: boolean;
canSubmit?: boolean;
}
interface DialogEmits {
(e: "update:modelValue", value: boolean): void;
(e: "submit" | "cancel" | "confirm" | "delete" | "close"): void;
}
// Using TypeScript interface with withDefaults for props
const props = withDefaults(defineProps<DialogProps>(), {
color: "primary",
title: "Modal Title",
icon: null,
width: "500",
maxWidth: null,
loading: false,
top: null,
submitIcon: null,
submitText: () => useNuxtApp().$i18n.t("general.create"),
submitDisabled: false,
keepOpen: false,
canDelete: false,
canConfirm: false,
canSubmit: false,
});
const emit = defineEmits<DialogEmits>();
const dialog = computed({
get: () => props.modelValue,
set: val => emit("update:modelValue", val),
});
const submitted = ref(false);
const determineClose = computed(() => {
return submitted.value && !props.loading && !props.keepOpen;
});
watch(determineClose, (shouldClose) => {
if (shouldClose) {
submitted.value = false;
dialog.value = false;
}
});
watch(dialog, (val) => {
if (val) submitted.value = false;
if (!val) emit("close");
});
function submitEvent() {
emit("submit");
submitted.value = true;
}
function deleteEvent() {
emit("delete");
submitted.value = true;
}
function open() {
dialog.value = true;
logDeprecatedProp("open");
}
/* function close() {
dialog.value = false;
logDeprecatedProp("close");
} */
function logDeprecatedProp(val: string) {
console.warn(
`[BaseDialog] The method '${val}' is deprecated. Please use v-model="value" to manage state instead.`,
);
}
</script>
<style>

View file

@ -1,11 +1,13 @@
<template>
<v-divider :width="width" :class="color" :style="`border-width: ${thickness} !important`" />
<v-divider
:width="width"
:class="color"
:style="`border-width: ${thickness} !important`"
/>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
export default defineNuxtComponent({
props: {
width: {
type: String,

View file

@ -1,59 +1,104 @@
<template>
<v-menu offset-y>
<template #activator="{ on, attrs }">
<v-btn color="primary" v-bind="{ ...attrs, ...$attrs }" :class="btnClass" :disabled="disabled" v-on="on">
<v-icon v-if="activeObj.icon" left>
<template #activator="{ props }">
<v-btn
color="primary"
v-bind="{ ...props, ...$attrs }"
:class="btnClass"
:disabled="disabled"
>
<v-icon
v-if="activeObj.icon"
start
>
{{ activeObj.icon }}
</v-icon>
{{ mode === MODES.model ? activeObj.text : btnText }}
<v-icon right>
<v-icon end>
{{ $globals.icons.chevronDown }}
</v-icon>
</v-btn>
</template>
<!-- Model -->
<v-list v-if="mode === MODES.model" dense>
<v-list-item-group v-model="itemGroup">
<template v-for="(item, index) in items">
<div v-if="!item.hide" :key="index">
<v-list-item @click="setValue(item)">
<v-list-item-icon v-if="item.icon">
<v-icon>{{ item.icon }}</v-icon>
</v-list-item-icon>
<v-list-item-title>{{ item.text }}</v-list-item-title>
</v-list-item>
<v-divider v-if="item.divider" :key="`divider-${index}`" class="my-1" ></v-divider>
</div>
</template>
</v-list-item-group>
</v-list>
<!-- Links -->
<v-list v-else-if="mode === MODES.link" dense>
<v-list-item-group v-model="itemGroup">
<template v-for="(item, index) in items">
<div v-if="!item.hide" :key="index">
<v-list-item :to="item.to">
<v-list-item-icon v-if="item.icon">
<v-icon>{{ item.icon }}</v-icon>
</v-list-item-icon>
<v-list-item-title>{{ item.text }}</v-list-item-title>
</v-list-item>
<v-divider v-if="item.divider" :key="`divider-${index}`" class="my-1" ></v-divider>
</div>
</template>
</v-list-item-group>
</v-list>
<!-- Event -->
<v-list v-else-if="mode === MODES.event" dense>
<v-list
v-if="mode === MODES.model"
v-model:selected="itemGroup"
density="compact"
>
<template v-for="(item, index) in items">
<div v-if="!item.hide" :key="index">
<v-list-item @click="$emit(item.event)">
<v-list-item-icon v-if="item.icon">
<div
v-if="!item.hide"
:key="index"
>
<v-list-item @click="setValue(item)">
<template
v-if="item.icon"
#prepend
>
<v-icon>{{ item.icon }}</v-icon>
</v-list-item-icon>
</template>
<v-list-item-title>{{ item.text }}</v-list-item-title>
</v-list-item>
<v-divider v-if="item.divider" :key="`divider-${index}`" class="my-1" ></v-divider>
<v-divider
v-if="item.divider"
:key="`divider-${index}`"
class="my-1"
/>
</div>
</template>
</v-list>
<!-- Links -->
<v-list
v-else-if="mode === MODES.link"
v-model:selected="itemGroup"
density="compact"
>
<template v-for="(item, index) in items">
<div
v-if="!item.hide"
:key="index"
>
<v-list-item :to="item.to">
<template
v-if="item.icon"
#prepend
>
<v-icon>{{ item.icon }}</v-icon>
</template>
<v-list-item-title>{{ item.text }}</v-list-item-title>
</v-list-item>
<v-divider
v-if="item.divider"
:key="`divider-${index}`"
class="my-1"
/>
</div>
</template>
</v-list>
<!-- Event -->
<v-list
v-else-if="mode === MODES.event"
density="compact"
>
<template v-for="(item, index) in items">
<div
v-if="!item.hide"
:key="index"
>
<v-list-item @click="$emit(item.event)">
<template
v-if="item.icon"
#prepend
>
<v-icon>{{ item.icon }}</v-icon>
</template>
<v-list-item-title>{{ item.text }}</v-list-item-title>
</v-list-item>
<v-divider
v-if="item.divider"
:key="`divider-${index}`"
class="my-1"
/>
</div>
</template>
</v-list>
@ -61,18 +106,14 @@
</template>
<script lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api";
const INPUT_EVENT = "input";
type modes = "model" | "link" | "event";
const MODES = {
model: "model",
link: "link",
event: "event",
};
type modes = "model" | "link" | "event";
export interface MenuItem {
text: string;
icon?: string;
@ -83,7 +124,7 @@ export interface MenuItem {
hide?: boolean;
}
export default defineComponent({
export default defineNuxtComponent({
props: {
mode: {
type: String as () => modes,
@ -98,7 +139,7 @@ export default defineComponent({
required: false,
default: false,
},
value: {
modelValue: {
type: String,
required: false,
default: "",
@ -112,10 +153,11 @@ export default defineComponent({
type: String,
required: false,
default: function () {
return this.$t("general.actions");
}
return useI18n().t("general.actions");
},
},
},
emits: ["update:modelValue"],
setup(props, context) {
const activeObj = ref<MenuItem>({
text: "DEFAULT",
@ -124,7 +166,7 @@ export default defineComponent({
let startIndex = 0;
props.items.forEach((item, index) => {
if (item.value === props.value) {
if (item.value === props.modelValue) {
startIndex = index;
activeObj.value = item;
@ -133,7 +175,7 @@ export default defineComponent({
const itemGroup = ref(startIndex);
function setValue(v: MenuItem) {
context.emit(INPUT_EVENT, v.value);
context.emit("update:modelValue", v.value);
activeObj.value = v;
}

View file

@ -1,26 +1,29 @@
<template>
<div class="mt-4">
<section class="d-flex flex-column align-center">
<slot name="header"></slot>
<h2 class="headline">
<slot name="title"> 👋 Here's a Title </slot>
<slot name="header" />
<h2 class="text-h5">
<slot name="title">
👋 Here's a Title
</slot>
</h2>
<h3 class="subtitle-1">
<slot> </slot>
<slot />
</h3>
</section>
<section class="d-flex">
<slot name="content"></slot>
<slot name="content" />
</section>
<v-divider v-if="divider" class="my-4"></v-divider>
<v-divider
v-if="divider"
class="my-4"
/>
</div>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
export default defineNuxtComponent({
props: {
divider: {
type: Boolean,
@ -29,3 +32,11 @@ export default defineComponent({
},
});
</script>
<style scoped>
.subtitle-1 {
font-size: 1rem;
font-weight: normal;
color: var(--v-text-caption);
}
</style>

View file

@ -1,5 +1,9 @@
<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">
<slot name="avatar">
<v-sheet
@ -10,12 +14,24 @@
class="text-start v-card--material__heading mb-n6 mt-n10 pa-7"
dark
>
<v-icon v-if="icon" size="40"> {{ icon }} </v-icon>
<div v-if="text" class="headline font-weight-thin" v-text="text" />
<v-icon
v-if="icon"
size="40"
>
{{ icon }}
</v-icon>
<div
v-if="text"
class="headline font-weight-thin"
v-text="text"
/>
</v-sheet>
</slot>
<div v-if="$slots['after-heading']" class="ml-auto">
<div
v-if="$slots['after-heading']"
class="ml-auto"
>
<slot name="after-heading" />
</div>
</div>
@ -31,7 +47,10 @@
</template>
<template v-if="$slots.bottom">
<v-divider v-if="!$slots.actions" class="mt-2" />
<v-divider
v-if="!$slots.actions"
class="mt-2"
/>
<div class="pb-0">
<slot name="bottom" />
@ -41,9 +60,7 @@
</template>
<script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
export default defineComponent({
export default defineNuxtComponent({
name: "MaterialCard",
props: {
@ -73,14 +90,13 @@ export default defineComponent({
},
},
setup() {
const { $vuetify } = useContext();
const { $vuetify } = useNuxtApp();
const hasHeading = computed(() => false);
const hasAltHeading = computed(() => false);
const classes = computed(() => {
return {
"v-card--material--has-heading": hasHeading,
"mt-3": $vuetify.breakpoint.name === "xs" || $vuetify.breakpoint.name === "sm",
"mt-3": $vuetify.display.name.value === "xs" || $vuetify.display.name.value === "sm",
};
});

View file

@ -3,14 +3,30 @@
<LanguageDialog v-model="langDialog" />
<v-card>
<div>
<v-toolbar width="100%" color="primary" class="d-flex justify-center" style="margin-bottom: 4rem" dark>
<v-toolbar-title class="headline text-h4"> Mealie </v-toolbar-title>
<v-toolbar
width="100%"
color="primary"
class="d-flex justify-center"
style="margin-bottom: 4rem"
dark
>
<v-toolbar-title class="headline text-h4">
Mealie
</v-toolbar-title>
</v-toolbar>
<div class="icon-container">
<v-divider class="icon-divider"></v-divider>
<v-avatar class="pa-2 icon-avatar" color="primary" size="75">
<svg class="icon-white" style="width: 75" viewBox="0 0 24 24">
<v-divider class="icon-divider" />
<v-avatar
class="pa-2 icon-avatar"
color="primary"
size="75"
>
<svg
class="icon-white"
style="width: 75"
viewBox="0 0 24 24"
>
<path
d="M8.1,13.34L3.91,9.16C2.35,7.59 2.35,5.06 3.91,3.5L10.93,10.5L8.1,13.34M13.41,13L20.29,19.88L18.88,21.29L12,14.41L5.12,21.29L3.71,19.88L13.36,10.22L13.16,10C12.38,9.23 12.38,7.97 13.16,7.19L17.5,2.82L18.43,3.74L15.19,7L16.15,7.94L19.39,4.69L20.31,5.61L17.06,8.85L18,9.81L21.26,6.56L22.18,7.5L17.81,11.84C17.03,12.62 15.77,12.62 15,11.84L14.78,11.64L13.41,13Z"
/>
@ -19,12 +35,12 @@
</div>
</div>
<div class="d-flex justify-center grow items-center my-4">
<slot :width="pageWidth"></slot>
<slot :width="pageWidth" />
</div>
<div class="mx-2 my-4">
<v-progress-linear
v-if="value > 0"
:value="Math.ceil((value/maxPageNumber)*100)"
v-if="wizardPage > 0"
:value="Math.ceil((wizardPage / maxPageNumber) * 100)"
striped
height="10"
/>
@ -45,12 +61,17 @@
<v-spacer />
<v-btn
v-if="nextButtonShow"
variant="elevated"
:disabled="!nextButtonEnable"
:color="nextButtonColorRef"
@click="incrementPage"
>
<div v-if="isSubmitting">
<v-progress-circular indeterminate color="white" size="24" />
<v-progress-circular
indeterminate
color="white"
size="24"
/>
</div>
<div v-else>
<v-icon v-if="nextButtonIconRef && !nextButtonIconAfter">
@ -64,8 +85,14 @@
</v-btn>
</v-card-actions>
<v-card-actions class="justify-center flex-column py-8">
<BaseButton large color="primary" @click="langDialog = true">
<template #icon> {{ $globals.icons.translate }}</template>
<BaseButton
large
color="primary"
@click="langDialog = true"
>
<template #icon>
{{ $globals.icons.translate }}
</template>
{{ $t("language-dialog.choose-language") }}
</BaseButton>
</v-card-actions>
@ -74,11 +101,9 @@
</template>
<script lang="ts">
import { computed, defineComponent, ref, useContext } from "@nuxtjs/composition-api";
export default defineComponent({
export default defineNuxtComponent({
props: {
value: {
modelValue: {
type: Number,
required: true,
},
@ -157,56 +182,65 @@ export default defineComponent({
isSubmitting: {
type: Boolean,
default: false,
}
},
},
emits: ["update:modelValue", "submit"],
setup(props, context) {
const { $globals, i18n } = useContext();
const i18n = useI18n();
const { $globals } = useNuxtApp();
const ready = ref(false);
const langDialog = ref(false);
const prevButtonTextRef = computed(() => props.prevButtonText || i18n.tc("general.back"));
const wizardPage = computed({
get: () => props.modelValue,
set: value => context.emit("update:modelValue", value),
});
const prevButtonTextRef = computed(() => props.prevButtonText || i18n.t("general.back"));
const prevButtonIconRef = computed(() => props.prevButtonIcon || $globals.icons.back);
const nextButtonTextRef = computed(
() => props.nextButtonText || (
props.nextButtonIsSubmit ? i18n.tc("general.submit") : i18n.tc("general.next")
)
);
props.nextButtonIsSubmit ? i18n.t("general.submit") : i18n.t("general.next")
),
);
const nextButtonIconRef = computed(
() => props.nextButtonIcon || (
props.nextButtonIsSubmit ? $globals.icons.createAlt : $globals.icons.forward
)
props.nextButtonIsSubmit ? $globals.icons.createAlt : $globals.icons.forward
),
);
const nextButtonColorRef = computed(
() => props.nextButtonColor || (props.nextButtonIsSubmit ? "success" : "info")
() => props.nextButtonColor || (props.nextButtonIsSubmit ? "success" : "info"),
);
function goToPage(page: number) {
if (page < props.minPageNumber) {
goToPage(props.minPageNumber);
return;
} else if (page > props.maxPageNumber) {
}
else if (page > props.maxPageNumber) {
goToPage(props.maxPageNumber);
return;
}
context.emit("input", page);
wizardPage.value = page;
}
function decrementPage() {
goToPage(props.value - 1);
goToPage(wizardPage.value - 1);
}
function incrementPage() {
if (props.nextButtonIsSubmit) {
context.emit("submit", props.value);
} else {
goToPage(props.value + 1);
context.emit("submit", wizardPage.value);
}
else {
goToPage(wizardPage.value + 1);
}
}
ready.value = true;
return {
wizardPage,
ready,
langDialog,
prevButtonTextRef,
@ -217,7 +251,7 @@ export default defineComponent({
decrementPage,
incrementPage,
};
}
},
});
</script>

View file

@ -1,7 +1,14 @@
<template>
<div>
<v-btn outlined class="rounded-xl my-1 mx-1" :to="to">
<v-icon v-if="icon != ''" left>
<v-btn
variant="outlined"
class="rounded-xl my-1 mx-1"
:to="to"
>
<v-icon
v-if="icon != ''"
start
>
{{ icon }}
</v-icon>
{{ text }}
@ -10,9 +17,7 @@
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
export default defineNuxtComponent({
props: {
to: {
type: String,

View file

@ -1,7 +1,7 @@
<template>
<v-menu
offset-y
left
start
:bottom="!menuTop"
:nudge-bottom="!menuTop ? '5' : '0'"
:top="menuTop"
@ -11,18 +11,30 @@
open-on-hover
content-class="d-print-none"
>
<template #activator="{ on, attrs }">
<v-btn :fab="fab" :small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent>
<template #activator="{ props }">
<v-btn
:class="{ 'rounded-circle': fab }"
:small="fab"
:color="color"
:icon="!fab"
dark
v-bind="props"
@click.prevent
>
<v-icon>{{ $globals.icons.dotsVertical }}</v-icon>
</v-btn>
</template>
<v-list dense>
<v-list-item v-for="(item, index) in items" :key="index" @click="$emit(item.event)">
<v-list-item-icon>
<v-list density="compact">
<v-list-item
v-for="(item, index) in items"
:key="index"
@click="$emit(item.event)"
>
<template #prepend>
<v-icon :color="item.color ? item.color : undefined">
{{ item.icon }}
</v-icon>
</v-list-item-icon>
</template>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
</v-list>
@ -30,10 +42,9 @@
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
import { ContextMenuItem } from "~/composables/use-context-presents";
import type { ContextMenuItem } from "~/composables/use-context-presents";
export default defineComponent({
export default defineNuxtComponent({
props: {
items: {
type: Array as () => ContextMenuItem[],
@ -49,7 +60,7 @@ export default defineComponent({
},
color: {
type: String,
default: "grey darken-2",
default: "grey-darken-2",
},
},
});

View file

@ -1,9 +1,19 @@
<template>
<div>
<v-card-actions>
<v-menu v-if="tableConfig.hideColumns" offset-y bottom nudge-bottom="6" :close-on-content-click="false">
<template #activator="{ on, attrs }">
<v-btn color="accent" class="mr-2" dark v-bind="attrs" v-on="on">
<v-menu
v-if="tableConfig.hideColumns"
offset-y
bottom
nudge-bottom="6"
:close-on-content-click="false"
>
<template #activator="{ props }">
<v-btn
color="accent"
variant="elevated"
v-bind="props"
>
<v-icon>
{{ $globals.icons.cog }}
</v-icon>
@ -16,12 +26,12 @@
:key="itemValue.text + itemValue.show"
v-model="filteredHeaders"
:value="itemValue.value"
dense
density="compact"
flat
inset
:label="itemValue.text"
hide-details
></v-checkbox>
/>
</v-card-text>
</v-card>
</v-menu>
@ -30,41 +40,52 @@
:disabled="selected.length < 1"
mode="event"
color="info"
variant="elevated"
:items="bulkActions"
v-on="bulkActionListener"
>
</BaseOverflowButton>
<slot name="button-row"> </slot>
v-bind="bulkActionListener"
/>
<slot name="button-row" />
</v-card-actions>
<div class="mx-2 clip-width">
<v-text-field v-model="search" :label="$t('search.search')"></v-text-field>
<v-text-field
v-model="search"
variant="underlined"
:label="$t('search.search')"
/>
</div>
<v-data-table
v-model="selected"
item-key="id"
:show-select="bulkActions.length > 0"
:headers="activeHeaders"
:sort-by="initialSort"
:sort-desc="initialSortDesc"
:show-select="bulkActions.length > 0"
:sort-by="sortBy"
:items="data || []"
:items-per-page="15"
:search="search"
class="elevation-2"
>
<template v-for="header in activeHeaders" #[`item.${header.value}`]="{ item }">
<slot :name="'item.' + header.value" v-bind="{ item }"> {{ item[header.value] }}</slot>
<template
v-for="header in headersWithoutActions"
#[`item.${header.value}`]="{ item }"
>
<slot
:name="'item.' + header.value"
v-bind="{ item }"
>
{{ item[header.value] }}
</slot>
</template>
<template #item.actions="{ item }">
<template #[`item.actions`]="{ item }">
<BaseButtonGroup
:buttons="[
{
icon: $globals.icons.edit,
text: $tc('general.edit'),
text: $t('general.edit'),
event: 'edit',
},
{
icon: $globals.icons.delete,
text: $tc('general.delete'),
text: $t('general.delete'),
event: 'delete',
},
]"
@ -74,8 +95,11 @@
</template>
</v-data-table>
<v-card-actions class="justify-end">
<slot name="button-bottom"> </slot>
<BaseButton color="info" @click="downloadAsJson(data, 'export.json')">
<slot name="button-bottom" />
<BaseButton
color="info"
@click="downloadAsJson(data, 'export.json')"
>
<template #icon>
{{ $globals.icons.download }}
</template>
@ -86,7 +110,6 @@
</template>
<script lang="ts">
import { computed, defineComponent, ref } from "@nuxtjs/composition-api";
import { downloadAsJson } from "~/composables/use-utils";
export interface TableConfig {
@ -109,7 +132,7 @@ export interface BulkAction {
event: string;
}
export default defineComponent({
export default defineNuxtComponent({
props: {
tableConfig: {
type: Object as () => TableConfig,
@ -139,28 +162,34 @@ export default defineComponent({
default: false,
},
},
emits: ["delete-one", "edit-one"],
setup(props, context) {
const i18n = useI18n();
const sortBy = computed(() => [{
key: props.initialSort,
order: props.initialSortDesc ? "desc" : "asc",
}]);
// ===========================================================
// Reactive Headers
const filteredHeaders = ref<string[]>([]);
// Set default filtered
filteredHeaders.value = (() => {
const filtered: string[] = [];
props.headers.forEach((element) => {
if (element.show) {
filtered.push(element.value);
}
});
return filtered;
})();
const activeHeaders = computed(() => {
const filtered = props.headers.filter((header) => filteredHeaders.value.includes(header.value));
filtered.push({ text: "", value: "actions", show: true, align: "right" });
return filtered;
const filteredHeaders = computed<string[]>(() => {
return props.headers.filter(header => header.show).map(header => header.value);
});
const headersWithoutActions = computed(() =>
props.headers
.filter(header => filteredHeaders.value.includes(header.value))
.map(header => ({
...header,
title: i18n.t(header.text),
})),
);
const activeHeaders = computed(() => [
...headersWithoutActions.value,
{ title: "", value: "actions", show: true, align: "end" },
]);
const selected = ref<any[]>([]);
// ===========================================================
@ -183,8 +212,10 @@ export default defineComponent({
const search = ref("");
return {
sortBy,
selected,
filteredHeaders,
headersWithoutActions,
activeHeaders,
bulkActionListener,
search,
@ -198,4 +229,7 @@ export default defineComponent({
.clip-width {
max-width: 400px;
}
.v-btn--disabled {
opacity: 0.5 !important;
}
</style>

View file

@ -5,9 +5,7 @@
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
export default defineNuxtComponent({
props: {
data: {
type: Object,

View file

@ -1,16 +1,22 @@
<template>
<v-btn x-small :href="href" color="primary" target="_blank">
<v-icon left small>
<v-btn
size="x-small"
:href="href"
color="primary"
target="_blank"
>
<v-icon
start
size="small"
>
{{ $globals.icons.folderOutline }}
</v-icon>
{{ $tc("about.docs") }}
{{ $t("about.docs") }}
</v-btn>
</template>
<script lang="ts">
import { computed, defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
export default defineNuxtComponent({
props: {
link: {
type: String,

View file

@ -1,27 +1,39 @@
<template>
<div ref="el" :class="isOverDropZone ? 'over' : ''">
<div v-if="isOverDropZone" class="overlay"></div>
<div v-if="isOverDropZone" class="absolute text-container">
<p class="text-center drop-text"> {{ $t("recipe.drop-image") }} </p>
<div
ref="el"
:class="isOverDropZone ? 'over' : ''"
>
<div
v-if="isOverDropZone"
class="overlay"
/>
<div
v-if="isOverDropZone"
class="absolute text-container"
>
<p class="text-center drop-text">
{{ $t("recipe.drop-image") }}
</p>
</div>
<slot></slot>
<slot />
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api";
import { useDropZone } from "@vueuse/core";
export default defineComponent({
export default defineNuxtComponent({
emits: ["drop"],
setup(_, context) {
const el = ref<HTMLDivElement>();
function onDrop(files: File[]) {
context.emit("drop", files);
function onDrop(files: File[] | null) {
if (files) {
context.emit("drop", files);
}
}
// @ts-ignore - This should work?
const { isOverDropZone } = useDropZone(el, onDrop);
const { isOverDropZone } = useDropZone(el, files => onDrop(files));
return { el, isOverDropZone };
},

View file

@ -1,14 +1,28 @@
<template>
<div class="text-center">
<v-menu top offset-y :right="right" :left="!right" open-on-hover>
<template #activator="{ on, attrs }">
<v-btn :small="small" icon v-bind="attrs" v-on="on" @click.stop>
<v-icon :small="small"> {{ $globals.icons.help }} </v-icon>
<v-menu
top
offset-y
:right="right"
:left="!right"
open-on-hover
>
<template #activator="{ props }">
<v-btn
:size="small ? 'small' : undefined"
icon
v-bind="props"
variant="flat"
@click.stop
>
<v-icon :small="small">
{{ $globals.icons.help }}
</v-icon>
</v-btn>
</template>
<v-card max-width="300px">
<v-card-text>
<slot></slot>
<slot />
</v-card-text>
</v-card>
</v-menu>
@ -16,9 +30,7 @@
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
export default defineNuxtComponent({
props: {
small: {
type: Boolean,

View file

@ -1,152 +1,165 @@
<template>
<v-container class="pa-0">
<v-row no-gutters>
<v-col cols="8" align-self="center">
<Cropper
ref="cropper"
class="cropper"
:src="img"
:default-size="defaultSize"
:style="`height: ${cropperHeight}; width: ${cropperWidth};`"
/>
<v-container class="pa-0">
<v-row no-gutters>
<v-col
cols="8"
align-self="center"
>
<Cropper
ref="cropper"
class="cropper"
:src="img"
:default-size="defaultSize"
:style="`height: ${cropperHeight}; width: ${cropperWidth};`"
/>
</v-col>
<v-spacer />
<v-col
cols="2"
align-self="center"
>
<v-container class="pa-0 mx-0">
<v-row
v-for="(row, keyRow) in controls"
:key="keyRow"
>
<v-col
v-for="(control, keyControl) in row"
:key="keyControl"
:cols="12 / row.length"
class="py-2 mx-0"
style="display: flex; align-items: center; justify-content: center;"
>
<v-btn
icon
:color="control.color"
@click="control.callback()"
>
<v-icon> {{ control.icon }} </v-icon>
</v-btn>
</v-col>
<v-spacer />
<v-col cols="2" align-self="center">
<v-container class="pa-0 mx-0">
<v-row v-for="(row, keyRow) in controls" :key="keyRow">
<v-col
v-for="(control, keyControl) in row" :key="keyControl"
:cols="12 / row.length"
class="py-2 mx-0"
style="display: flex; align-items: center; justify-content: center;"
>
<v-btn icon :color="control.color" @click="control.callback()">
<v-icon> {{ control.icon }} </v-icon>
</v-btn>
</v-col>
</v-row>
</v-container>
</v-col>
</v-row>
</v-container>
</v-row>
</v-container>
</v-col>
</v-row>
</v-container>
</template>
<script lang="ts">
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
import { Cropper } from "vue-advanced-cropper";
import "vue-advanced-cropper/dist/style.css";
export default defineComponent({
components: { Cropper },
props: {
img: {
type: String,
required: true,
},
cropperHeight: {
type: String,
default: undefined,
},
cropperWidth: {
type: String,
default: undefined,
}
export default defineNuxtComponent({
components: { Cropper },
props: {
img: {
type: String,
required: true,
},
setup(_, context) {
const cropper = ref<Cropper>();
const { $globals } = useContext();
interface Control {
color: string;
icon: string;
callback: CallableFunction;
}
const controls = ref<Control[][]>([
[
{
color: "info",
icon: $globals.icons.flipHorizontal,
callback: () => flip(true, false),
},
{
color: "info",
icon: $globals.icons.flipVertical,
callback: () => flip(false, true),
},
],
[
{
color: "info",
icon: $globals.icons.rotateLeft,
callback: () => rotate(-90),
},
{
color: "info",
icon: $globals.icons.rotateRight,
callback: () => rotate(90),
},
],
[
{
color: "success",
icon: $globals.icons.save,
callback: () => save(),
},
],
]);
function flip(hortizontal: boolean, vertical?: boolean) {
if (!cropper.value) {
return;
}
cropper.value.flip(hortizontal, vertical);
}
function rotate(angle: number) {
if (!cropper.value) {
return;
}
cropper.value.rotate(angle);
}
function save() {
if (!cropper.value) {
return;
}
const { canvas } = cropper.value.getResult();
if (!canvas) {
return;
}
canvas.toBlob((blob) => {
if (blob) {
context.emit("save", blob);
}
})
}
return {
cropper,
controls,
flip,
rotate,
save,
};
cropperHeight: {
type: String,
default: undefined,
},
cropperWidth: {
type: String,
default: undefined,
},
},
emits: ["save"],
setup(_, context) {
const cropper = ref<Cropper>();
const { $globals } = useNuxtApp();
methods: {
// @ts-expect-error https://advanced-cropper.github.io/vue-advanced-cropper/guides/advanced-recipes.html
defaultSize({ imageSize, visibleArea }) {
return {
width: (visibleArea || imageSize).width,
height: (visibleArea || imageSize).height,
};
interface Control {
color: string;
icon: string;
callback: CallableFunction;
}
const controls = ref<Control[][]>([
[
{
color: "info",
icon: $globals.icons.flipHorizontal,
callback: () => flip(true, false),
},
{
color: "info",
icon: $globals.icons.flipVertical,
callback: () => flip(false, true),
},
],
[
{
color: "info",
icon: $globals.icons.rotateLeft,
callback: () => rotate(-90),
},
{
color: "info",
icon: $globals.icons.rotateRight,
callback: () => rotate(90),
},
],
[
{
color: "success",
icon: $globals.icons.save,
callback: () => save(),
},
],
]);
function flip(hortizontal: boolean, vertical?: boolean) {
if (!cropper.value) {
return;
}
cropper.value.flip(hortizontal, vertical);
}
function rotate(angle: number) {
if (!cropper.value) {
return;
}
cropper.value.rotate(angle);
}
function save() {
if (!cropper.value) {
return;
}
const { canvas } = cropper.value.getResult();
if (!canvas) {
return;
}
canvas.toBlob((blob) => {
if (blob) {
context.emit("save", blob);
}
});
}
return {
cropper,
controls,
flip,
rotate,
save,
};
},
methods: {
// @ts-expect-error https://advanced-cropper.github.io/vue-advanced-cropper/guides/advanced-recipes.html
defaultSize({ imageSize, visibleArea }) {
return {
width: (visibleArea || imageSize).width,
height: (visibleArea || imageSize).height,
};
},
},
});
</script>

View file

@ -1,22 +1,44 @@
<template>
<v-text-field v-model="inputVal" :label="$t('general.color')">
<v-text-field
v-model="inputVal"
:label="$t('general.color')"
>
<template #prepend>
<v-btn class="elevation-0" small height="30px" width="30px" :color="inputVal || 'grey'" @click="setRandomHex">
<v-btn
class="elevation-0"
size="small"
height="30px"
width="30px"
:color="inputVal || 'grey'"
@click="setRandomHex"
>
<v-icon color="white">
{{ $globals.icons.refreshCircle }}
</v-icon>
</v-btn>
</template>
<template #append>
<v-menu v-model="menu" left nudge-left="30" nudge-top="20" :close-on-content-click="false">
<template #activator="{ on }">
<v-icon v-on="on">
<v-menu
v-model="menu"
start
nudge-left="30"
nudge-top="20"
:close-on-content-click="false"
>
<template #activator="{ props }">
<v-icon v-bind="props">
{{ $globals.icons.formatColorFill }}
</v-icon>
</template>
<v-card>
<v-card-text class="pa-0">
<v-color-picker v-model="inputVal" flat hide-inputs show-swatches swatches-max-height="200" />
<v-color-picker
v-model="inputVal"
flat
hide-inputs
show-swatches
swatches-max-height="200"
/>
</v-card-text>
</v-card>
</v-menu>
@ -25,24 +47,23 @@
</template>
<script lang="ts">
import { defineComponent, computed, ref } from "@nuxtjs/composition-api";
export default defineComponent({
export default defineNuxtComponent({
props: {
value: {
modelValue: {
type: String,
required: true,
},
},
emits: ["update:modelValue"],
setup(props, context) {
const menu = ref(false);
const inputVal = computed({
get: () => {
return props.value;
return props.modelValue;
},
set: (val) => {
context.emit("input", val);
context.emit("update:modelValue", val);
},
});

View file

@ -3,22 +3,27 @@
ref="autocompleteRef"
v-model="itemVal"
v-bind="$attrs"
:search-input.sync="searchInput"
item-text="name"
v-model:search="searchInput"
item-title="name"
return-object
:items="items"
:prepend-icon="icon || $globals.icons.tags"
auto-select-first
clearable
color="primary"
hide-details
@keyup.enter="emitCreate"
>
<template v-if="$listeners.create" #no-data>
<div class="caption text-center pb-2">{{ $t("recipe.press-enter-to-create") }}</div>
</template>
<template v-if="$listeners.create" #append-item>
<template
v-if="create"
#append-item
>
<div class="px-2">
<BaseButton block small @click="emitCreate"></BaseButton>
<BaseButton
block
size="small"
@click="emitCreate"
/>
</div>
</template>
</v-autocomplete>
@ -44,13 +49,13 @@
* Both the ID and Item can be synced. The item can be synced using the v-model syntax and the itemId can be synced
* using the .sync syntax `item-id.sync="item.labelId"`
*/
import { computed, defineComponent, ref } from "@nuxtjs/composition-api";
import { MultiPurposeLabelSummary } from "~/lib/api/types/labels";
import { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
export default defineComponent({
import type { MultiPurposeLabelSummary } from "~/lib/api/types/labels";
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
export default defineNuxtComponent({
props: {
value: {
modelValue: {
type: Object as () => MultiPurposeLabelSummary | IngredientFood | IngredientUnit,
required: false,
default: () => {
@ -70,7 +75,12 @@ export default defineComponent({
required: false,
default: undefined,
},
create: {
type: Boolean,
default: false,
},
},
emits: ["update:modelValue", "update:item-id", "create"],
setup(props, context) {
const autocompleteRef = ref<HTMLInputElement>();
const searchInput = ref("");
@ -85,11 +95,16 @@ export default defineComponent({
const itemVal = computed({
get: () => {
return props.value;
try {
return Object.keys(props.modelValue).length !== 0 ? props.modelValue : null;
}
catch {
return null;
}
},
set: (val) => {
itemIdVal.value = val?.id || undefined;
context.emit("input", val);
context.emit("update:modelValue", val);
},
});

View file

@ -1,5 +1,8 @@
<template>
<div class="d-flex align-center" style="max-width: 60px">
<div
class="d-flex align-center"
style="max-width: 60px"
>
<v-text-field
v-model.number="quantity"
hide-details
@ -8,17 +11,14 @@
:max="max"
type="number"
class="rounded-xl"
small
text
>
</v-text-field>
size="small"
variant="plain"
/>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
export default defineNuxtComponent({
name: "VInputNumber",
props: {
min: {
@ -37,18 +37,19 @@ export default defineComponent({
type: Number,
default: 1,
},
value: {
modelValue: {
type: Number,
default: 0,
},
},
emits: ["update:modelValue"],
setup(props, context) {
const quantity = computed({
get: () => {
return Number(props.value);
return Number(props.modelValue);
},
set: (val) => {
context.emit("input", val);
context.emit("update:modelValue", val);
},
});

View file

@ -1,52 +1,70 @@
<template>
<BaseDialog v-model="dialog" :icon="$globals.icons.translate" :title="$tc('language-dialog.choose-language')">
<BaseDialog
v-model="dialog"
:icon="$globals.icons.translate"
:title="$t('language-dialog.choose-language')"
>
<v-card-text>
{{ $t("language-dialog.select-description") }}
<v-autocomplete v-model="locale" :items="locales" item-text="name" class="my-3" hide-details outlined offset>
<template #item="{ item }">
<v-list-item-content>
<v-list-item-title> {{ item.name }} </v-list-item-title>
<v-list-item-subtitle> {{ item.progress }}% {{ $tc("language-dialog.translated") }} </v-list-item-subtitle>
</v-list-item-content>
<v-autocomplete
v-model="locale"
:items="locales"
item-title="name"
class="my-3"
hide-details
variant="outlined"
offset
>
<template #item="{ item, props }">
<div
v-bind="props"
class="px-2 py-2"
>
<v-list-item-title> {{ item.raw.name }} </v-list-item-title>
<v-list-item-subtitle>
{{ item.raw.progress }}% {{ $t("language-dialog.translated") }}
</v-list-item-subtitle>
</div>
</template>
</v-autocomplete>
<i18n path="language-dialog.how-to-contribute-description">
<i18n-t keypath="language-dialog.how-to-contribute-description">
<template #read-the-docs-link>
<a href="https://docs.mealie.io/contributors/translating/" target="_blank">{{
$t("language-dialog.read-the-docs")
}}</a>
<a
href="https://docs.mealie.io/contributors/translating/"
target="_blank"
>
{{ $t("language-dialog.read-the-docs") }}
</a>
</template>
</i18n>
</i18n-t>
</v-card-text>
</BaseDialog>
</template>
<script lang="ts">
import { computed, defineComponent } from "@nuxtjs/composition-api";
import type { LocaleObject } from "@nuxtjs/i18n";
import { useLocales } from "~/composables/use-locales";
export default defineComponent({
export default defineNuxtComponent({
props: {
value: {
modelValue: {
type: Boolean,
default: false,
required: true,
},
},
setup(props, context) {
const dialog = computed<boolean>({
get() {
return props.value;
},
set(val) {
context.emit("input", val);
},
emits: ["update:modelValue"],
setup(props, { emit }) {
const dialog = computed({
get: () => props.modelValue,
set: value => emit("update:modelValue", value),
});
const { locales: LOCALES, locale, i18n } = useLocales();
watch(locale, () => {
dialog.value = false; // Close dialog when locale changes
});
const locales = LOCALES.filter((locale) =>
(i18n.locales as LocaleObject[]).map((i18nLocale) => i18nLocale.code).includes(locale.value)
const locales = LOCALES.filter(lc =>
i18n.locales.value.map(i18nLocale => i18nLocale.code).includes(lc.value as any),
);
return {
@ -58,5 +76,3 @@ export default defineComponent({
},
});
</script>
<style scoped></style>

View file

@ -1,11 +1,14 @@
<template>
<div>
<div v-if="displayPreview" class="d-flex justify-end">
<div
v-if="displayPreview"
class="d-flex justify-end"
>
<BaseButtonGroup
:buttons="[
{
icon: previewState ? $globals.icons.edit : $globals.icons.eye,
text: previewState ? $tc('general.edit') : $tc('markdown-editor.preview-markdown-button-label'),
text: previewState ? $t('general.edit') : $t('markdown-editor.preview-markdown-button-label'),
event: 'toggle',
},
]"
@ -19,20 +22,22 @@
:class="label == '' ? '' : 'mt-5'"
:label="label"
auto-grow
dense
density="compact"
rows="4"
variant="underlined"
/>
<SafeMarkdown
v-else
:source="modelValue"
/>
<SafeMarkdown v-else :source="value" />
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref } from "@nuxtjs/composition-api";
export default defineComponent({
export default defineNuxtComponent({
name: "MarkdownEditor",
props: {
value: {
modelValue: {
type: String,
required: true,
},
@ -53,6 +58,7 @@ export default defineComponent({
default: () => ({}),
},
},
emits: ["update:modelValue", "input:preview"],
setup(props, context) {
const fallbackPreview = ref(false);
const previewState = computed({
@ -62,7 +68,8 @@ export default defineComponent({
set: (val) => {
if (props.preview) {
context.emit("input:preview", val);
} else {
}
else {
fallbackPreview.value = val;
}
},
@ -70,10 +77,10 @@ export default defineComponent({
const inputVal = computed({
get: () => {
return props.value;
return props.modelValue;
},
set: (val) => {
context.emit("input", val);
context.emit("update:modelValue", val);
},
});
return {

View file

@ -1,22 +1,22 @@
<template>
<VJsoneditor
:value="value"
:height="height"
:options="options"
:attrs="$attrs"
@input="$emit('input', $event)"
></VJsoneditor>
<JsonEditorVue
:model-value="modelValue"
v-bind="$attrs"
:style="{ height }"
:stringified="false"
@change="onChange"
/>
</template>
<script lang="ts">
// @ts-ignore v-jsoneditor has no types
import VJsoneditor from "v-jsoneditor";
import { defineComponent } from "@nuxtjs/composition-api";
import { defineComponent } from "vue";
import JsonEditorVue from "json-editor-vue";
export default defineComponent({
components: { VJsoneditor },
name: "RecipeJsonEditor",
components: { JsonEditorVue },
props: {
value: {
modelValue: {
type: Object,
default: () => ({}),
},
@ -24,10 +24,34 @@ export default defineComponent({
type: String,
default: "1500px",
},
options: {
type: Object,
default: () => ({}),
},
},
emits: ["update:modelValue"],
setup(_, { emit }) {
function parseEvent(event: any): object {
if (!event) {
return {};
}
try {
if (event.json) {
return event.json;
}
else if (event.text) {
return JSON.parse(event.text);
}
else {
return event;
}
}
catch {
return {};
}
}
function onChange(event: any) {
emit("update:modelValue", parseEvent(event));
}
return {
onChange,
};
},
});
</script>

View file

@ -5,19 +5,22 @@
item-key="id"
class="elevation-0"
:items-per-page="50"
@click:row="handleRowClick"
@click:row="($event, { item }) => handleRowClick(item)"
>
<template #item.category="{ item }">
<template #[`item.category`]="{ item }">
{{ capitalize(item.category) }}
</template>
<template #item.timestamp="{ item }">
{{ $d(Date.parse(item.timestamp), "long") }}
<template #[`item.timestamp`]="{ item }">
{{ $d(Date.parse(item.timestamp!), "long") }}
</template>
<template #item.status="{ item }">
{{ capitalize(item.status) }}
<template #[`item.status`]="{ item }">
{{ capitalize(item.status!) }}
</template>
<template #item.actions="{ item }">
<v-btn icon @click.stop="deleteReport(item.id)">
<template #[`item.actions`]="{ item }">
<v-btn
icon
@click.stop="deleteReport(item.id)"
>
<v-icon>{{ $globals.icons.delete }}</v-icon>
</v-btn>
</template>
@ -25,27 +28,27 @@
</template>
<script lang="ts">
import { defineComponent, useContext, useRouter } from "@nuxtjs/composition-api";
import { ReportSummary } from "~/lib/api/types/reports";
import type { ReportSummary } from "~/lib/api/types/reports";
export default defineComponent({
export default defineNuxtComponent({
props: {
items: {
required: true,
type: Array as () => Array<ReportSummary>,
},
},
emits: ["delete"],
setup(_, context) {
const { i18n } = useContext();
const i18n = useI18n();
const router = useRouter();
const headers = [
{ text: i18n.t("category.category"), value: "category" },
{ text: i18n.t("general.name"), value: "name" },
{ text: i18n.t("general.timestamp"), value: "timestamp" },
{ text: i18n.t("general.status"), value: "status" },
{ text: i18n.t("general.delete"), value: "actions" },
{ title: i18n.t("category.category"), value: "category", key: "category" },
{ title: i18n.t("general.name"), value: "name", key: "name" },
{ title: i18n.t("general.timestamp"), value: "timestamp", key: "timestamp" },
{ title: i18n.t("general.status"), value: "status", key: "status" },
{ title: i18n.t("general.delete"), value: "actions", key: "actions" },
];
function handleRowClick(item: ReportSummary) {

View file

@ -1,24 +1,21 @@
<template>
<VueMarkdown :source="sanitizeMarkdown(source)"></VueMarkdown>
<MDC
:value="value"
tag="article"
/>
</template>
<script lang="ts">
// @ts-ignore vue-markdown has no types
import VueMarkdown from "@adapttive/vue-markdown";
import { defineComponent } from "@nuxtjs/composition-api";
import DOMPurify from "isomorphic-dompurify";
export default defineComponent({
components: {
VueMarkdown,
},
export default defineNuxtComponent({
props: {
source: {
type: String,
default: "",
},
},
setup() {
setup(props) {
function sanitizeMarkdown(rawHtml: string | null | undefined): string {
if (!rawHtml) {
return "";
@ -28,40 +25,44 @@ export default defineComponent({
// List based on
// https://support.zendesk.com/hc/en-us/articles/4408824584602-Allowing-unsafe-HTML-in-help-center-articles
ALLOWED_TAGS: [
"strong", "em", "b", "i", "u", "p", "code", "pre", "samp", "kbd", "var", "sub", "sup", "dfn", "cite",
"small", "address", "hr", "br", "id", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6",
"ul", "ol", "li", "dl", "dt", "dd", "abbr", "a", "img", "blockquote", "iframe",
"del", "ins", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "colgroup",
],
"strong", "em", "b", "i", "u", "p", "code", "pre", "samp", "kbd", "var", "sub", "sup", "dfn", "cite",
"small", "address", "hr", "br", "id", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6",
"ul", "ol", "li", "dl", "dt", "dd", "abbr", "a", "img", "blockquote", "iframe",
"del", "ins", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "colgroup",
],
ADD_ATTR: [
"href", "src", "alt", "height", "width", "class", "allow", "title", "allowfullscreen", "frameborder",
"scrolling", "cite", "datetime", "name", "abbr", "target", "border",
],
"href", "src", "alt", "height", "width", "class", "allow", "title", "allowfullscreen", "frameborder",
"scrolling", "cite", "datetime", "name", "abbr", "target", "border",
],
});
return sanitized;
}
const value = computed(() => {
return sanitizeMarkdown(props.source) || "";
});
return {
sanitizeMarkdown,
value,
};
},
});
</script>
<style scoped>
::v-deep table {
:deep(table) {
border-collapse: collapse;
width: 100%;
}
::v-deep th, ::v-deep td {
:deep(th, td) {
border: 1px solid;
padding: 8px;
text-align: left;
}
::v-deep th {
:deep(th) {
font-weight: bold;
}
</style>

View file

@ -1,17 +1,29 @@
<template>
<v-card :min-width="minWidth" :to="to" :hover="to ? true : false">
<v-card
:min-width="minWidth"
:to="to"
:hover="to ? true : false"
>
<div class="d-flex flex-no-wrap">
<v-avatar class="ml-3 mr-0 mt-3" color="primary" size="36">
<v-icon color="white" class="pa-1">
<v-avatar
class="ml-3 mr-0 mt-3"
color="primary"
size="36"
>
<v-icon
color="white"
class="pa-1"
size="x-large"
>
{{ activeIcon }}
</v-icon>
</v-avatar>
<div>
<v-card-title class="text-subtitle-1 pt-2 pb-2">
<slot name="title"></slot>
<slot name="title" />
</v-card-title>
<v-card-subtitle class="pb-2">
<slot name="value"></slot>
<slot name="value" />
</v-card-subtitle>
</div>
</div>
@ -19,9 +31,7 @@
</template>
<script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
export default defineComponent({
export default defineNuxtComponent({
props: {
icon: {
type: String,
@ -37,7 +47,7 @@ export default defineComponent({
},
},
setup(props) {
const { $globals } = useContext();
const { $globals } = useNuxtApp();
const activeIcon = computed(() => {
return props.icon ?? $globals.icons.primary;

View file

@ -1,16 +1,17 @@
<template>
<component :is="tag">
<slot name="activator" v-bind="{ toggle, state }"> </slot>
<slot v-bind="{ state, toggle }"></slot>
<slot
name="activator"
v-bind="{ toggle, state }"
/>
<slot v-bind="{ state, toggle }" />
</component>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from "@nuxtjs/composition-api";
export default defineComponent({
export default defineNuxtComponent({
props: {
value: {
modelValue: {
type: Boolean,
default: false,
},
@ -19,7 +20,8 @@ export default defineComponent({
default: "div",
},
},
setup(_, context) {
emits: ["update:modelValue"],
setup(props, context) {
const state = ref(false);
const toggle = () => {
@ -27,7 +29,7 @@ export default defineComponent({
};
watch(state, () => {
context.emit("input", state);
context.emit("update:modelValue", state.value);
});
return {

View file

@ -1,49 +1,53 @@
<template>
<div
v-if="wakeIsSupported"
class="d-print-none d-flex px-2"
:class="$vuetify.breakpoint.smAndDown ? 'justify-center' : 'justify-end'"
>
<v-switch v-model="wakeLock" small :label="$t('recipe.screen-awake')" />
</div>
<div
v-if="wakeIsSupported"
class="d-print-none d-flex px-2"
:class="$vuetify.display.smAndDown ? 'justify-center' : 'justify-end'"
>
<v-switch
v-model="wakeLock"
color="primary"
:label="$t('recipe.screen-awake')"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, onMounted, onUnmounted } from "@nuxtjs/composition-api";
import { useWakeLock } from "@vueuse/core";
export default defineComponent({
setup() {
const { isSupported: wakeIsSupported, isActive, request, release } = useWakeLock();
const wakeLock = computed({
get: () => isActive.value,
set: () => {
if (isActive.value) {
unlockScreen();
} else {
lockScreen();
}
},
});
async function lockScreen() {
if (wakeIsSupported) {
console.debug("Wake Lock Requested");
await request("screen");
}
export default defineNuxtComponent({
setup() {
const { isSupported: wakeIsSupported, isActive, request, release } = useWakeLock();
const wakeLock = computed({
get: () => isActive.value,
set: () => {
if (isActive.value) {
unlockScreen();
}
async function unlockScreen() {
if (wakeIsSupported || isActive) {
console.debug("Wake Lock Released");
await release();
}
else {
lockScreen();
}
onMounted(() => lockScreen());
onUnmounted(() => unlockScreen());
},
});
async function lockScreen() {
if (wakeIsSupported) {
console.debug("Wake Lock Requested");
await request("screen");
}
}
async function unlockScreen() {
if (wakeIsSupported || isActive) {
console.debug("Wake Lock Released");
await release();
}
}
onMounted(() => lockScreen());
onUnmounted(() => unlockScreen());
return {
wakeLock,
wakeIsSupported,
};
},
return {
wakeLock,
wakeIsSupported,
};
},
});
</script>