mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-04-10 15:05:35 -04:00
chore: Nuxt 4 upgrade (#7426)
This commit is contained in:
264
frontend/app/components/Domain/Recipe/RecipeOrganizerPage.vue
Normal file
264
frontend/app/components/Domain/Recipe/RecipeOrganizerPage.vue
Normal file
@@ -0,0 +1,264 @@
|
||||
<template>
|
||||
<div v-if="items">
|
||||
<RecipeOrganizerDialog
|
||||
v-model="dialogs.organizer"
|
||||
:item-type="itemType"
|
||||
/>
|
||||
|
||||
<BaseDialog
|
||||
v-if="deleteTarget"
|
||||
v-model="dialogs.delete"
|
||||
:title="$t('general.delete-with-name', { name: $t(translationKey) })"
|
||||
color="error"
|
||||
:icon="$globals.icons.alertCircle"
|
||||
can-confirm
|
||||
@confirm="deleteOne()"
|
||||
>
|
||||
<v-card-text>
|
||||
<p>{{ $t("general.confirm-delete-generic-with-name", { name: $t(translationKey) }) }}</p>
|
||||
<p class="mt-4 mb-0 ml-4">
|
||||
{{ deleteTarget.name }}
|
||||
</p>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
||||
<BaseDialog
|
||||
v-if="updateTarget"
|
||||
v-model="dialogs.update"
|
||||
:title="$t('general.update')"
|
||||
:icon="$globals.icons.edit"
|
||||
can-confirm
|
||||
@confirm="updateOne()"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="updateTarget.name"
|
||||
:label="$t('general.name')"
|
||||
/>
|
||||
<v-checkbox
|
||||
v-if="itemType === Organizer.Tool"
|
||||
v-model="updateTarget.onHand"
|
||||
:label="$t('tool.on-hand')"
|
||||
/>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
||||
<v-row dense>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="searchString"
|
||||
variant="outlined"
|
||||
autofocus
|
||||
color="primary accent-3"
|
||||
:placeholder="$t('search.search-placeholder')"
|
||||
:prepend-inner-icon="$globals.icons.search"
|
||||
clearable
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-app-bar
|
||||
color="transparent"
|
||||
flat
|
||||
class="mt-n1 rounded align-center position-relative w-100 left-0 top-0"
|
||||
>
|
||||
<v-icon
|
||||
size="large"
|
||||
start
|
||||
>
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headline">
|
||||
<slot name="title" />
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<BaseButton
|
||||
create
|
||||
@click="dialogs.organizer = true"
|
||||
/>
|
||||
</v-app-bar>
|
||||
<section
|
||||
v-for="(itms, key, idx) in itemsSorted"
|
||||
:key="'header' + idx"
|
||||
:class="idx === 1 ? null : 'my-4'"
|
||||
>
|
||||
<BaseCardSectionTitle
|
||||
v-if="isTitle(key)"
|
||||
:title="key"
|
||||
/>
|
||||
<v-row>
|
||||
<v-col
|
||||
v-for="(item, index) in itms"
|
||||
:key="'cat' + index"
|
||||
cols="12"
|
||||
:sm="12"
|
||||
:md="6"
|
||||
:lg="4"
|
||||
:xl="3"
|
||||
>
|
||||
<v-card
|
||||
v-if="item"
|
||||
class="left-border"
|
||||
hover
|
||||
:to="`/g/${groupSlug}?${itemType}=${item.id}`"
|
||||
>
|
||||
<v-card-actions>
|
||||
<v-icon>
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
<v-card-title class="py-1 text-truncate flex-shrink-1 flex-grow-1">
|
||||
{{ item.name }}
|
||||
</v-card-title>
|
||||
<ContextMenu
|
||||
:items="[presets.delete, presets.edit]"
|
||||
@delete="confirmDelete(item)"
|
||||
@edit="openUpdateDialog(item)"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Fuse from "fuse.js";
|
||||
import { useContextPresets } from "~/composables/use-context-presents";
|
||||
import RecipeOrganizerDialog from "~/components/Domain/Recipe/RecipeOrganizerDialog.vue";
|
||||
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
|
||||
import { useRouteQuery } from "~/composables/use-router";
|
||||
import { deepCopy } from "~/composables/use-utils";
|
||||
|
||||
interface GenericItem {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
onHand: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
items: GenericItem[];
|
||||
icon: string;
|
||||
itemType: RecipeOrganizer;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [item: GenericItem];
|
||||
delete: [id: string];
|
||||
}>();
|
||||
|
||||
const state = reactive({
|
||||
// Search Options
|
||||
options: {
|
||||
ignoreLocation: true,
|
||||
shouldSort: true,
|
||||
threshold: 0.2,
|
||||
location: 0,
|
||||
distance: 20,
|
||||
findAllMatches: true,
|
||||
maxPatternLength: 32,
|
||||
minMatchCharLength: 1,
|
||||
keys: ["name"],
|
||||
},
|
||||
});
|
||||
|
||||
const auth = useMealieAuth();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user?.value?.groupSlug || "");
|
||||
|
||||
// =================================================================
|
||||
// Context Menu
|
||||
|
||||
const dialogs = ref({
|
||||
organizer: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
});
|
||||
|
||||
const presets = useContextPresets();
|
||||
|
||||
const translationKey = computed<string>(() => {
|
||||
const typeMap = {
|
||||
categories: "category.category",
|
||||
tags: "tag.tag",
|
||||
tools: "tool.tool",
|
||||
foods: "shopping-list.food",
|
||||
households: "household.household",
|
||||
};
|
||||
return typeMap[props.itemType] || "";
|
||||
});
|
||||
|
||||
const deleteTarget = ref<GenericItem | null>(null);
|
||||
const updateTarget = ref<GenericItem | null>(null);
|
||||
|
||||
function confirmDelete(item: GenericItem) {
|
||||
deleteTarget.value = item;
|
||||
dialogs.value.delete = true;
|
||||
}
|
||||
|
||||
function deleteOne() {
|
||||
if (!deleteTarget.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit("delete", deleteTarget.value.id);
|
||||
}
|
||||
|
||||
function openUpdateDialog(item: GenericItem) {
|
||||
updateTarget.value = deepCopy(item);
|
||||
dialogs.value.update = true;
|
||||
}
|
||||
|
||||
function updateOne() {
|
||||
if (!updateTarget.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit("update", updateTarget.value);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Search Functions
|
||||
|
||||
const searchString = useRouteQuery("q", "");
|
||||
|
||||
const fuse = computed(() => {
|
||||
return new Fuse(props.items, state.options);
|
||||
});
|
||||
|
||||
const fuzzyItems = computed<GenericItem[]>(() => {
|
||||
if (searchString.value.trim() === "") {
|
||||
return props.items;
|
||||
}
|
||||
const result = fuse.value.search(searchString.value.trim() as string);
|
||||
return result.map(x => x.item);
|
||||
});
|
||||
|
||||
// =================================================================
|
||||
// Sorted Items
|
||||
|
||||
const itemsSorted = computed(() => {
|
||||
const byLetter: { [key: string]: Array<GenericItem> } = {};
|
||||
|
||||
if (!fuzzyItems.value) {
|
||||
return byLetter;
|
||||
}
|
||||
|
||||
[...fuzzyItems.value]
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.forEach((item) => {
|
||||
const letter = item.name[0].toUpperCase();
|
||||
if (!byLetter[letter]) {
|
||||
byLetter[letter] = [];
|
||||
}
|
||||
byLetter[letter].push(item);
|
||||
});
|
||||
|
||||
return byLetter;
|
||||
});
|
||||
|
||||
function isTitle(str: number | string) {
|
||||
return typeof str === "string" && str.length === 1;
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user