mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-04-05 20:45:34 -04:00
chore: script setup components (#7299)
This commit is contained in:
@@ -37,73 +37,68 @@
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
setup() {
|
||||
const i18n = useI18n();
|
||||
const auth = useMealieAuth();
|
||||
const groupSlug = computed(() => auth.user.value?.groupSlug);
|
||||
const { $globals } = useNuxtApp();
|
||||
<script setup lang="ts">
|
||||
const i18n = useI18n();
|
||||
const auth = useMealieAuth();
|
||||
const groupSlug = computed(() => auth.user.value?.groupSlug);
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const sections = ref([
|
||||
const sections = ref([
|
||||
{
|
||||
title: i18n.t("profile.data-migrations"),
|
||||
color: "info",
|
||||
links: [
|
||||
{
|
||||
title: i18n.t("profile.data-migrations"),
|
||||
color: "info",
|
||||
links: [
|
||||
{
|
||||
icon: $globals.icons.backupRestore,
|
||||
to: "/admin/backups",
|
||||
text: i18n.t("settings.backup.backup-restore"),
|
||||
description: i18n.t("admin.setup.restore-from-v1-backup"),
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.import,
|
||||
to: "/group/migrations",
|
||||
text: i18n.t("migration.recipe-migration"),
|
||||
description: i18n.t("migration.coming-from-another-application-or-an-even-older-version-of-mealie"),
|
||||
},
|
||||
],
|
||||
icon: $globals.icons.backupRestore,
|
||||
to: "/admin/backups",
|
||||
text: i18n.t("settings.backup.backup-restore"),
|
||||
description: i18n.t("admin.setup.restore-from-v1-backup"),
|
||||
},
|
||||
{
|
||||
title: i18n.t("recipe.create-recipes"),
|
||||
color: "success",
|
||||
links: [
|
||||
{
|
||||
icon: $globals.icons.createAlt,
|
||||
to: computed(() => `/g/${groupSlug.value || ""}/r/create/new`),
|
||||
text: i18n.t("recipe.create-recipe"),
|
||||
description: i18n.t("recipe.create-recipe-description"),
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.link,
|
||||
to: computed(() => `/g/${groupSlug.value || ""}/r/create/url`),
|
||||
text: i18n.t("recipe.import-with-url"),
|
||||
description: i18n.t("recipe.scrape-recipe-description"),
|
||||
},
|
||||
],
|
||||
icon: $globals.icons.import,
|
||||
to: "/group/migrations",
|
||||
text: i18n.t("migration.recipe-migration"),
|
||||
description: i18n.t("migration.coming-from-another-application-or-an-even-older-version-of-mealie"),
|
||||
},
|
||||
{
|
||||
title: i18n.t("user.manage-users"),
|
||||
color: "primary",
|
||||
links: [
|
||||
{
|
||||
icon: $globals.icons.group,
|
||||
to: "/admin/manage/users",
|
||||
text: i18n.t("user.manage-users"),
|
||||
description: i18n.t("user.manage-users-description"),
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.user,
|
||||
to: "/user/profile",
|
||||
text: i18n.t("profile.manage-user-profile"),
|
||||
description: i18n.t("admin.setup.manage-profile-or-get-invite-link"),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
return { sections };
|
||||
],
|
||||
},
|
||||
});
|
||||
{
|
||||
title: i18n.t("recipe.create-recipes"),
|
||||
color: "success",
|
||||
links: [
|
||||
{
|
||||
icon: $globals.icons.createAlt,
|
||||
to: computed(() => `/g/${groupSlug.value || ""}/r/create/new`),
|
||||
text: i18n.t("recipe.create-recipe"),
|
||||
description: i18n.t("recipe.create-recipe-description"),
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.link,
|
||||
to: computed(() => `/g/${groupSlug.value || ""}/r/create/url`),
|
||||
text: i18n.t("recipe.import-with-url"),
|
||||
description: i18n.t("recipe.scrape-recipe-description"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: i18n.t("user.manage-users"),
|
||||
color: "primary",
|
||||
links: [
|
||||
{
|
||||
icon: $globals.icons.group,
|
||||
to: "/admin/manage/users",
|
||||
text: i18n.t("user.manage-users"),
|
||||
description: i18n.t("user.manage-users-description"),
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.user,
|
||||
to: "/user/profile",
|
||||
text: i18n.t("profile.manage-user-profile"),
|
||||
description: i18n.t("admin.setup.manage-profile-or-get-invite-link"),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -25,48 +25,32 @@
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import RecipeExplorerPageSearch from "./RecipeExplorerPageParts/RecipeExplorerPageSearch.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||
import { useLazyRecipes } from "~/composables/recipes";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeCardSection, RecipeExplorerPageSearch },
|
||||
setup() {
|
||||
const auth = useMealieAuth();
|
||||
const route = useRoute();
|
||||
const auth = useMealieAuth();
|
||||
const route = useRoute();
|
||||
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||
|
||||
const { recipes, appendRecipes, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||
const { recipes, appendRecipes, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||
|
||||
const ready = ref(false);
|
||||
const searchComponent = ref<InstanceType<typeof RecipeExplorerPageSearch>>();
|
||||
const ready = ref(false);
|
||||
const searchComponent = ref<InstanceType<typeof RecipeExplorerPageSearch>>();
|
||||
|
||||
const searchQuery = computed(() => {
|
||||
return searchComponent.value?.passedQueryWithSeed || {};
|
||||
});
|
||||
|
||||
function onSearchReady() {
|
||||
ready.value = true;
|
||||
}
|
||||
|
||||
function onItemSelected(item: any, urlPrefix: string) {
|
||||
searchComponent.value?.filterItems(item, urlPrefix);
|
||||
}
|
||||
|
||||
return {
|
||||
ready,
|
||||
searchComponent,
|
||||
searchQuery,
|
||||
recipes,
|
||||
appendRecipes,
|
||||
replaceRecipes,
|
||||
onSearchReady,
|
||||
onItemSelected,
|
||||
};
|
||||
},
|
||||
const searchQuery = computed(() => {
|
||||
return searchComponent.value?.passedQueryWithSeed || {};
|
||||
});
|
||||
|
||||
function onSearchReady() {
|
||||
ready.value = true;
|
||||
}
|
||||
|
||||
function onItemSelected(item: any, urlPrefix: string) {
|
||||
searchComponent.value?.filterItems(item, urlPrefix);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
nudge-bottom="3"
|
||||
:close-on-content-click="false"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-badge
|
||||
v-memo="[selectedCount]"
|
||||
:model-value="selectedCount > 0"
|
||||
@@ -19,7 +19,7 @@
|
||||
size="small"
|
||||
color="accent"
|
||||
dark
|
||||
v-bind="props"
|
||||
v-bind="menuProps"
|
||||
>
|
||||
<slot />
|
||||
</v-btn>
|
||||
@@ -145,89 +145,72 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { ISearchableItem } from "~/composables/use-search";
|
||||
import { useSearch } from "~/composables/use-search";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
items: {
|
||||
type: Array as () => ISearchableItem[],
|
||||
required: true,
|
||||
},
|
||||
modelValue: {
|
||||
type: Array as () => any[],
|
||||
required: true,
|
||||
},
|
||||
requireAll: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
radio: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array as () => ISearchableItem[],
|
||||
required: true,
|
||||
},
|
||||
emits: ["update:requireAll", "update:modelValue"],
|
||||
setup(props, context) {
|
||||
const state = reactive({
|
||||
menu: false,
|
||||
});
|
||||
|
||||
// Use the search composable
|
||||
const { search: searchInput, filtered } = useSearch(computed(() => props.items));
|
||||
|
||||
const combinator = computed({
|
||||
get: () => (props.requireAll ? "hasAll" : "hasAny"),
|
||||
set: (value) => {
|
||||
context.emit("update:requireAll", value === "hasAll");
|
||||
},
|
||||
});
|
||||
|
||||
// Use shallowRef to prevent deep reactivity on large arrays
|
||||
const selected = computed({
|
||||
get: () => props.modelValue as ISearchableItem[],
|
||||
set: (value) => {
|
||||
context.emit("update:modelValue", value);
|
||||
},
|
||||
});
|
||||
|
||||
const selectedRadio = computed({
|
||||
get: () => (selected.value.length > 0 ? selected.value[0] : null),
|
||||
set: (value) => {
|
||||
context.emit("update:modelValue", value ? [value] : []);
|
||||
},
|
||||
});
|
||||
|
||||
const selectedCount = computed(() => selected.value.length);
|
||||
const selectedIds = computed(() => {
|
||||
return new Set(selected.value.map(item => item.id));
|
||||
});
|
||||
|
||||
const handleRadioClick = (item: ISearchableItem) => {
|
||||
if (selectedRadio.value === item) {
|
||||
selectedRadio.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
function clearSelection() {
|
||||
selected.value = [];
|
||||
selectedRadio.value = null;
|
||||
searchInput.value = "";
|
||||
}
|
||||
|
||||
return {
|
||||
combinator,
|
||||
state,
|
||||
searchInput,
|
||||
selected,
|
||||
selectedRadio,
|
||||
selectedCount,
|
||||
selectedIds,
|
||||
filtered,
|
||||
handleRadioClick,
|
||||
clearSelection,
|
||||
};
|
||||
requireAll: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
radio: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const modelValue = defineModel<ISearchableItem[]>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:requireAll", value: boolean | undefined): void;
|
||||
}>();
|
||||
|
||||
const state = reactive({
|
||||
menu: false,
|
||||
});
|
||||
|
||||
// Use the search composable
|
||||
const { search: searchInput, filtered } = useSearch(computed(() => props.items));
|
||||
|
||||
const combinator = computed({
|
||||
get: () => (props.requireAll ? "hasAll" : "hasAny"),
|
||||
set: (value: string) => {
|
||||
emit("update:requireAll", value === "hasAll");
|
||||
},
|
||||
});
|
||||
|
||||
const selected = computed<ISearchableItem[]>({
|
||||
get: () => modelValue.value ?? [],
|
||||
set: (value: ISearchableItem[]) => {
|
||||
modelValue.value = value;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedRadio = computed<null | ISearchableItem>({
|
||||
get: () => (selected.value.length > 0 ? selected.value[0] : null),
|
||||
set: (value: ISearchableItem | null) => {
|
||||
const next = value ? [value] : [];
|
||||
selected.value = next;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedCount = computed(() => selected.value.length);
|
||||
const selectedIds = computed(() => new Set(selected.value.map(item => item.id)));
|
||||
|
||||
const handleRadioClick = (item: ISearchableItem) => {
|
||||
if (selectedRadio.value === item) {
|
||||
selectedRadio.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
function clearSelection() {
|
||||
selected.value = [];
|
||||
selectedRadio.value = null;
|
||||
searchInput.value = "";
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -12,23 +12,13 @@
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { getTextColor } from "~/composables/use-text-color";
|
||||
import type { MultiPurposeLabelSummary } from "~/lib/api/types/recipe";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
label: {
|
||||
type: Object as () => MultiPurposeLabelSummary,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const textColor = computed(() => getTextColor(props.label.color));
|
||||
const props = defineProps<{
|
||||
label: MultiPurposeLabelSummary;
|
||||
}>();
|
||||
|
||||
return {
|
||||
textColor,
|
||||
};
|
||||
},
|
||||
});
|
||||
const textColor = computed(() => getTextColor(props.label.color));
|
||||
</script>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
start
|
||||
min-width="125px"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: hoverProps }">
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="text"
|
||||
class="ml-2 handle"
|
||||
icon
|
||||
v-bind="props"
|
||||
v-bind="hoverProps"
|
||||
>
|
||||
<v-icon>
|
||||
{{ $globals.icons.arrowUpDown }}
|
||||
@@ -35,31 +35,13 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { ShoppingListMultiPurposeLabelOut } from "~/lib/api/types/household";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object as () => ShoppingListMultiPurposeLabelOut,
|
||||
required: true,
|
||||
},
|
||||
useColor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const labelColor = ref<string | undefined>(props.useColor ? props.modelValue.label.color : undefined);
|
||||
const props = defineProps<{
|
||||
useColor?: boolean;
|
||||
}>();
|
||||
const modelValue = defineModel<ShoppingListMultiPurposeLabelOut>({ required: true });
|
||||
|
||||
function contextHandler(event: string) {
|
||||
context.emit(event);
|
||||
}
|
||||
|
||||
return {
|
||||
contextHandler,
|
||||
labelColor,
|
||||
};
|
||||
},
|
||||
});
|
||||
const labelColor = ref<string | undefined>(props.useColor ? modelValue.value.label.color : undefined);
|
||||
</script>
|
||||
|
||||
@@ -10,15 +10,12 @@
|
||||
<v-col :cols="itemLabelCols">
|
||||
<div class="d-flex align-center flex-nowrap">
|
||||
<v-checkbox
|
||||
v-model="listItem.checked"
|
||||
:model-value="listItem.checked"
|
||||
hide-details
|
||||
density="compact"
|
||||
class="mt-0 flex-shrink-0"
|
||||
color="null"
|
||||
@click="() => {
|
||||
listItem.checked = !listItem.checked
|
||||
$emit('checked', listItem)
|
||||
}"
|
||||
@click="toggleChecked"
|
||||
/>
|
||||
<div
|
||||
class="ml-2 text-truncate"
|
||||
@@ -43,7 +40,7 @@
|
||||
start
|
||||
min-width="125px"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: hoverProps }">
|
||||
<v-tooltip
|
||||
v-if="recipeList && recipeList.length"
|
||||
open-delay="200"
|
||||
@@ -84,7 +81,7 @@
|
||||
variant="text"
|
||||
class="handle"
|
||||
icon
|
||||
v-bind="props"
|
||||
v-bind="hoverProps"
|
||||
>
|
||||
<v-icon>
|
||||
{{ $globals.icons.arrowUpDown }}
|
||||
@@ -155,158 +152,99 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { useOnline } from "@vueuse/core";
|
||||
import RecipeIngredientListItem from "../Recipe/RecipeIngredientListItem.vue";
|
||||
import ShoppingListItemEditor from "./ShoppingListItemEditor.vue";
|
||||
import type { ShoppingListItemOut } from "~/lib/api/types/household";
|
||||
import type { MultiPurposeLabelOut, MultiPurposeLabelSummary } from "~/lib/api/types/labels";
|
||||
import type { MultiPurposeLabelOut } from "~/lib/api/types/labels";
|
||||
import type { IngredientFood, IngredientUnit, RecipeSummary } from "~/lib/api/types/recipe";
|
||||
import RecipeList from "~/components/Domain/Recipe/RecipeList.vue";
|
||||
|
||||
interface actions {
|
||||
text: string;
|
||||
event: string;
|
||||
const model = defineModel<ShoppingListItemOut>({ type: Object as () => ShoppingListItemOut, required: true });
|
||||
|
||||
const props = defineProps({
|
||||
labels: {
|
||||
type: Array as () => MultiPurposeLabelOut[],
|
||||
required: true,
|
||||
},
|
||||
units: {
|
||||
type: Array as () => IngredientUnit[],
|
||||
required: true,
|
||||
},
|
||||
foods: {
|
||||
type: Array as () => IngredientFood[],
|
||||
required: true,
|
||||
},
|
||||
recipes: {
|
||||
type: Map as unknown as () => Map<string, RecipeSummary>,
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "checked" | "save", item: ShoppingListItemOut): void;
|
||||
(e: "delete"): void;
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const displayRecipeRefs = ref(false);
|
||||
const itemLabelCols = computed<string>(() => (model.value?.checked ? "auto" : "6"));
|
||||
const online = useOnline();
|
||||
const isOffline = computed(() => online.value === false);
|
||||
|
||||
type actions = { text: string; event: string };
|
||||
const contextMenu = ref<actions[]>([
|
||||
{ text: i18n.t("general.edit") as string, event: "edit" },
|
||||
{ text: i18n.t("general.delete") as string, event: "delete" },
|
||||
]);
|
||||
|
||||
// copy prop value so a refresh doesn't interrupt the user
|
||||
const localListItem = ref(Object.assign({}, model.value));
|
||||
|
||||
const listItem = computed<ShoppingListItemOut>({
|
||||
get: () => model.value,
|
||||
set: (val: ShoppingListItemOut) => {
|
||||
localListItem.value = val;
|
||||
model.value = val;
|
||||
},
|
||||
});
|
||||
|
||||
const edit = ref(false);
|
||||
function toggleEdit(val = !edit.value) {
|
||||
if (edit.value === val) return;
|
||||
if (val) localListItem.value = model.value;
|
||||
edit.value = val;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { ShoppingListItemEditor, RecipeList, RecipeIngredientListItem },
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object as () => ShoppingListItemOut,
|
||||
required: true,
|
||||
},
|
||||
labels: {
|
||||
type: Array as () => MultiPurposeLabelOut[],
|
||||
required: true,
|
||||
},
|
||||
units: {
|
||||
type: Array as () => IngredientUnit[],
|
||||
required: true,
|
||||
},
|
||||
foods: {
|
||||
type: Array as () => IngredientFood[],
|
||||
required: true,
|
||||
},
|
||||
recipes: {
|
||||
type: Map<string, RecipeSummary>,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ["checked", "update:modelValue", "save", "delete"],
|
||||
setup(props, context) {
|
||||
const i18n = useI18n();
|
||||
const displayRecipeRefs = ref(false);
|
||||
const itemLabelCols = ref<string>(props.modelValue.checked ? "auto" : "6");
|
||||
const isOffline = computed(() => useOnline().value === false);
|
||||
function toggleChecked() {
|
||||
const updated = { ...model.value, checked: !model.value.checked } as ShoppingListItemOut;
|
||||
model.value = updated;
|
||||
emit("checked", updated);
|
||||
}
|
||||
|
||||
const contextMenu: actions[] = [
|
||||
{
|
||||
text: i18n.t("general.edit") as string,
|
||||
event: "edit",
|
||||
},
|
||||
{
|
||||
text: i18n.t("general.delete") as string,
|
||||
event: "delete",
|
||||
},
|
||||
];
|
||||
function contextHandler(event: string) {
|
||||
if (event === "edit") {
|
||||
toggleEdit(true);
|
||||
}
|
||||
else {
|
||||
emit(event as any);
|
||||
}
|
||||
}
|
||||
|
||||
// copy prop value so a refresh doesn't interrupt the user
|
||||
const localListItem = ref(Object.assign({}, props.modelValue));
|
||||
const listItem = computed({
|
||||
get: () => {
|
||||
return props.modelValue;
|
||||
},
|
||||
set: (val) => {
|
||||
// keep local copy in sync
|
||||
localListItem.value = val;
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
const edit = ref(false);
|
||||
function toggleEdit(val = !edit.value) {
|
||||
if (edit.value === val) {
|
||||
return;
|
||||
}
|
||||
function save() {
|
||||
emit("save", localListItem.value);
|
||||
edit.value = false;
|
||||
}
|
||||
|
||||
if (val) {
|
||||
// update local copy of item with the current value
|
||||
localListItem.value = props.modelValue;
|
||||
}
|
||||
|
||||
edit.value = val;
|
||||
}
|
||||
|
||||
function contextHandler(event: string) {
|
||||
if (event === "edit") {
|
||||
toggleEdit(true);
|
||||
}
|
||||
else {
|
||||
context.emit(event);
|
||||
}
|
||||
}
|
||||
function save() {
|
||||
context.emit("save", localListItem.value);
|
||||
edit.value = false;
|
||||
}
|
||||
|
||||
const updatedLabels = computed(() => {
|
||||
return props.labels.map((label) => {
|
||||
return {
|
||||
id: label.id,
|
||||
text: label.name,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Gets the label for the shopping list item. Either the label assign to the item
|
||||
* or the label of the food applied.
|
||||
*/
|
||||
const label = computed<MultiPurposeLabelSummary | undefined>(() => {
|
||||
if (listItem.value.label) {
|
||||
return listItem.value.label as MultiPurposeLabelSummary;
|
||||
}
|
||||
|
||||
if (listItem.value.food?.label) {
|
||||
return listItem.value.food.label;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const recipeList = computed<RecipeSummary[]>(() => {
|
||||
const recipeList: RecipeSummary[] = [];
|
||||
if (!listItem.value.recipeReferences) {
|
||||
return recipeList;
|
||||
}
|
||||
|
||||
listItem.value.recipeReferences.forEach((ref) => {
|
||||
const recipe = props.recipes?.get(ref.recipeId);
|
||||
if (recipe) {
|
||||
recipeList.push(recipe);
|
||||
}
|
||||
});
|
||||
|
||||
return recipeList;
|
||||
});
|
||||
|
||||
return {
|
||||
updatedLabels,
|
||||
save,
|
||||
contextHandler,
|
||||
displayRecipeRefs,
|
||||
edit,
|
||||
contextMenu,
|
||||
itemLabelCols,
|
||||
listItem,
|
||||
localListItem,
|
||||
label,
|
||||
recipeList,
|
||||
toggleEdit,
|
||||
isOffline,
|
||||
};
|
||||
},
|
||||
const recipeList = computed<RecipeSummary[]>(() => {
|
||||
const ret: RecipeSummary[] = [];
|
||||
if (!listItem.value.recipeReferences) return ret;
|
||||
listItem.value.recipeReferences.forEach((ref) => {
|
||||
const recipe = props.recipes?.get(ref.recipeId);
|
||||
if (recipe) ret.push(recipe);
|
||||
});
|
||||
return ret;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -102,125 +102,108 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { ShoppingListItemCreate, ShoppingListItemOut } from "~/lib/api/types/household";
|
||||
import type { MultiPurposeLabelOut } from "~/lib/api/types/labels";
|
||||
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||
import { useFoodStore, useFoodData, useUnitStore, useUnitData } from "~/composables/store";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object as () => ShoppingListItemCreate | ShoppingListItemOut,
|
||||
required: true,
|
||||
},
|
||||
labels: {
|
||||
type: Array as () => MultiPurposeLabelOut[],
|
||||
required: true,
|
||||
},
|
||||
units: {
|
||||
type: Array as () => IngredientUnit[],
|
||||
required: true,
|
||||
},
|
||||
foods: {
|
||||
type: Array as () => IngredientFood[],
|
||||
required: true,
|
||||
},
|
||||
allowDelete: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
// modelValue as reactive v-model
|
||||
const listItem = defineModel<ShoppingListItemCreate | ShoppingListItemOut>({ required: true });
|
||||
|
||||
defineProps({
|
||||
labels: {
|
||||
type: Array as () => MultiPurposeLabelOut[],
|
||||
required: true,
|
||||
},
|
||||
emits: ["update:modelValue", "save", "cancel", "delete"],
|
||||
setup(props, context) {
|
||||
const foodStore = useFoodStore();
|
||||
const foodData = useFoodData();
|
||||
|
||||
const unitStore = useUnitStore();
|
||||
const unitData = useUnitData();
|
||||
|
||||
const listItem = computed({
|
||||
get: () => {
|
||||
return props.modelValue;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue.quantity,
|
||||
() => {
|
||||
if (!props.modelValue.quantity) {
|
||||
listItem.value.quantity = 0;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.modelValue.food,
|
||||
(newFood) => {
|
||||
listItem.value.label = newFood?.label || null;
|
||||
listItem.value.labelId = listItem.value.label?.id || null;
|
||||
},
|
||||
);
|
||||
|
||||
const autoFocus = !listItem.value.food && listItem.value.note ? "note" : "food";
|
||||
|
||||
async function createAssignFood(val: string) {
|
||||
// keep UI reactive
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
listItem.value.food ? (listItem.value.food.name = val) : (listItem.value.food = { name: val });
|
||||
|
||||
foodData.data.name = val;
|
||||
const newFood = await foodStore.actions.createOne(foodData.data);
|
||||
if (newFood) {
|
||||
listItem.value.food = newFood;
|
||||
listItem.value.foodId = newFood.id;
|
||||
}
|
||||
foodData.reset();
|
||||
}
|
||||
|
||||
async function createAssignUnit(val: string) {
|
||||
// keep UI reactive
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
listItem.value.unit ? (listItem.value.unit.name = val) : (listItem.value.unit = { name: val });
|
||||
|
||||
unitData.data.name = val;
|
||||
const newUnit = await unitStore.actions.createOne(unitData.data);
|
||||
if (newUnit) {
|
||||
listItem.value.unit = newUnit;
|
||||
listItem.value.unitId = newUnit.id;
|
||||
}
|
||||
unitData.reset();
|
||||
}
|
||||
|
||||
async function assignLabelToFood() {
|
||||
if (!(listItem.value.food && listItem.value.foodId && listItem.value.labelId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
listItem.value.food.labelId = listItem.value.labelId;
|
||||
await foodStore.actions.updateOne(listItem.value.food);
|
||||
}
|
||||
|
||||
return {
|
||||
listItem,
|
||||
autoFocus,
|
||||
createAssignFood,
|
||||
createAssignUnit,
|
||||
assignLabelToFood,
|
||||
};
|
||||
units: {
|
||||
type: Array as () => IngredientUnit[],
|
||||
required: true,
|
||||
},
|
||||
methods: {
|
||||
handleNoteKeyPress(event) {
|
||||
// Save on Enter
|
||||
if (!event.shiftKey && event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
this.$emit("save");
|
||||
}
|
||||
},
|
||||
foods: {
|
||||
type: Array as () => IngredientFood[],
|
||||
required: true,
|
||||
},
|
||||
allowDelete: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
// const emit = defineEmits<["save", "cancel", "delete"]>();
|
||||
const emit = defineEmits<{
|
||||
(e: "save", item: ShoppingListItemOut): void;
|
||||
(e: "cancel" | "delete"): void;
|
||||
}>();
|
||||
|
||||
const foodStore = useFoodStore();
|
||||
const foodData = useFoodData();
|
||||
|
||||
const unitStore = useUnitStore();
|
||||
const unitData = useUnitData();
|
||||
|
||||
watch(
|
||||
() => listItem.value.quantity,
|
||||
(newQty) => {
|
||||
if (!newQty) {
|
||||
listItem.value.quantity = 0;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => listItem.value.food,
|
||||
(newFood) => {
|
||||
listItem.value.label = newFood?.label || null;
|
||||
listItem.value.labelId = listItem.value.label?.id || null;
|
||||
},
|
||||
);
|
||||
|
||||
const autoFocus = computed(() => (!listItem.value.food && listItem.value.note ? "note" : "food"));
|
||||
|
||||
async function createAssignFood(val: string) {
|
||||
// keep UI reactive
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
listItem.value.food ? (listItem.value.food.name = val) : (listItem.value.food = { name: val } as any);
|
||||
|
||||
foodData.data.name = val;
|
||||
const newFood = await foodStore.actions.createOne(foodData.data);
|
||||
if (newFood) {
|
||||
listItem.value.food = newFood;
|
||||
listItem.value.foodId = newFood.id;
|
||||
}
|
||||
foodData.reset();
|
||||
}
|
||||
|
||||
async function createAssignUnit(val: string) {
|
||||
// keep UI reactive
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
listItem.value.unit ? (listItem.value.unit.name = val) : (listItem.value.unit = { name: val } as any);
|
||||
|
||||
unitData.data.name = val;
|
||||
const newUnit = await unitStore.actions.createOne(unitData.data);
|
||||
if (newUnit) {
|
||||
listItem.value.unit = newUnit;
|
||||
listItem.value.unitId = newUnit.id;
|
||||
}
|
||||
unitData.reset();
|
||||
}
|
||||
|
||||
async function assignLabelToFood() {
|
||||
if (!(listItem.value.food && listItem.value.foodId && listItem.value.labelId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
listItem.value.food.labelId = listItem.value.labelId;
|
||||
await foodStore.actions.updateOne(listItem.value.food);
|
||||
}
|
||||
|
||||
function handleNoteKeyPress(event: KeyboardEvent) {
|
||||
const e = event as KeyboardEvent & { key: string; shiftKey: boolean };
|
||||
if (!e.shiftKey && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
emit("save");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
:disabled="!user || !tooltip"
|
||||
location="end"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-avatar
|
||||
v-if="list"
|
||||
v-bind="props"
|
||||
v-bind="tooltipProps"
|
||||
>
|
||||
<v-img
|
||||
:src="imageURL"
|
||||
@@ -19,7 +19,7 @@
|
||||
<v-avatar
|
||||
v-else
|
||||
:size="size"
|
||||
v-bind="props"
|
||||
v-bind="tooltipProps"
|
||||
>
|
||||
<v-img
|
||||
:src="imageURL"
|
||||
@@ -35,51 +35,40 @@
|
||||
</v-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from "~/composables/store/use-user-store";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
list: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "42",
|
||||
},
|
||||
tooltip: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
const props = defineProps({
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
setup(props) {
|
||||
const state = reactive({
|
||||
error: false,
|
||||
});
|
||||
|
||||
const auth = useMealieAuth();
|
||||
const { store: users } = useUserStore();
|
||||
const user = computed(() => {
|
||||
return users.value.find(user => user.id === props.userId);
|
||||
});
|
||||
|
||||
const imageURL = computed(() => {
|
||||
// Note: auth.user is a ref now
|
||||
const authUser = auth.user.value;
|
||||
const key = authUser?.cacheKey ?? "";
|
||||
return `/api/media/users/${props.userId}/profile.webp?cacheKey=${key}`;
|
||||
});
|
||||
|
||||
return {
|
||||
user,
|
||||
imageURL,
|
||||
...toRefs(state),
|
||||
};
|
||||
list: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "42",
|
||||
},
|
||||
tooltip: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const error = ref(false);
|
||||
|
||||
const auth = useMealieAuth();
|
||||
const { store: users } = useUserStore();
|
||||
const user = computed(() => {
|
||||
return users.value.find(user => user.id === props.userId);
|
||||
});
|
||||
|
||||
const imageURL = computed(() => {
|
||||
// Note: auth.user is a ref now
|
||||
const authUser = auth.user.value;
|
||||
const key = authUser?.cacheKey ?? "";
|
||||
return `/api/media/users/${props.userId}/profile.webp?cacheKey=${key}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -73,8 +73,7 @@
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { watchEffect } from "vue";
|
||||
<script setup lang="ts">
|
||||
import { useUserApi } from "@/composables/api";
|
||||
import BaseDialog from "~/components/global/BaseDialog.vue";
|
||||
import AppButtonCopy from "~/components/global/AppButtonCopy.vue";
|
||||
@@ -86,147 +85,95 @@ import type { HouseholdInDB } from "~/lib/api/types/household";
|
||||
import { useGroups } from "~/composables/use-groups";
|
||||
import { useAdminHouseholds } from "~/composables/use-households";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
name: "UserInviteDialog",
|
||||
components: {
|
||||
BaseDialog,
|
||||
AppButtonCopy,
|
||||
BaseButton,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const i18n = useI18n();
|
||||
const auth = useMealieAuth();
|
||||
const inviteDialog = defineModel<boolean>("modelValue", { type: Boolean, default: false });
|
||||
|
||||
const isAdmin = computed(() => auth.user.value?.admin);
|
||||
const token = ref("");
|
||||
const selectedGroup = ref<string | null>(null);
|
||||
const selectedHousehold = ref<string | null>(null);
|
||||
const groups = ref<GroupInDB[]>([]);
|
||||
const households = ref<HouseholdInDB[]>([]);
|
||||
const api = useUserApi();
|
||||
const i18n = useI18n();
|
||||
const auth = useMealieAuth();
|
||||
|
||||
const fetchGroupsAndHouseholds = () => {
|
||||
if (isAdmin.value) {
|
||||
const groupsResponse = useGroups();
|
||||
const householdsResponse = useAdminHouseholds();
|
||||
watchEffect(() => {
|
||||
groups.value = groupsResponse.groups.value || [];
|
||||
households.value = householdsResponse.households.value || [];
|
||||
});
|
||||
}
|
||||
};
|
||||
const isAdmin = computed(() => auth.user.value?.admin);
|
||||
const token = ref("");
|
||||
const selectedGroup = ref<string | null>(null);
|
||||
const selectedHousehold = ref<string | null>(null);
|
||||
const groups = ref<GroupInDB[]>([]);
|
||||
const households = ref<HouseholdInDB[]>([]);
|
||||
const api = useUserApi();
|
||||
|
||||
const inviteDialog = computed<boolean>({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(val) {
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
const fetchGroupsAndHouseholds = () => {
|
||||
if (isAdmin.value) {
|
||||
const groupsResponse = useGroups();
|
||||
const householdsResponse = useAdminHouseholds();
|
||||
watchEffect(() => {
|
||||
groups.value = groupsResponse.groups.value || [];
|
||||
households.value = householdsResponse.households.value || [];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async function getSignupLink(group: string | null = null, household: string | null = null) {
|
||||
const payload = (group && household) ? { uses: 1, group_id: group, household_id: household } : { uses: 1 };
|
||||
const { data } = await api.households.createInvitation(payload);
|
||||
if (data) {
|
||||
token.value = data.token;
|
||||
}
|
||||
}
|
||||
async function getSignupLink(group: string | null = null, household: string | null = null) {
|
||||
const payload = (group && household) ? { uses: 1, group_id: group, household_id: household } : { uses: 1 };
|
||||
const { data } = await api.households.createInvitation(payload);
|
||||
if (data) {
|
||||
token.value = data.token;
|
||||
}
|
||||
}
|
||||
|
||||
const filteredHouseholds = computed(() => {
|
||||
if (!selectedGroup.value) return [];
|
||||
return households.value?.filter(household => household.groupId === selectedGroup.value);
|
||||
});
|
||||
|
||||
function constructLink(token: string) {
|
||||
return token ? `${window.location.origin}/register?token=${token}` : "";
|
||||
}
|
||||
|
||||
const generatedSignupLink = computed(() => {
|
||||
return constructLink(token.value);
|
||||
});
|
||||
|
||||
// =================================================
|
||||
// Email Invitation
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
sendTo: "",
|
||||
});
|
||||
|
||||
async function sendInvite() {
|
||||
state.loading = true;
|
||||
if (!token.value) {
|
||||
getSignupLink(selectedGroup.value, selectedHousehold.value);
|
||||
}
|
||||
const { data } = await api.email.sendInvitation({
|
||||
email: state.sendTo,
|
||||
token: token.value,
|
||||
});
|
||||
|
||||
if (data && data.success) {
|
||||
alert.success(i18n.t("profile.email-sent"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("profile.error-sending-email"));
|
||||
}
|
||||
state.loading = false;
|
||||
inviteDialog.value = false;
|
||||
}
|
||||
|
||||
const validEmail = computed(() => {
|
||||
if (state.sendTo === "") {
|
||||
return false;
|
||||
}
|
||||
const valid = validators.email(state.sendTo);
|
||||
|
||||
// Explicit bool check because validators.email sometimes returns a string
|
||||
if (valid === true) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return {
|
||||
sendInvite,
|
||||
validators,
|
||||
validEmail,
|
||||
inviteDialog,
|
||||
getSignupLink,
|
||||
generatedSignupLink,
|
||||
selectedGroup,
|
||||
selectedHousehold,
|
||||
filteredHouseholds,
|
||||
groups,
|
||||
households,
|
||||
fetchGroupsAndHouseholds,
|
||||
...toRefs(state),
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
immediate: false,
|
||||
handler(val) {
|
||||
if (val && !this.isAdmin) {
|
||||
this.getSignupLink();
|
||||
}
|
||||
},
|
||||
},
|
||||
selectedHousehold(newVal) {
|
||||
if (newVal && this.selectedGroup) {
|
||||
this.getSignupLink(this.selectedGroup, this.selectedHousehold);
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchGroupsAndHouseholds();
|
||||
},
|
||||
const filteredHouseholds = computed(() => {
|
||||
if (!selectedGroup.value) return [];
|
||||
return households.value?.filter(household => household.groupId === selectedGroup.value);
|
||||
});
|
||||
|
||||
function constructLink(tokenVal: string) {
|
||||
return tokenVal ? `${window.location.origin}/register?token=${tokenVal}` : "";
|
||||
}
|
||||
|
||||
const generatedSignupLink = computed(() => constructLink(token.value));
|
||||
|
||||
// Email Invitation
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
sendTo: "",
|
||||
});
|
||||
const { loading, sendTo } = toRefs(state);
|
||||
|
||||
async function sendInvite() {
|
||||
state.loading = true;
|
||||
if (!token.value) {
|
||||
getSignupLink(selectedGroup.value, selectedHousehold.value);
|
||||
}
|
||||
const { data } = await api.email.sendInvitation({
|
||||
email: state.sendTo,
|
||||
token: token.value,
|
||||
});
|
||||
|
||||
if (data && data.success) {
|
||||
alert.success(i18n.t("profile.email-sent"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("profile.error-sending-email"));
|
||||
}
|
||||
state.loading = false;
|
||||
inviteDialog.value = false;
|
||||
}
|
||||
|
||||
const validEmail = computed(() => {
|
||||
if (sendTo.value === "") return false;
|
||||
const valid = validators.email(sendTo.value);
|
||||
return valid === true;
|
||||
});
|
||||
|
||||
// Watchers (replacing options API watchers)
|
||||
watch(inviteDialog, (val) => {
|
||||
if (val && !isAdmin.value) {
|
||||
getSignupLink();
|
||||
}
|
||||
});
|
||||
|
||||
watch(selectedHousehold, (newVal) => {
|
||||
if (newVal && selectedGroup.value) {
|
||||
getSignupLink(selectedGroup.value, selectedHousehold.value);
|
||||
}
|
||||
});
|
||||
|
||||
// initial fetch
|
||||
fetchGroupsAndHouseholds();
|
||||
</script>
|
||||
|
||||
@@ -52,14 +52,14 @@
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
interface LinkProp {
|
||||
text: string;
|
||||
url?: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
link: {
|
||||
type: Object as () => LinkProp,
|
||||
required: true,
|
||||
@@ -70,6 +70,4 @@ const props = defineProps({
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Props", props);
|
||||
</script>
|
||||
|
||||
@@ -78,57 +78,29 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useDark } from "@vueuse/core";
|
||||
<script setup lang="ts">
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { useUserRegistrationForm } from "~/composables/use-users/user-registration-form";
|
||||
import { usePasswordField } from "~/composables/use-passwords";
|
||||
import UserPasswordStrength from "~/components/Domain/User/UserPasswordStrength.vue";
|
||||
|
||||
definePageMeta({ layout: "blank" });
|
||||
|
||||
const inputAttrs = {
|
||||
validateOnBlur: true,
|
||||
class: "pb-1",
|
||||
variant: "solo-filled" as any,
|
||||
};
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { UserPasswordStrength },
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "blank",
|
||||
});
|
||||
|
||||
const isDark = useDark();
|
||||
const langDialog = ref(false);
|
||||
|
||||
const pwFields = usePasswordField();
|
||||
const {
|
||||
accountDetails,
|
||||
credentials,
|
||||
emailErrorMessages,
|
||||
usernameErrorMessages,
|
||||
validateUsername,
|
||||
validateEmail,
|
||||
domAccountForm,
|
||||
} = useUserRegistrationForm();
|
||||
return {
|
||||
accountDetails,
|
||||
credentials,
|
||||
emailErrorMessages,
|
||||
inputAttrs,
|
||||
isDark,
|
||||
langDialog,
|
||||
pwFields,
|
||||
usernameErrorMessages,
|
||||
validators,
|
||||
// Validators
|
||||
validateUsername,
|
||||
validateEmail,
|
||||
// Dom Refs
|
||||
domAccountForm,
|
||||
};
|
||||
},
|
||||
});
|
||||
const pwFields = usePasswordField();
|
||||
const {
|
||||
accountDetails,
|
||||
credentials,
|
||||
emailErrorMessages,
|
||||
usernameErrorMessages,
|
||||
validateUsername,
|
||||
validateEmail,
|
||||
} = useUserRegistrationForm();
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
@@ -94,210 +94,195 @@
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import type { SideBarLink } from "~/types/application-types";
|
||||
import { useCookbookPreferences } from "~/composables/use-users/preferences";
|
||||
import { useCookbookStore, usePublicCookbookStore } from "~/composables/store/use-cookbook-store";
|
||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
setup() {
|
||||
const i18n = useI18n();
|
||||
const { $appInfo, $globals } = useNuxtApp();
|
||||
const display = useDisplay();
|
||||
const auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const i18n = useI18n();
|
||||
const { $appInfo, $globals } = useNuxtApp();
|
||||
const display = useDisplay();
|
||||
const auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||
|
||||
const cookbookPreferences = useCookbookPreferences();
|
||||
const ownCookbookStore = useCookbookStore(i18n);
|
||||
const publicCookbookStoreCache = ref<Record<string, ReturnType<typeof usePublicCookbookStore>>>({});
|
||||
const cookbookPreferences = useCookbookPreferences();
|
||||
const ownCookbookStore = useCookbookStore(i18n);
|
||||
const publicCookbookStoreCache = ref<Record<string, ReturnType<typeof usePublicCookbookStore>>>({});
|
||||
|
||||
function getPublicCookbookStore(slug: string) {
|
||||
if (!publicCookbookStoreCache.value[slug]) {
|
||||
publicCookbookStoreCache.value[slug] = usePublicCookbookStore(slug, i18n);
|
||||
}
|
||||
return publicCookbookStoreCache.value[slug];
|
||||
}
|
||||
function getPublicCookbookStore(slug: string) {
|
||||
if (!publicCookbookStoreCache.value[slug]) {
|
||||
publicCookbookStoreCache.value[slug] = usePublicCookbookStore(slug, i18n);
|
||||
}
|
||||
return publicCookbookStoreCache.value[slug];
|
||||
}
|
||||
|
||||
const cookbooks = computed(() => {
|
||||
if (isOwnGroup.value) {
|
||||
return ownCookbookStore.store.value;
|
||||
}
|
||||
else if (groupSlug.value) {
|
||||
const publicStore = getPublicCookbookStore(groupSlug.value);
|
||||
return unref(publicStore.store);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const showImageImport = computed(() => $appInfo.enableOpenaiImageServices);
|
||||
const languageDialog = ref<boolean>(false);
|
||||
|
||||
const sidebar = ref<boolean>(false);
|
||||
onMounted(() => {
|
||||
sidebar.value = display.lgAndUp.value;
|
||||
});
|
||||
|
||||
function cookbookAsLink(cookbook: ReadCookBook): SideBarLink {
|
||||
return {
|
||||
key: cookbook.slug || "",
|
||||
icon: $globals.icons.pages,
|
||||
title: cookbook.name,
|
||||
to: `/g/${groupSlug.value}/cookbooks/${cookbook.slug || ""}`,
|
||||
restricted: false,
|
||||
};
|
||||
}
|
||||
|
||||
const currentUserHouseholdId = computed(() => auth.user.value?.householdId);
|
||||
const cookbookLinks = computed<SideBarLink[]>(() => {
|
||||
if (!cookbooks.value?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sortedCookbooks = [...cookbooks.value].sort((a, b) => (a.position || 0) - (b.position || 0));
|
||||
|
||||
const ownLinks: SideBarLink[] = [];
|
||||
const links: SideBarLink[] = [];
|
||||
const cookbooksByHousehold = sortedCookbooks.reduce((acc, cookbook) => {
|
||||
const householdName = cookbook.household?.name || "";
|
||||
(acc[householdName] ||= []).push(cookbook);
|
||||
return acc;
|
||||
}, {} as Record<string, ReadCookBook[]>);
|
||||
|
||||
Object.entries(cookbooksByHousehold).forEach(([householdName, cookbooks]) => {
|
||||
if (!cookbooks.length) {
|
||||
return;
|
||||
}
|
||||
if (cookbooks[0].householdId === currentUserHouseholdId.value) {
|
||||
ownLinks.push(...cookbooks.map(cookbookAsLink));
|
||||
}
|
||||
else {
|
||||
links.push({
|
||||
key: householdName,
|
||||
icon: $globals.icons.book,
|
||||
title: householdName,
|
||||
children: cookbooks.map(cookbookAsLink),
|
||||
restricted: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
links.sort((a, b) => a.title.localeCompare(b.title));
|
||||
if (auth.user.value && cookbookPreferences.value.hideOtherHouseholds) {
|
||||
return ownLinks;
|
||||
}
|
||||
else {
|
||||
return [...ownLinks, ...links];
|
||||
}
|
||||
});
|
||||
|
||||
const createLinks = computed(() => [
|
||||
{
|
||||
insertDivider: false,
|
||||
icon: $globals.icons.link,
|
||||
title: i18n.t("general.import"),
|
||||
subtitle: i18n.t("new-recipe.import-by-url"),
|
||||
to: `/g/${groupSlug.value}/r/create/url`,
|
||||
restricted: true,
|
||||
hide: false,
|
||||
},
|
||||
{
|
||||
insertDivider: false,
|
||||
icon: $globals.icons.fileImage,
|
||||
title: i18n.t("recipe.create-from-images"),
|
||||
subtitle: i18n.t("recipe.create-recipe-from-an-image"),
|
||||
to: `/g/${groupSlug.value}/r/create/image`,
|
||||
restricted: true,
|
||||
hide: !showImageImport.value,
|
||||
},
|
||||
{
|
||||
insertDivider: true,
|
||||
icon: $globals.icons.edit,
|
||||
title: i18n.t("general.create"),
|
||||
subtitle: i18n.t("new-recipe.create-manually"),
|
||||
to: `/g/${groupSlug.value}/r/create/new`,
|
||||
restricted: true,
|
||||
hide: false,
|
||||
},
|
||||
]);
|
||||
|
||||
const topLinks = computed<SideBarLink[]>(() => [
|
||||
{
|
||||
icon: $globals.icons.silverwareForkKnife,
|
||||
to: `/g/${groupSlug.value}`,
|
||||
title: i18n.t("general.recipes"),
|
||||
restricted: false,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.search,
|
||||
to: `/g/${groupSlug.value}/recipes/finder`,
|
||||
title: i18n.t("recipe-finder.recipe-finder"),
|
||||
restricted: false,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.calendarMultiselect,
|
||||
title: i18n.t("meal-plan.meal-planner"),
|
||||
to: "/household/mealplan/planner/view",
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.formatListCheck,
|
||||
title: i18n.t("shopping-list.shopping-lists"),
|
||||
to: "/shopping-lists",
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.timelineText,
|
||||
title: i18n.t("recipe.timeline"),
|
||||
to: `/g/${groupSlug.value}/recipes/timeline`,
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.book,
|
||||
to: `/g/${groupSlug.value}/cookbooks`,
|
||||
title: i18n.t("cookbook.cookbooks"),
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.organizers,
|
||||
title: i18n.t("general.organizers"),
|
||||
restricted: true,
|
||||
children: [
|
||||
{
|
||||
icon: $globals.icons.categories,
|
||||
to: `/g/${groupSlug.value}/recipes/categories`,
|
||||
title: i18n.t("sidebar.categories"),
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.tags,
|
||||
to: `/g/${groupSlug.value}/recipes/tags`,
|
||||
title: i18n.t("sidebar.tags"),
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.potSteam,
|
||||
to: `/g/${groupSlug.value}/recipes/tools`,
|
||||
title: i18n.t("tool.tools"),
|
||||
restricted: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
return {
|
||||
groupSlug,
|
||||
cookbookLinks,
|
||||
createLinks,
|
||||
topLinks,
|
||||
isOwnGroup,
|
||||
languageDialog,
|
||||
sidebar,
|
||||
};
|
||||
},
|
||||
const cookbooks = computed(() => {
|
||||
if (isOwnGroup.value) {
|
||||
return ownCookbookStore.store.value;
|
||||
}
|
||||
else if (groupSlug.value) {
|
||||
const publicStore = getPublicCookbookStore(groupSlug.value);
|
||||
return unref(publicStore.store);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const showImageImport = computed(() => $appInfo.enableOpenaiImageServices);
|
||||
|
||||
const sidebar = ref<boolean>(false);
|
||||
onMounted(() => {
|
||||
sidebar.value = display.lgAndUp.value;
|
||||
});
|
||||
|
||||
function cookbookAsLink(cookbook: ReadCookBook): SideBarLink {
|
||||
return {
|
||||
key: cookbook.slug || "",
|
||||
icon: $globals.icons.pages,
|
||||
title: cookbook.name,
|
||||
to: `/g/${groupSlug.value}/cookbooks/${cookbook.slug || ""}`,
|
||||
restricted: false,
|
||||
};
|
||||
}
|
||||
|
||||
const currentUserHouseholdId = computed(() => auth.user.value?.householdId);
|
||||
const cookbookLinks = computed<SideBarLink[]>(() => {
|
||||
if (!cookbooks.value?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sortedCookbooks = [...cookbooks.value].sort((a, b) => (a.position || 0) - (b.position || 0));
|
||||
|
||||
const ownLinks: SideBarLink[] = [];
|
||||
const links: SideBarLink[] = [];
|
||||
const cookbooksByHousehold = sortedCookbooks.reduce((acc, cookbook) => {
|
||||
const householdName = cookbook.household?.name || "";
|
||||
(acc[householdName] ||= []).push(cookbook);
|
||||
return acc;
|
||||
}, {} as Record<string, ReadCookBook[]>);
|
||||
|
||||
Object.entries(cookbooksByHousehold).forEach(([householdName, cookbooks]) => {
|
||||
if (!cookbooks.length) {
|
||||
return;
|
||||
}
|
||||
if (cookbooks[0].householdId === currentUserHouseholdId.value) {
|
||||
ownLinks.push(...cookbooks.map(cookbookAsLink));
|
||||
}
|
||||
else {
|
||||
links.push({
|
||||
key: householdName,
|
||||
icon: $globals.icons.book,
|
||||
title: householdName,
|
||||
children: cookbooks.map(cookbookAsLink),
|
||||
restricted: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
links.sort((a, b) => a.title.localeCompare(b.title));
|
||||
if (auth.user.value && cookbookPreferences.value.hideOtherHouseholds) {
|
||||
return ownLinks;
|
||||
}
|
||||
else {
|
||||
return [...ownLinks, ...links];
|
||||
}
|
||||
});
|
||||
|
||||
const createLinks = computed(() => [
|
||||
{
|
||||
insertDivider: false,
|
||||
icon: $globals.icons.link,
|
||||
title: i18n.t("general.import"),
|
||||
subtitle: i18n.t("new-recipe.import-by-url"),
|
||||
to: `/g/${groupSlug.value}/r/create/url`,
|
||||
restricted: true,
|
||||
hide: false,
|
||||
},
|
||||
{
|
||||
insertDivider: false,
|
||||
icon: $globals.icons.fileImage,
|
||||
title: i18n.t("recipe.create-from-images"),
|
||||
subtitle: i18n.t("recipe.create-recipe-from-an-image"),
|
||||
to: `/g/${groupSlug.value}/r/create/image`,
|
||||
restricted: true,
|
||||
hide: !showImageImport.value,
|
||||
},
|
||||
{
|
||||
insertDivider: true,
|
||||
icon: $globals.icons.edit,
|
||||
title: i18n.t("general.create"),
|
||||
subtitle: i18n.t("new-recipe.create-manually"),
|
||||
to: `/g/${groupSlug.value}/r/create/new`,
|
||||
restricted: true,
|
||||
hide: false,
|
||||
},
|
||||
]);
|
||||
|
||||
const topLinks = computed<SideBarLink[]>(() => [
|
||||
{
|
||||
icon: $globals.icons.silverwareForkKnife,
|
||||
to: `/g/${groupSlug.value}`,
|
||||
title: i18n.t("general.recipes"),
|
||||
restricted: false,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.search,
|
||||
to: `/g/${groupSlug.value}/recipes/finder`,
|
||||
title: i18n.t("recipe-finder.recipe-finder"),
|
||||
restricted: false,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.calendarMultiselect,
|
||||
title: i18n.t("meal-plan.meal-planner"),
|
||||
to: "/household/mealplan/planner/view",
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.formatListCheck,
|
||||
title: i18n.t("shopping-list.shopping-lists"),
|
||||
to: "/shopping-lists",
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.timelineText,
|
||||
title: i18n.t("recipe.timeline"),
|
||||
to: `/g/${groupSlug.value}/recipes/timeline`,
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.book,
|
||||
to: `/g/${groupSlug.value}/cookbooks`,
|
||||
title: i18n.t("cookbook.cookbooks"),
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.organizers,
|
||||
title: i18n.t("general.organizers"),
|
||||
restricted: true,
|
||||
children: [
|
||||
{
|
||||
icon: $globals.icons.categories,
|
||||
to: `/g/${groupSlug.value}/recipes/categories`,
|
||||
title: i18n.t("sidebar.categories"),
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.tags,
|
||||
to: `/g/${groupSlug.value}/recipes/tags`,
|
||||
title: i18n.t("sidebar.tags"),
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.potSteam,
|
||||
to: `/g/${groupSlug.value}/recipes/tools`,
|
||||
title: i18n.t("tool.tools"),
|
||||
restricted: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -29,11 +29,3 @@
|
||||
</v-row>
|
||||
</v-footer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
class="d-print-none"
|
||||
>
|
||||
<slot />
|
||||
<router-link :to="routerLink">
|
||||
<RouterLink :to="routerLink">
|
||||
<v-btn
|
||||
icon
|
||||
color="white"
|
||||
>
|
||||
<v-icon size="40"> {{ $globals.icons.primary }} </v-icon>
|
||||
</v-btn>
|
||||
</router-link>
|
||||
</RouterLink>
|
||||
|
||||
<div
|
||||
btn
|
||||
@@ -84,67 +84,53 @@
|
||||
</v-app-bar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import RecipeDialogSearch from "~/components/Domain/Recipe/RecipeDialogSearch.vue";
|
||||
import type RecipeDialogSearch from "~/components/Domain/Recipe/RecipeDialogSearch.vue";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeDialogSearch },
|
||||
props: {
|
||||
menu: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const auth = useMealieAuth();
|
||||
const { loggedIn } = useLoggedInState();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||
const { xs, smAndUp } = useDisplay();
|
||||
|
||||
const routerLink = computed(() => groupSlug.value ? `/g/${groupSlug.value}` : "/");
|
||||
const domSearchDialog = ref<InstanceType<typeof RecipeDialogSearch> | null>(null);
|
||||
|
||||
function activateSearch() {
|
||||
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);
|
||||
});
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await auth.signOut("/login?direct=1");
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
activateSearch,
|
||||
domSearchDialog,
|
||||
routerLink,
|
||||
loggedIn,
|
||||
logout,
|
||||
xs, smAndUp,
|
||||
};
|
||||
defineProps({
|
||||
menu: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
const auth = useMealieAuth();
|
||||
const { loggedIn } = useLoggedInState();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||
const { xs, smAndUp } = useDisplay();
|
||||
|
||||
const routerLink = computed(() => groupSlug.value ? `/g/${groupSlug.value}` : "/");
|
||||
const domSearchDialog = ref<InstanceType<typeof RecipeDialogSearch> | null>(null);
|
||||
|
||||
function activateSearch() {
|
||||
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);
|
||||
});
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await auth.signOut("/login?direct=1");
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<v-navigation-drawer v-model="showDrawer" class="d-flex flex-column d-print-none position-fixed" touchless>
|
||||
<LanguageDialog v-model="languageDialog" />
|
||||
<v-navigation-drawer v-model="modelValue" class="d-flex flex-column d-print-none position-fixed" touchless>
|
||||
<LanguageDialog v-model="state.languageDialog" />
|
||||
<!-- User Profile -->
|
||||
<template v-if="loggedIn">
|
||||
<template v-if="loggedIn && sessionUser">
|
||||
<v-list-item lines="two" :to="userProfileLink" exact>
|
||||
<div class="d-flex align-center ga-2">
|
||||
<UserAvatar list :user-id="sessionUser.id" :tooltip="false" />
|
||||
@@ -29,20 +29,20 @@
|
||||
|
||||
<!-- Primary Links -->
|
||||
<template v-if="topLink">
|
||||
<v-list v-model:selected="secondarySelected" nav density="comfortable" color="primary">
|
||||
<v-list v-model:selected="state.secondarySelected" nav density="comfortable" color="primary">
|
||||
<template v-for="nav in topLink">
|
||||
<div v-if="!nav.restricted || isOwnGroup" :key="nav.key || nav.title">
|
||||
<!-- Multi Items -->
|
||||
<v-list-group
|
||||
v-if="nav.children"
|
||||
:key="(nav.key || nav.title) + 'multi-item'"
|
||||
v-model="dropDowns[nav.title]"
|
||||
v-model="state.dropDowns[nav.title]"
|
||||
color="primary"
|
||||
:prepend-icon="nav.icon"
|
||||
:fluid="true"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-list-item v-bind="props" :prepend-icon="nav.icon" :title="nav.title" />
|
||||
<template #activator="{ props: hoverProps }">
|
||||
<v-list-item v-bind="hoverProps" :prepend-icon="nav.icon" :title="nav.title" />
|
||||
</template>
|
||||
|
||||
<v-list-item
|
||||
@@ -75,20 +75,20 @@
|
||||
<!-- Secondary Links -->
|
||||
<template v-if="secondaryLinks.length > 0">
|
||||
<v-divider class="mt-2" />
|
||||
<v-list v-model:selected="secondarySelected" nav density="compact" exact>
|
||||
<v-list v-model:selected="state.secondarySelected" nav density="compact" exact>
|
||||
<template v-for="nav in secondaryLinks">
|
||||
<div v-if="!nav.restricted || isOwnGroup" :key="nav.key || nav.title">
|
||||
<!-- Multi Items -->
|
||||
<v-list-group
|
||||
v-if="nav.children"
|
||||
:key="(nav.key || nav.title) + 'multi-item'"
|
||||
v-model="dropDowns[nav.title]"
|
||||
v-model="state.dropDowns[nav.title]"
|
||||
color="primary"
|
||||
:prepend-icon="nav.icon"
|
||||
fluid
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-list-item v-bind="props" :prepend-icon="nav.icon" :title="nav.title" />
|
||||
<template #activator="{ props: hoverProps }">
|
||||
<v-list-item v-bind="hoverProps" :prepend-icon="nav.icon" :title="nav.title" />
|
||||
</template>
|
||||
|
||||
<v-list-item
|
||||
@@ -116,13 +116,13 @@
|
||||
|
||||
<!-- Bottom Navigation Links -->
|
||||
<template #append>
|
||||
<v-list v-model:selected="bottomSelected" nav density="comfortable">
|
||||
<v-list v-model:selected="state.bottomSelected" nav density="comfortable">
|
||||
<v-menu location="end bottom" :offset="15">
|
||||
<template #activator="{ props }">
|
||||
<v-list-item v-bind="props" :prepend-icon="$globals.icons.cog" :title="$t('general.settings')" />
|
||||
<template #activator="{ props: hoverProps }">
|
||||
<v-list-item v-bind="hoverProps" :prepend-icon="$globals.icons.cog" :title="$t('general.settings')" />
|
||||
</template>
|
||||
<v-list density="comfortable" color="primary">
|
||||
<v-list-item :prepend-icon="$globals.icons.translate" :title="$t('sidebar.language')" @click="languageDialog=true" />
|
||||
<v-list-item :prepend-icon="$globals.icons.translate" :title="$t('sidebar.language')" @click="state.languageDialog=true" />
|
||||
<v-list-item :prepend-icon="$vuetify.theme.current.dark ? $globals.icons.weatherSunny : $globals.icons.weatherNight" :title="$vuetify.theme.current.dark ? $t('settings.theme.light-mode') : $t('settings.theme.dark-mode')" @click="toggleDark" />
|
||||
<v-divider v-if="loggedIn" class="my-2" />
|
||||
<v-list-item v-if="loggedIn" :prepend-icon="$globals.icons.cog" :title="$t('profile.user-settings')" to="/user/profile" />
|
||||
@@ -136,92 +136,63 @@
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import type { SidebarLinks } from "~/types/application-types";
|
||||
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
||||
import { useToggleDarkMode } from "~/composables/use-utils";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
UserAvatar,
|
||||
const props = defineProps({
|
||||
user: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
user: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
topLink: {
|
||||
type: Array as () => SidebarLinks,
|
||||
required: true,
|
||||
},
|
||||
secondaryLinks: {
|
||||
type: Array as () => SidebarLinks,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
topLink: {
|
||||
type: Array as () => SidebarLinks,
|
||||
required: true,
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const auth = useMealieAuth();
|
||||
const { loggedIn, isOwnGroup } = useLoggedInState();
|
||||
const isAdmin = computed(() => auth.user.value?.admin);
|
||||
const canManage = computed(() => auth.user.value?.canManage);
|
||||
|
||||
const userFavoritesLink = computed(() => auth.user.value ? `/user/${auth.user.value.id}/favorites` : undefined);
|
||||
const userProfileLink = computed(() => auth.user.value ? "/user/profile" : undefined);
|
||||
|
||||
const toggleDark = useToggleDarkMode();
|
||||
|
||||
const state = reactive({
|
||||
dropDowns: {} as Record<string, boolean>,
|
||||
topSelected: null as string[] | null,
|
||||
secondarySelected: null as string[] | null,
|
||||
bottomSelected: null as string[] | null,
|
||||
hasOpenedBefore: false as boolean,
|
||||
languageDialog: false as boolean,
|
||||
});
|
||||
// model to control the drawer
|
||||
const showDrawer = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => context.emit("update:modelValue", value),
|
||||
});
|
||||
|
||||
const allLinks = computed(() => [...props.topLink, ...(props.secondaryLinks || [])]);
|
||||
function initDropdowns() {
|
||||
allLinks.value.forEach((link) => {
|
||||
state.dropDowns[link.title] = link.childrenStartExpanded || false;
|
||||
});
|
||||
}
|
||||
watch(
|
||||
() => allLinks,
|
||||
() => {
|
||||
initDropdowns();
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
userFavoritesLink,
|
||||
userProfileLink,
|
||||
showDrawer,
|
||||
loggedIn,
|
||||
isAdmin,
|
||||
canManage,
|
||||
isOwnGroup,
|
||||
sessionUser: auth.user,
|
||||
toggleDark,
|
||||
};
|
||||
secondaryLinks: {
|
||||
type: Array as () => SidebarLinks,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const modelValue = defineModel<boolean>({ default: false });
|
||||
|
||||
const auth = useMealieAuth();
|
||||
const sessionUser = computed(() => auth.user.value);
|
||||
const { loggedIn, isOwnGroup } = useLoggedInState();
|
||||
const isAdmin = computed(() => auth.user.value?.admin);
|
||||
const canManage = computed(() => auth.user.value?.canManage);
|
||||
|
||||
const userFavoritesLink = computed(() => auth.user.value ? `/user/${auth.user.value.id}/favorites` : undefined);
|
||||
const userProfileLink = computed(() => auth.user.value ? "/user/profile" : undefined);
|
||||
|
||||
const toggleDark = useToggleDarkMode();
|
||||
|
||||
const state = reactive({
|
||||
dropDowns: {} as Record<string, boolean>,
|
||||
secondarySelected: null as string[] | null,
|
||||
bottomSelected: null as string[] | null,
|
||||
languageDialog: false as boolean,
|
||||
});
|
||||
|
||||
const allLinks = computed(() => [...props.topLink, ...(props.secondaryLinks || [])]);
|
||||
function initDropdowns() {
|
||||
allLinks.value.forEach((link) => {
|
||||
state.dropDowns[link.title] = link.childrenStartExpanded || false;
|
||||
});
|
||||
}
|
||||
watch(
|
||||
() => allLinks,
|
||||
() => {
|
||||
initDropdowns();
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -49,27 +49,21 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useNuxtApp } from "#app";
|
||||
import { toastAlert, toastLoading } from "~/composables/use-toast";
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const { $globals } = useNuxtApp();
|
||||
const icon = computed(() => {
|
||||
switch (toastAlert.color) {
|
||||
case "error":
|
||||
return $globals.icons.alertOutline;
|
||||
case "success":
|
||||
return $globals.icons.checkBold;
|
||||
case "info":
|
||||
return $globals.icons.informationOutline;
|
||||
default:
|
||||
return $globals.icons.alertOutline;
|
||||
}
|
||||
});
|
||||
|
||||
return { icon, toastAlert, toastLoading };
|
||||
},
|
||||
};
|
||||
const { $globals } = useNuxtApp();
|
||||
const icon = computed(() => {
|
||||
switch (toastAlert.color) {
|
||||
case "error":
|
||||
return $globals.icons.alertOutline;
|
||||
case "success":
|
||||
return $globals.icons.checkBold;
|
||||
case "info":
|
||||
return $globals.icons.informationOutline;
|
||||
default:
|
||||
return $globals.icons.alertOutline;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
<template>
|
||||
<div scoped-slot />
|
||||
<slot v-if="advanced" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Renderless component that only renders if the user is logged in.
|
||||
* and has advanced options toggled.
|
||||
*/
|
||||
export default defineNuxtComponent({
|
||||
setup(_, ctx) {
|
||||
const auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
|
||||
const r = auth.user.value?.advanced || false;
|
||||
|
||||
return () => {
|
||||
return r ? ctx.slots.default?.() : null;
|
||||
};
|
||||
},
|
||||
});
|
||||
const advanced = auth.user.value?.advanced || false;
|
||||
</script>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
close-delay="500"
|
||||
transition="slide-y-transition"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: hoverProps }">
|
||||
<v-btn
|
||||
variant="flat"
|
||||
:icon="icon"
|
||||
@@ -16,7 +16,7 @@
|
||||
retain-focus-on-click
|
||||
:class="btnClass"
|
||||
:disabled="copyText !== '' ? false : true"
|
||||
v-bind="props"
|
||||
v-bind="hoverProps"
|
||||
@click="textToClipboard()"
|
||||
>
|
||||
<v-icon>{{ $globals.icons.contentCopy }}</v-icon>
|
||||
@@ -33,66 +33,53 @@
|
||||
</v-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
copyText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
icon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
btnClass: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
const props = defineProps({
|
||||
copyText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
setup(props) {
|
||||
const { copy, copied, isSupported } = useClipboard();
|
||||
const show = ref(false);
|
||||
const copyToolTip = ref<VTooltip | null>(null);
|
||||
const copiedSuccess = ref<boolean | null>(null);
|
||||
|
||||
async function textToClipboard() {
|
||||
if (isSupported.value) {
|
||||
await copy(props.copyText);
|
||||
if (copied.value) {
|
||||
copiedSuccess.value = true;
|
||||
console.info(`Copied\n${props.copyText}`);
|
||||
}
|
||||
else {
|
||||
copiedSuccess.value = false;
|
||||
console.error("Copy failed: ", copied.value);
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.warn("Clipboard is currently not supported by your browser. Ensure you're on a secure (https) site.");
|
||||
}
|
||||
|
||||
show.value = true;
|
||||
setTimeout(() => {
|
||||
show.value = false;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
return {
|
||||
show,
|
||||
copyToolTip,
|
||||
textToClipboard,
|
||||
copied,
|
||||
isSupported,
|
||||
copiedSuccess,
|
||||
};
|
||||
color: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
icon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
btnClass: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const { copy, copied, isSupported } = useClipboard();
|
||||
const show = ref(false);
|
||||
const copiedSuccess = ref<boolean | null>(null);
|
||||
|
||||
async function textToClipboard() {
|
||||
if (isSupported.value) {
|
||||
await copy(props.copyText);
|
||||
if (copied.value) {
|
||||
copiedSuccess.value = true;
|
||||
console.info(`Copied\n${props.copyText}`);
|
||||
}
|
||||
else {
|
||||
copiedSuccess.value = false;
|
||||
console.error("Copy failed: ", copied.value);
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.warn("Clipboard is currently not supported by your browser. Ensure you're on a secure (https) site.");
|
||||
}
|
||||
|
||||
show.value = true;
|
||||
setTimeout(() => {
|
||||
show.value = false;
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-form ref="files">
|
||||
<v-form ref="form">
|
||||
<input
|
||||
ref="uploader"
|
||||
class="d-none"
|
||||
@@ -26,144 +26,129 @@
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
const UPLOAD_EVENT = "uploaded";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
post: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
fileName: {
|
||||
type: String,
|
||||
default: "archive",
|
||||
},
|
||||
textBtn: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "info",
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
const props = defineProps({
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
setup(props, context) {
|
||||
const files = ref<File[]>([]);
|
||||
const uploader = ref<HTMLInputElement | null>(null);
|
||||
const isSelecting = ref(false);
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const effIcon = props.icon ? props.icon : $globals.icons.upload;
|
||||
|
||||
const defaultText = i18n.t("general.upload");
|
||||
|
||||
const api = useUserApi();
|
||||
async function upload() {
|
||||
if (files.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSelecting.value = true;
|
||||
|
||||
if (!props.post) {
|
||||
// NOTE: To preserve behaviour for other parents of this component,
|
||||
// we emit a single File if !props.multiple.
|
||||
context.emit(UPLOAD_EVENT, props.multiple ? files.value : files.value[0]);
|
||||
isSelecting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// WARN: My change is only for !props.post.
|
||||
// I have not added support for multiple files in the API.
|
||||
// Existing call-sites never passed the `multiple` prop,
|
||||
// so this case will only be hit if the prop is set to true.
|
||||
if (props.multiple && files.value.length > 1) {
|
||||
console.warn("Multiple file uploads are not supported by the API.");
|
||||
return;
|
||||
}
|
||||
|
||||
const file = files.value[0];
|
||||
const formData = new FormData();
|
||||
formData.append(props.fileName, file);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function onFileChanged(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
if (target.files !== null && target.files.length > 0) {
|
||||
files.value = Array.from(target.files);
|
||||
upload();
|
||||
}
|
||||
}
|
||||
|
||||
function onButtonClick() {
|
||||
isSelecting.value = true;
|
||||
window.addEventListener(
|
||||
"focus",
|
||||
() => {
|
||||
isSelecting.value = false;
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
uploader.value?.click();
|
||||
}
|
||||
|
||||
return {
|
||||
files,
|
||||
uploader,
|
||||
isSelecting,
|
||||
effIcon,
|
||||
defaultText,
|
||||
onFileChanged,
|
||||
onButtonClick,
|
||||
};
|
||||
post: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
fileName: {
|
||||
type: String,
|
||||
default: "archive",
|
||||
},
|
||||
textBtn: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "info",
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "uploaded", payload: File | File[] | unknown | null): void;
|
||||
}>();
|
||||
|
||||
const selectedFiles = ref<File[]>([]);
|
||||
const uploader = ref<HTMLInputElement | null>(null);
|
||||
const isSelecting = ref(false);
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const effIcon = props.icon ? props.icon : $globals.icons.upload;
|
||||
|
||||
const defaultText = i18n.t("general.upload");
|
||||
|
||||
const api = useUserApi();
|
||||
async function upload() {
|
||||
if (selectedFiles.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSelecting.value = true;
|
||||
|
||||
if (!props.post) {
|
||||
emit(UPLOAD_EVENT, props.multiple ? selectedFiles.value : selectedFiles.value[0]);
|
||||
isSelecting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.multiple && selectedFiles.value.length > 1) {
|
||||
console.warn("Multiple file uploads are not supported by the API.");
|
||||
return;
|
||||
}
|
||||
|
||||
const file = selectedFiles.value[0];
|
||||
const formData = new FormData();
|
||||
formData.append(props.fileName, file);
|
||||
|
||||
try {
|
||||
const response = await api.upload.file(props.url, formData);
|
||||
if (response) {
|
||||
emit(UPLOAD_EVENT, response);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
emit(UPLOAD_EVENT, null);
|
||||
}
|
||||
|
||||
isSelecting.value = false;
|
||||
}
|
||||
|
||||
function onFileChanged(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
if (target.files !== null && target.files.length > 0) {
|
||||
selectedFiles.value = Array.from(target.files);
|
||||
upload();
|
||||
}
|
||||
}
|
||||
|
||||
function onButtonClick() {
|
||||
isSelecting.value = true;
|
||||
window.addEventListener(
|
||||
"focus",
|
||||
() => {
|
||||
isSelecting.value = false;
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
uploader.value?.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
||||
@@ -39,71 +39,63 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
tiny: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
medium: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
large: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
waitingText: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
setup(props) {
|
||||
const size = computed(() => {
|
||||
if (props.tiny) {
|
||||
return {
|
||||
width: 2,
|
||||
icon: 0,
|
||||
size: 25,
|
||||
};
|
||||
}
|
||||
if (props.small) {
|
||||
return {
|
||||
width: 2,
|
||||
icon: 30,
|
||||
size: 50,
|
||||
};
|
||||
}
|
||||
else if (props.large) {
|
||||
return {
|
||||
width: 4,
|
||||
icon: 120,
|
||||
size: 200,
|
||||
};
|
||||
}
|
||||
return {
|
||||
width: 3,
|
||||
icon: 75,
|
||||
size: 125,
|
||||
};
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const waitingTextCalculated = props.waitingText == null ? i18n.t("general.loading-recipes") : props.waitingText;
|
||||
|
||||
return {
|
||||
size,
|
||||
waitingTextCalculated,
|
||||
};
|
||||
tiny: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
medium: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
large: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
waitingText: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const size = computed(() => {
|
||||
if (props.tiny) {
|
||||
return {
|
||||
width: 2,
|
||||
icon: 0,
|
||||
size: 25,
|
||||
};
|
||||
}
|
||||
if (props.small) {
|
||||
return {
|
||||
width: 2,
|
||||
icon: 30,
|
||||
size: 50,
|
||||
};
|
||||
}
|
||||
else if (props.large) {
|
||||
return {
|
||||
width: 4,
|
||||
icon: 120,
|
||||
size: 200,
|
||||
};
|
||||
}
|
||||
return {
|
||||
width: 3,
|
||||
icon: 75,
|
||||
size: 125,
|
||||
};
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const waitingTextCalculated = props.waitingText == null ? i18n.t("general.loading-recipes") : props.waitingText;
|
||||
</script>
|
||||
|
||||
@@ -18,13 +18,11 @@
|
||||
</v-toolbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
back: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
back: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -15,14 +15,12 @@
|
||||
</BannerWarning>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: {
|
||||
issue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
issue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<v-alert
|
||||
border="start"
|
||||
border-color
|
||||
variant="tonal"
|
||||
type="warning"
|
||||
elevation="2"
|
||||
@@ -20,19 +19,17 @@
|
||||
</v-alert>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
};
|
||||
description: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -32,191 +32,168 @@
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
name: "BaseButton",
|
||||
props: {
|
||||
// Types
|
||||
cancel: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
create: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
update: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
edit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
save: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
delete: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// Download
|
||||
download: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
downloadUrl: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
// Property
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// Styles
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
xSmall: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
secondary: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
minor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
iconRight: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
const props = defineProps({
|
||||
cancel: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
setup(props) {
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const buttonOptions = {
|
||||
create: {
|
||||
text: i18n.t("general.create"),
|
||||
icon: $globals.icons.createAlt,
|
||||
color: "success",
|
||||
},
|
||||
update: {
|
||||
text: i18n.t("general.update"),
|
||||
icon: $globals.icons.edit,
|
||||
color: "success",
|
||||
},
|
||||
save: {
|
||||
text: i18n.t("general.save"),
|
||||
icon: $globals.icons.save,
|
||||
color: "success",
|
||||
},
|
||||
edit: {
|
||||
text: i18n.t("general.edit"),
|
||||
icon: $globals.icons.edit,
|
||||
color: "info",
|
||||
},
|
||||
delete: {
|
||||
text: i18n.t("general.delete"),
|
||||
icon: $globals.icons.delete,
|
||||
color: "error",
|
||||
},
|
||||
cancel: {
|
||||
text: i18n.t("general.cancel"),
|
||||
icon: $globals.icons.close,
|
||||
color: "grey",
|
||||
},
|
||||
download: {
|
||||
text: i18n.t("general.download"),
|
||||
icon: $globals.icons.download,
|
||||
color: "info",
|
||||
},
|
||||
};
|
||||
|
||||
const btnAttrs = computed(() => {
|
||||
if (props.delete) {
|
||||
return buttonOptions.delete;
|
||||
}
|
||||
else if (props.update) {
|
||||
return buttonOptions.update;
|
||||
}
|
||||
else if (props.edit) {
|
||||
return buttonOptions.edit;
|
||||
}
|
||||
else if (props.cancel) {
|
||||
return buttonOptions.cancel;
|
||||
}
|
||||
else if (props.save) {
|
||||
return buttonOptions.save;
|
||||
}
|
||||
else if (props.download) {
|
||||
return buttonOptions.download;
|
||||
}
|
||||
return buttonOptions.create;
|
||||
});
|
||||
|
||||
const buttonStyles = {
|
||||
defaults: {
|
||||
text: false,
|
||||
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 buttonStyles.defaults;
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
function downloadFile() {
|
||||
api.utils.download(props.downloadUrl);
|
||||
}
|
||||
|
||||
return {
|
||||
btnAttrs,
|
||||
btnStyle,
|
||||
downloadFile,
|
||||
};
|
||||
create: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
update: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
edit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
save: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
delete: {
|
||||
type: Boolean,
|
||||
default: false },
|
||||
download: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
downloadUrl: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
xSmall: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
secondary: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
minor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
iconRight: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const buttonOptions = {
|
||||
create: {
|
||||
text: i18n.t("general.create"),
|
||||
icon: $globals.icons.createAlt,
|
||||
color: "success",
|
||||
},
|
||||
update: {
|
||||
text: i18n.t("general.update"),
|
||||
icon: $globals.icons.edit,
|
||||
color: "success",
|
||||
},
|
||||
save: {
|
||||
text: i18n.t("general.save"),
|
||||
icon: $globals.icons.save,
|
||||
color: "success",
|
||||
},
|
||||
edit: {
|
||||
text: i18n.t("general.edit"),
|
||||
icon: $globals.icons.edit,
|
||||
color: "info",
|
||||
},
|
||||
delete: {
|
||||
text: i18n.t("general.delete"),
|
||||
icon: $globals.icons.delete,
|
||||
color: "error",
|
||||
},
|
||||
cancel: {
|
||||
text: i18n.t("general.cancel"),
|
||||
icon: $globals.icons.close,
|
||||
color: "grey",
|
||||
},
|
||||
download: {
|
||||
text: i18n.t("general.download"),
|
||||
icon: $globals.icons.download,
|
||||
color: "info",
|
||||
},
|
||||
};
|
||||
|
||||
const btnAttrs = computed(() => {
|
||||
if (props.delete) {
|
||||
return buttonOptions.delete;
|
||||
}
|
||||
if (props.update) {
|
||||
return buttonOptions.update;
|
||||
}
|
||||
if (props.edit) {
|
||||
return buttonOptions.edit;
|
||||
}
|
||||
if (props.cancel) {
|
||||
return buttonOptions.cancel;
|
||||
}
|
||||
if (props.save) {
|
||||
return buttonOptions.save;
|
||||
}
|
||||
if (props.download) {
|
||||
return buttonOptions.download;
|
||||
}
|
||||
return buttonOptions.create;
|
||||
});
|
||||
|
||||
const buttonStyles = {
|
||||
defaults: { text: false, outlined: false },
|
||||
secondary: { text: false, outlined: true },
|
||||
minor: { text: true, outlined: false },
|
||||
};
|
||||
|
||||
const btnStyle = computed(() => {
|
||||
if (props.secondary) {
|
||||
return buttonStyles.secondary;
|
||||
}
|
||||
if (props.minor || props.cancel) {
|
||||
return buttonStyles.minor;
|
||||
}
|
||||
return buttonStyles.defaults;
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
function downloadFile() {
|
||||
api.utils.download(props.downloadUrl);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
start
|
||||
:style="stretch ? 'width: 100%;' : ''"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: hoverProps }">
|
||||
<v-btn
|
||||
tile
|
||||
:large="large"
|
||||
icon
|
||||
variant="plain"
|
||||
v-bind="props"
|
||||
v-bind="hoverProps"
|
||||
>
|
||||
<v-icon>
|
||||
{{ btn.icon }}
|
||||
@@ -51,7 +51,7 @@
|
||||
location="bottom"
|
||||
content-class="text-caption"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-btn
|
||||
tile
|
||||
icon
|
||||
@@ -60,7 +60,7 @@
|
||||
:disabled="btn.disabled"
|
||||
:style="stretch ? `width: ${maxButtonWidth};` : ''"
|
||||
variant="plain"
|
||||
v-bind="props"
|
||||
v-bind="tooltipProps"
|
||||
@click="$emit(btn.event)"
|
||||
>
|
||||
<v-icon> {{ btn.icon }} </v-icon>
|
||||
@@ -72,7 +72,7 @@
|
||||
</v-item-group>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
export interface ButtonOption {
|
||||
icon?: string;
|
||||
color?: string;
|
||||
@@ -83,26 +83,20 @@ export interface ButtonOption {
|
||||
divider?: boolean;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
buttons: {
|
||||
type: Array as () => ButtonOption[],
|
||||
required: true,
|
||||
},
|
||||
large: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
stretch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
const props = defineProps({
|
||||
buttons: {
|
||||
type: Array as () => ButtonOption[],
|
||||
required: true,
|
||||
},
|
||||
setup(props) {
|
||||
const maxButtonWidth = computed(() => `${100 / props.buttons.length}%`);
|
||||
return {
|
||||
maxButtonWidth,
|
||||
};
|
||||
large: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
stretch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const maxButtonWidth = computed(() => `${100 / props.buttons.length}%`);
|
||||
</script>
|
||||
|
||||
@@ -29,21 +29,19 @@
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
section: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
section: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -6,21 +6,19 @@
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
width: {
|
||||
type: String,
|
||||
default: "100px",
|
||||
},
|
||||
thickness: {
|
||||
type: String,
|
||||
default: "2px",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "accent",
|
||||
},
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
width: {
|
||||
type: String,
|
||||
default: "100px",
|
||||
},
|
||||
thickness: {
|
||||
type: String,
|
||||
default: "2px",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "accent",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<v-menu offset-y>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: hoverProps }">
|
||||
<v-btn
|
||||
color="primary"
|
||||
v-bind="{ ...props, ...$attrs }"
|
||||
v-bind="{ ...hoverProps, ...$attrs }"
|
||||
:class="btnClass"
|
||||
:disabled="disabled"
|
||||
>
|
||||
@@ -105,7 +105,7 @@
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
const MODES = {
|
||||
model: "model",
|
||||
link: "link",
|
||||
@@ -124,67 +124,57 @@ export interface MenuItem {
|
||||
hide?: boolean;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
mode: {
|
||||
type: String as () => modes,
|
||||
default: "model",
|
||||
},
|
||||
items: {
|
||||
type: Array as () => MenuItem[],
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
btnClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
btnText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: function () {
|
||||
return useI18n().t("general.actions");
|
||||
},
|
||||
},
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String as () => modes,
|
||||
default: "model",
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const activeObj = ref<MenuItem>({
|
||||
text: "DEFAULT",
|
||||
value: "",
|
||||
});
|
||||
|
||||
let startIndex = 0;
|
||||
props.items.forEach((item, index) => {
|
||||
if (item.value === props.modelValue) {
|
||||
startIndex = index;
|
||||
|
||||
activeObj.value = item;
|
||||
}
|
||||
});
|
||||
const itemGroup = ref(startIndex);
|
||||
|
||||
function setValue(v: MenuItem) {
|
||||
context.emit("update:modelValue", v.value);
|
||||
activeObj.value = v;
|
||||
}
|
||||
|
||||
return {
|
||||
MODES,
|
||||
activeObj,
|
||||
itemGroup,
|
||||
setValue,
|
||||
};
|
||||
items: {
|
||||
type: Array as () => MenuItem[],
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
btnClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
btnText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: function () {
|
||||
return useI18n().t("general.actions");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const modelValue = defineModel({
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
});
|
||||
|
||||
const activeObj = ref<MenuItem>({
|
||||
text: "DEFAULT",
|
||||
value: "",
|
||||
});
|
||||
|
||||
let startIndex = 0;
|
||||
props.items.forEach((item, index) => {
|
||||
if (item.value === modelValue.value) {
|
||||
startIndex = index;
|
||||
|
||||
activeObj.value = item;
|
||||
}
|
||||
});
|
||||
const itemGroup = ref(startIndex);
|
||||
|
||||
function setValue(v: MenuItem) {
|
||||
modelValue.value = v.value || "";
|
||||
activeObj.value = v;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -22,13 +22,11 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
divider: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
divider: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
<template>
|
||||
<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
|
||||
:color="color"
|
||||
:max-height="icon ? 90 : undefined"
|
||||
:width="icon ? 'auto' : '100%'"
|
||||
elevation="6"
|
||||
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-sheet>
|
||||
</slot>
|
||||
|
||||
<div
|
||||
v-if="$slots['after-heading']"
|
||||
class="ml-auto"
|
||||
>
|
||||
<slot name="after-heading" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
|
||||
<template v-if="$slots.actions">
|
||||
<v-divider class="mt-2" />
|
||||
|
||||
<v-card-actions class="pb-0">
|
||||
<slot name="actions" />
|
||||
</v-card-actions>
|
||||
</template>
|
||||
|
||||
<template v-if="$slots.bottom">
|
||||
<v-divider
|
||||
v-if="!$slots.actions"
|
||||
class="mt-2"
|
||||
/>
|
||||
|
||||
<div class="pb-0">
|
||||
<slot name="bottom" />
|
||||
</div>
|
||||
</template>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
name: "MaterialCard",
|
||||
|
||||
props: {
|
||||
avatar: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "primary",
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
image: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const display = useDisplay();
|
||||
const hasHeading = computed(() => false);
|
||||
const hasAltHeading = computed(() => false);
|
||||
const classes = computed(() => {
|
||||
return {
|
||||
"v-card--material--has-heading": hasHeading,
|
||||
"mt-3": display.name.value === "xs" || display.name.value === "sm",
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
hasHeading,
|
||||
hasAltHeading,
|
||||
classes,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.v-card--material
|
||||
&__avatar
|
||||
position: relative
|
||||
top: -64px
|
||||
margin-bottom: -32px
|
||||
|
||||
&__heading
|
||||
position: relative
|
||||
top: -40px
|
||||
transition: .3s ease
|
||||
z-index: 1
|
||||
</style>
|
||||
@@ -16,21 +16,19 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
to: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -40,27 +40,25 @@
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuItem } from "~/composables/use-context-presents";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
items: {
|
||||
type: Array as () => ContextMenuItem[],
|
||||
required: true,
|
||||
},
|
||||
menuTop: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fab: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "grey-darken-2",
|
||||
},
|
||||
defineProps({
|
||||
items: {
|
||||
type: Array as () => ContextMenuItem[],
|
||||
required: true,
|
||||
},
|
||||
menuTop: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fab: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "grey-darken-2",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -4,20 +4,13 @@
|
||||
</pre>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const prettyJson = JSON.stringify(props.data, null, 2);
|
||||
|
||||
return {
|
||||
prettyJson,
|
||||
};
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const prettyJson = JSON.stringify(props.data, null, 2);
|
||||
</script>
|
||||
|
||||
@@ -15,21 +15,16 @@
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
link: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const href = computed(() => {
|
||||
// TODO: dynamically set docs link based off env
|
||||
return `https://nightly.mealie.io${props.link}`;
|
||||
});
|
||||
|
||||
return { href };
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
link: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const href = computed(() => {
|
||||
// TODO: dynamically set docs link based off env
|
||||
return `https://docs.mealie.io${props.link}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -19,25 +19,27 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useDropZone } from "@vueuse/core";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
emits: ["drop"],
|
||||
setup(_, context) {
|
||||
const el = ref<HTMLDivElement>();
|
||||
|
||||
function onDrop(files: File[] | null) {
|
||||
if (files) {
|
||||
context.emit("drop", files);
|
||||
}
|
||||
}
|
||||
|
||||
const { isOverDropZone } = useDropZone(el, files => onDrop(files));
|
||||
|
||||
return { el, isOverDropZone };
|
||||
defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["drop"]);
|
||||
|
||||
const el = ref<HTMLDivElement>();
|
||||
|
||||
function onDrop(files: File[] | null) {
|
||||
if (files) {
|
||||
emit("drop", files);
|
||||
}
|
||||
}
|
||||
|
||||
const { isOverDropZone } = useDropZone(el, files => onDrop(files));
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
|
||||
@@ -29,17 +29,15 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
right: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
right: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
color="success"
|
||||
:icon="$globals.icons.save"
|
||||
:disabled="submitted"
|
||||
@click="() => save()"
|
||||
@click="save"
|
||||
/>
|
||||
<v-menu offset-y :close-on-content-click="false" location="bottom center">
|
||||
<template #activator="{ props }">
|
||||
<v-btn color="info" v-bind="props" :icon="$globals.icons.edit" :disabled="submitted" />
|
||||
<template #activator="{ props: slotProps }">
|
||||
<v-btn color="info" v-bind="slotProps" :icon="$globals.icons.edit" :disabled="submitted" />
|
||||
</template>
|
||||
<v-list class="mt-1">
|
||||
<template v-for="(row, keyRow) in controls" :key="keyRow">
|
||||
@@ -55,117 +55,98 @@
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { Cropper } from "vue-advanced-cropper";
|
||||
import "vue-advanced-cropper/dist/style.css";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { Cropper },
|
||||
props: {
|
||||
img: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
cropperHeight: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
cropperWidth: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
submitted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
defineProps({
|
||||
img: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emits: ["save", "delete"],
|
||||
setup(_, context) {
|
||||
const cropper = ref<any>();
|
||||
const changed = ref(0);
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
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),
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
function flip(hortizontal: boolean, vertical?: boolean) {
|
||||
if (!cropper.value) {
|
||||
return;
|
||||
}
|
||||
cropper.value.flip(hortizontal, vertical);
|
||||
changed.value = changed.value + 1;
|
||||
}
|
||||
|
||||
function rotate(angle: number) {
|
||||
if (!cropper.value) {
|
||||
return;
|
||||
}
|
||||
cropper.value.rotate(angle);
|
||||
changed.value = changed.value + 1;
|
||||
}
|
||||
|
||||
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,
|
||||
changed,
|
||||
};
|
||||
cropperHeight: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
|
||||
methods: {
|
||||
defaultSize({ imageSize, visibleArea }) {
|
||||
return {
|
||||
width: (visibleArea || imageSize).width,
|
||||
height: (visibleArea || imageSize).height,
|
||||
};
|
||||
},
|
||||
cropperWidth: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
submitted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "save", item: Blob): void;
|
||||
(e: "delete"): void;
|
||||
}>();
|
||||
|
||||
const cropper = ref<any>(null);
|
||||
const changed = ref(0);
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
type Control = {
|
||||
color: string;
|
||||
icon: string;
|
||||
callback: CallableFunction;
|
||||
};
|
||||
|
||||
function flip(hortizontal: boolean, vertical?: boolean) {
|
||||
if (!cropper.value) return;
|
||||
cropper.value.flip(hortizontal, vertical);
|
||||
changed.value = changed.value + 1;
|
||||
}
|
||||
|
||||
function rotate(angle: number) {
|
||||
if (!cropper.value) return;
|
||||
cropper.value.rotate(angle);
|
||||
changed.value = changed.value + 1;
|
||||
}
|
||||
|
||||
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),
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
function save() {
|
||||
if (!cropper.value) return;
|
||||
const { canvas } = cropper.value.getResult();
|
||||
if (!canvas) return;
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
emit("save", blob);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function defaultSize({ imageSize, visibleArea }: any) {
|
||||
return {
|
||||
width: (visibleArea || imageSize).width,
|
||||
height: (visibleArea || imageSize).height,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-text-field
|
||||
v-model="inputVal"
|
||||
v-model="modelValue"
|
||||
:label="$t('general.color')"
|
||||
>
|
||||
<template #prepend>
|
||||
@@ -9,7 +9,7 @@
|
||||
size="small"
|
||||
height="30px"
|
||||
width="30px"
|
||||
:color="inputVal || 'grey'"
|
||||
:color="modelValue || 'grey'"
|
||||
@click="setRandomHex"
|
||||
>
|
||||
<v-icon color="white">
|
||||
@@ -33,7 +33,7 @@
|
||||
<v-card>
|
||||
<v-card-text class="pa-0">
|
||||
<v-color-picker
|
||||
v-model="inputVal"
|
||||
v-model="modelValue"
|
||||
flat
|
||||
hide-inputs
|
||||
show-swatches
|
||||
@@ -46,42 +46,21 @@
|
||||
</v-text-field>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const menu = ref(false);
|
||||
|
||||
const inputVal = computed({
|
||||
get: () => {
|
||||
return props.modelValue;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
function getRandomHex() {
|
||||
return "#000000".replace(/0/g, function () {
|
||||
return (~~(Math.random() * 16)).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
function setRandomHex() {
|
||||
inputVal.value = getRandomHex();
|
||||
}
|
||||
|
||||
return {
|
||||
menu,
|
||||
setRandomHex,
|
||||
inputVal,
|
||||
};
|
||||
},
|
||||
<script setup lang="ts">
|
||||
const modelValue = defineModel({
|
||||
type: String,
|
||||
required: true,
|
||||
});
|
||||
|
||||
const menu = ref(false);
|
||||
|
||||
function getRandomHex() {
|
||||
return "#000000".replace(/0/g, function () {
|
||||
return (~~(Math.random() * 16)).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
function setRandomHex() {
|
||||
modelValue.value = getRandomHex();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -30,105 +30,60 @@
|
||||
</v-autocomplete>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/**
|
||||
* The InputLabelType component is a wrapper for v-autocomplete. It is used to abstract the selection functionality
|
||||
* of some common types within Mealie. This can mostly be used with any type of object provided it has a name and id
|
||||
* property. The name property is used to display the name of the object in the autocomplete dropdown. The id property
|
||||
* is used to store the id of the object in the itemId property.
|
||||
*
|
||||
* Supported Types
|
||||
* - MultiPurposeLabel
|
||||
* - RecipeIngredientFood
|
||||
* - RecipeIngredientUnit
|
||||
*
|
||||
* TODO: Add RecipeTag / Category to this selector
|
||||
* Future Supported Types
|
||||
* - RecipeTags
|
||||
* - RecipeCategories
|
||||
*
|
||||
* 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"`
|
||||
*/
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MultiPurposeLabelSummary } from "~/lib/api/types/labels";
|
||||
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||
import { useSearch } from "~/composables/use-search";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object as () => MultiPurposeLabelSummary | IngredientFood | IngredientUnit,
|
||||
required: false,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
items: {
|
||||
type: Array as () => Array<MultiPurposeLabelSummary | IngredientFood | IngredientUnit>,
|
||||
required: true,
|
||||
},
|
||||
itemId: {
|
||||
type: [String, Number],
|
||||
default: undefined,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
create: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// v-model for the selected item
|
||||
const modelValue = defineModel<MultiPurposeLabelSummary | IngredientFood | IngredientUnit | null>({ default: () => null });
|
||||
|
||||
// support v-model:item-id binding
|
||||
const itemId = defineModel<string | undefined>("item-id", { default: undefined });
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array as () => Array<MultiPurposeLabelSummary | IngredientFood | IngredientUnit>,
|
||||
required: true,
|
||||
},
|
||||
emits: ["update:modelValue", "update:item-id", "create"],
|
||||
setup(props, context) {
|
||||
const autocompleteRef = ref<HTMLInputElement>();
|
||||
|
||||
// Use the search composable
|
||||
const { search: searchInput, filtered: filteredItems } = useSearch(computed(() => props.items));
|
||||
|
||||
const itemIdVal = computed({
|
||||
get: () => {
|
||||
return props.itemId || undefined;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:item-id", val);
|
||||
},
|
||||
});
|
||||
|
||||
const itemVal = computed({
|
||||
get: () => {
|
||||
try {
|
||||
return Object.keys(props.modelValue).length !== 0 ? props.modelValue : null;
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
set: (val) => {
|
||||
itemIdVal.value = val?.id || undefined;
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
function emitCreate() {
|
||||
if (props.items.some(item => item.name === searchInput.value)) {
|
||||
return;
|
||||
}
|
||||
context.emit("create", searchInput.value);
|
||||
autocompleteRef.value?.blur();
|
||||
}
|
||||
|
||||
return {
|
||||
autocompleteRef,
|
||||
itemVal,
|
||||
itemIdVal,
|
||||
searchInput,
|
||||
filteredItems,
|
||||
emitCreate,
|
||||
};
|
||||
icon: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
create: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "create", val: string): void;
|
||||
}>();
|
||||
|
||||
const autocompleteRef = ref<HTMLInputElement>();
|
||||
|
||||
// Use the search composable
|
||||
const { search: searchInput, filtered: filteredItems } = useSearch(computed(() => props.items));
|
||||
|
||||
const itemVal = computed({
|
||||
get: () => {
|
||||
if (!modelValue.value || Object.keys(modelValue.value).length === 0) {
|
||||
return null;
|
||||
}
|
||||
return modelValue.value;
|
||||
},
|
||||
set: (val) => {
|
||||
itemId.value = val?.id || "";
|
||||
modelValue.value = val;
|
||||
},
|
||||
});
|
||||
|
||||
function emitCreate() {
|
||||
if (props.items.some(item => item.name === searchInput.value)) {
|
||||
return;
|
||||
}
|
||||
emit("create", searchInput.value);
|
||||
autocompleteRef.value?.blur();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<BaseDialog
|
||||
v-model="dialog"
|
||||
v-model="modelValue"
|
||||
:icon="$globals.icons.translate"
|
||||
:title="$t('language-dialog.choose-language')"
|
||||
>
|
||||
@@ -43,50 +43,26 @@
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useLocales } from "~/composables/use-locales";
|
||||
import { normalizeFilter } from "~/composables/use-utils";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, { emit }) {
|
||||
const dialog = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit("update:modelValue", value),
|
||||
});
|
||||
const modelValue = defineModel<boolean>({ default: () => false });
|
||||
|
||||
const { locales: LOCALES, locale, i18n } = useLocales();
|
||||
const { locales: LOCALES, locale, i18n } = useLocales();
|
||||
|
||||
const selectedLocale = ref(locale.value);
|
||||
const onLocaleSelect = (value: string) => {
|
||||
if (value && locales.some(l => l.value === value)) {
|
||||
locale.value = value as any;
|
||||
}
|
||||
};
|
||||
const selectedLocale = ref(locale.value);
|
||||
const onLocaleSelect = (value: string) => {
|
||||
if (value && locales.some(l => l.value === value)) {
|
||||
locale.value = value as any;
|
||||
}
|
||||
};
|
||||
|
||||
watch(locale, () => {
|
||||
dialog.value = false; // Close dialog when locale changes
|
||||
});
|
||||
|
||||
const locales = LOCALES.filter(lc =>
|
||||
i18n.locales.value.map(i18nLocale => i18nLocale.code).includes(lc.value as any),
|
||||
);
|
||||
|
||||
return {
|
||||
dialog,
|
||||
i18n,
|
||||
locales,
|
||||
locale,
|
||||
selectedLocale,
|
||||
onLocaleSelect,
|
||||
normalizeFilter,
|
||||
};
|
||||
},
|
||||
watch(locale, () => {
|
||||
modelValue.value = false; // Close dialog when locale changes
|
||||
});
|
||||
|
||||
const locales = LOCALES.filter(lc =>
|
||||
i18n.locales.value.map(i18nLocale => i18nLocale.code).includes(lc.value as any),
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<v-textarea
|
||||
v-if="!previewState"
|
||||
v-bind="textarea"
|
||||
v-model="inputVal"
|
||||
v-model="modelValue"
|
||||
:class="label == '' ? '' : 'mt-5'"
|
||||
:label="label"
|
||||
auto-grow
|
||||
@@ -33,60 +33,42 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
name: "MarkdownEditor",
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
preview: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
displayPreview: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
textarea: {
|
||||
type: Object as () => unknown,
|
||||
default: () => ({}),
|
||||
},
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
emits: ["update:modelValue", "input:preview"],
|
||||
setup(props, context) {
|
||||
const fallbackPreview = ref(false);
|
||||
const previewState = computed({
|
||||
get: () => {
|
||||
return props.preview ?? fallbackPreview.value;
|
||||
},
|
||||
set: (val) => {
|
||||
if (props.preview) {
|
||||
context.emit("input:preview", val);
|
||||
}
|
||||
else {
|
||||
fallbackPreview.value = val;
|
||||
}
|
||||
},
|
||||
});
|
||||
preview: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
displayPreview: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
textarea: {
|
||||
type: Object as () => unknown,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const inputVal = computed({
|
||||
get: () => {
|
||||
return props.modelValue;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
return {
|
||||
previewState,
|
||||
inputVal,
|
||||
};
|
||||
const emit = defineEmits<{
|
||||
(e: "input:preview", value: boolean): void;
|
||||
}>();
|
||||
|
||||
const modelValue = defineModel<string>("modelValue");
|
||||
|
||||
const fallbackPreview = ref(false);
|
||||
const previewState = computed({
|
||||
get: () => props.preview ?? fallbackPreview.value,
|
||||
set: (val: boolean) => {
|
||||
if (props.preview) {
|
||||
emit("input:preview", val);
|
||||
}
|
||||
else {
|
||||
fallbackPreview.value = val;
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -8,53 +8,40 @@
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
<script setup lang="ts">
|
||||
import JsonEditorVue from "json-editor-vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "RecipeJsonEditor",
|
||||
components: { JsonEditorVue },
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: "1500px",
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, { emit }) {
|
||||
function parseEvent(event: any): object {
|
||||
if (!event) {
|
||||
return props.modelValue || {};
|
||||
}
|
||||
try {
|
||||
if (event.json) {
|
||||
return event.json;
|
||||
}
|
||||
else if (event.text) {
|
||||
return JSON.parse(event.text);
|
||||
}
|
||||
else {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return props.modelValue || {};
|
||||
}
|
||||
}
|
||||
function onChange(event: any) {
|
||||
const parsed = parseEvent(event);
|
||||
if (parsed !== props.modelValue) {
|
||||
emit("update:modelValue", parsed);
|
||||
}
|
||||
}
|
||||
return {
|
||||
onChange,
|
||||
};
|
||||
const modelValue = defineModel<object>("modelValue", { default: () => ({}) });
|
||||
defineProps({
|
||||
height: {
|
||||
type: String,
|
||||
default: "1500px",
|
||||
},
|
||||
});
|
||||
|
||||
function parseEvent(event: any): object {
|
||||
if (!event) {
|
||||
return modelValue.value || {};
|
||||
}
|
||||
try {
|
||||
if (event.json) {
|
||||
return event.json;
|
||||
}
|
||||
else if (event.text) {
|
||||
return JSON.parse(event.text);
|
||||
}
|
||||
else {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return modelValue.value || {};
|
||||
}
|
||||
}
|
||||
function onChange(event: any) {
|
||||
const parsed = parseEvent(event);
|
||||
if (parsed !== modelValue.value) {
|
||||
modelValue.value = parsed;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -27,54 +27,46 @@
|
||||
</v-data-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { ReportSummary } from "~/lib/api/types/reports";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
items: {
|
||||
required: true,
|
||||
type: Array as () => Array<ReportSummary>,
|
||||
},
|
||||
},
|
||||
emits: ["delete"],
|
||||
|
||||
setup(_, context) {
|
||||
const i18n = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const headers = [
|
||||
{ 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) {
|
||||
if (item.status === "in-progress") {
|
||||
return;
|
||||
}
|
||||
|
||||
router.push(`/group/reports/${item.id}`);
|
||||
}
|
||||
|
||||
function capitalize(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
function deleteReport(id: string) {
|
||||
context.emit("delete", id);
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
handleRowClick,
|
||||
capitalize,
|
||||
deleteReport,
|
||||
};
|
||||
defineProps({
|
||||
items: {
|
||||
type: Array as () => Array<ReportSummary>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "delete", id: string): void;
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const headers = [
|
||||
{ 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) {
|
||||
if (item.status === "in-progress") {
|
||||
return;
|
||||
}
|
||||
|
||||
router.push(`/group/reports/${item.id}`);
|
||||
}
|
||||
|
||||
function capitalize(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
function deleteReport(id: string) {
|
||||
emit("delete", id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div v-html="value" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { marked } from "marked";
|
||||
|
||||
@@ -11,62 +11,55 @@ enum DOMPurifyHook {
|
||||
UponSanitizeAttribute = "uponSanitizeAttribute",
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
source: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
const props = defineProps({
|
||||
source: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
setup(props) {
|
||||
const ALLOWED_STYLE_TAGS = [
|
||||
"background-color", "color", "font-style", "font-weight", "text-decoration", "text-align",
|
||||
];
|
||||
});
|
||||
|
||||
function sanitizeMarkdown(rawHtml: string | null | undefined): string {
|
||||
if (!rawHtml) {
|
||||
return "";
|
||||
}
|
||||
const ALLOWED_STYLE_TAGS = [
|
||||
"background-color", "color", "font-style", "font-weight", "text-decoration", "text-align",
|
||||
];
|
||||
|
||||
DOMPurify.addHook(DOMPurifyHook.UponSanitizeAttribute, (node, data) => {
|
||||
if (data.attrName === "style") {
|
||||
const styles = data.attrValue.split(";").filter((style) => {
|
||||
const [property] = style.split(":");
|
||||
return ALLOWED_STYLE_TAGS.includes(property.trim().toLowerCase());
|
||||
});
|
||||
data.attrValue = styles.join(";");
|
||||
}
|
||||
function sanitizeMarkdown(rawHtml: string | null | undefined): string {
|
||||
if (!rawHtml) {
|
||||
return "";
|
||||
}
|
||||
|
||||
DOMPurify.addHook(DOMPurifyHook.UponSanitizeAttribute, (node, data) => {
|
||||
if (data.attrName === "style") {
|
||||
const styles = data.attrValue.split(";").filter((style) => {
|
||||
const [property] = style.split(":");
|
||||
return ALLOWED_STYLE_TAGS.includes(property.trim().toLowerCase());
|
||||
});
|
||||
|
||||
const sanitized = DOMPurify.sanitize(rawHtml, {
|
||||
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",
|
||||
],
|
||||
ALLOWED_ATTR: [
|
||||
"href", "src", "alt", "height", "width", "class", "allow", "title", "allowfullscreen", "frameborder",
|
||||
"scrolling", "cite", "datetime", "name", "abbr", "target", "border", "start", "style",
|
||||
],
|
||||
});
|
||||
|
||||
Object.values(DOMPurifyHook).forEach((hook) => {
|
||||
DOMPurify.removeHook(hook);
|
||||
});
|
||||
|
||||
return sanitized;
|
||||
data.attrValue = styles.join(";");
|
||||
}
|
||||
});
|
||||
|
||||
const value = computed(() => {
|
||||
const rawHtml = marked.parse(props.source || "", { async: false, breaks: true });
|
||||
return sanitizeMarkdown(rawHtml);
|
||||
});
|
||||
const sanitized = DOMPurify.sanitize(rawHtml, {
|
||||
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",
|
||||
],
|
||||
ALLOWED_ATTR: [
|
||||
"href", "src", "alt", "height", "width", "class", "allow", "title", "allowfullscreen", "frameborder",
|
||||
"scrolling", "cite", "datetime", "name", "abbr", "target", "border", "start", "style",
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
value,
|
||||
};
|
||||
},
|
||||
Object.values(DOMPurifyHook).forEach((hook) => {
|
||||
DOMPurify.removeHook(hook);
|
||||
});
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
const value = computed(() => {
|
||||
const rawHtml = marked.parse(props.source || "", { async: false, breaks: true });
|
||||
return sanitizeMarkdown(rawHtml);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -30,33 +30,26 @@
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
minWidth: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
setup(props) {
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const activeIcon = computed(() => {
|
||||
return props.icon ?? $globals.icons.primary;
|
||||
});
|
||||
|
||||
return {
|
||||
activeIcon,
|
||||
};
|
||||
minWidth: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const activeIcon = computed(() => {
|
||||
return props.icon ?? $globals.icons.primary;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,40 +2,26 @@
|
||||
<component :is="tag">
|
||||
<slot
|
||||
name="activator"
|
||||
v-bind="{ toggle, state }"
|
||||
v-bind="{ toggle, modelValue }"
|
||||
/>
|
||||
<slot v-bind="{ state, toggle }" />
|
||||
<slot v-bind="{ modelValue, toggle }" />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
tag: {
|
||||
type: String,
|
||||
default: "div",
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const state = ref(false);
|
||||
<script setup lang="ts">
|
||||
const modelValue = defineModel({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
});
|
||||
|
||||
const toggle = () => {
|
||||
state.value = !state.value;
|
||||
};
|
||||
|
||||
watch(state, () => {
|
||||
context.emit("update:modelValue", state.value);
|
||||
});
|
||||
|
||||
return {
|
||||
state,
|
||||
toggle,
|
||||
};
|
||||
defineProps({
|
||||
tag: {
|
||||
type: String,
|
||||
default: "div",
|
||||
},
|
||||
});
|
||||
|
||||
const toggle = () => {
|
||||
modelValue.value = !modelValue.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -12,42 +12,33 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useWakeLock } from "@vueuse/core";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
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");
|
||||
}
|
||||
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());
|
||||
|
||||
return {
|
||||
wakeLock,
|
||||
wakeIsSupported,
|
||||
};
|
||||
},
|
||||
});
|
||||
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());
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user