Recipe Card Section

This commit is contained in:
Kuchenpirat 2025-07-30 09:41:07 +00:00
commit c123e3e2a1

View file

@ -36,11 +36,11 @@
offset-y offset-y
start start
> >
<template #activator="{ props }"> <template #activator="{ props: activatorProps }">
<v-btn <v-btn
variant="text" variant="text"
:icon="$vuetify.display.xs" :icon="$vuetify.display.xs"
v-bind="props" v-bind="activatorProps"
:loading="sortLoading" :loading="sortLoading"
> >
<v-icon :start="!$vuetify.display.xs"> <v-icon :start="!$vuetify.display.xs">
@ -162,7 +162,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { useThrottleFn } from "@vueuse/core"; import { useThrottleFn } from "@vueuse/core";
import RecipeCard from "./RecipeCard.vue"; import RecipeCard from "./RecipeCard.vue";
import RecipeCardMobile from "./RecipeCardMobile.vue"; import RecipeCardMobile from "./RecipeCardMobile.vue";
@ -175,83 +175,69 @@ import type { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
const REPLACE_RECIPES_EVENT = "replaceRecipes"; const REPLACE_RECIPES_EVENT = "replaceRecipes";
const APPEND_RECIPES_EVENT = "appendRecipes"; const APPEND_RECIPES_EVENT = "appendRecipes";
export default defineNuxtComponent({ interface Props {
components: { disableToolbar?: boolean;
RecipeCard, disableSort?: boolean;
RecipeCardMobile, icon?: string | null;
}, title?: string | null;
props: { singleColumn?: boolean;
disableToolbar: { recipes?: Recipe[];
type: Boolean, query?: RecipeSearchQuery | null;
default: false, }
}, const props = withDefaults(defineProps<Props>(), {
disableSort: { disableToolbar: false,
type: Boolean, disableSort: false,
default: false, icon: null,
}, title: null,
icon: { singleColumn: false,
type: String, recipes: () => [],
default: null, query: null,
}, });
title: {
type: String,
default: null,
},
singleColumn: {
type: Boolean,
default: false,
},
recipes: {
type: Array as () => Recipe[],
default: () => [],
},
query: {
type: Object as () => RecipeSearchQuery,
default: null,
},
},
setup(props, context) {
const { $vuetify } = useNuxtApp();
const preferences = useUserSortPreferences();
const EVENTS = { const emit = defineEmits<{
replaceRecipes: [recipes: Recipe[]];
appendRecipes: [recipes: Recipe[]];
}>();
const { $vuetify } = useNuxtApp();
const preferences = useUserSortPreferences();
const EVENTS = {
az: "az", az: "az",
rating: "rating", rating: "rating",
created: "created", created: "created",
updated: "updated", updated: "updated",
lastMade: "lastMade", lastMade: "lastMade",
shuffle: "shuffle", shuffle: "shuffle",
}; };
const $auth = useMealieAuth(); const $auth = useMealieAuth();
const { $globals } = useNuxtApp(); const { $globals } = useNuxtApp();
const { isOwnGroup } = useLoggedInState(); const { isOwnGroup } = useLoggedInState();
const useMobileCards = computed(() => { const useMobileCards = computed(() => {
return $vuetify.display.smAndDown.value || preferences.value.useMobileCards; return $vuetify.display.smAndDown.value || preferences.value.useMobileCards;
}); });
const displayTitleIcon = computed(() => { const displayTitleIcon = computed(() => {
return props.icon || $globals.icons.tags; return props.icon || $globals.icons.tags;
}); });
const state = reactive({ const sortLoading = ref(false);
sortLoading: false,
});
const route = useRoute(); const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || ""); const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
const page = ref(1); const page = ref(1);
const perPage = 32; const perPage = 32;
const hasMore = ref(true); const hasMore = ref(true);
const ready = ref(false); const ready = ref(false);
const loading = ref(false); const loading = ref(false);
const { fetchMore, getRandom } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value); const { fetchMore, getRandom } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
const router = useRouter(); const router = useRouter();
const queryFilter = computed(() => { const queryFilter = computed(() => {
return props.query.queryFilter || null; return props.query?.queryFilter || null;
// TODO: allow user to filter out null values when ordering by a value that may be null (such as lastMade) // TODO: allow user to filter out null values when ordering by a value that may be null (such as lastMade)
@ -265,9 +251,9 @@ export default defineNuxtComponent({
// } else { // } else {
// return orderByFilter; // return orderByFilter;
// } // }
}); });
async function fetchRecipes(pageCount = 1) { async function fetchRecipes(pageCount = 1) {
const orderDir = props.query?.orderDirection || preferences.value.orderDirection; const orderDir = props.query?.orderDirection || preferences.value.orderDirection;
const orderByNullPosition = props.query?.orderByNullPosition || orderDir === "asc" ? "first" : "last"; const orderByNullPosition = props.query?.orderByNullPosition || orderDir === "asc" ? "first" : "last";
return await fetchMore( return await fetchMore(
@ -280,17 +266,17 @@ export default defineNuxtComponent({
// we use a computed queryFilter to filter out recipes that have a null value for the property we're sorting by // we use a computed queryFilter to filter out recipes that have a null value for the property we're sorting by
queryFilter.value, queryFilter.value,
); );
} }
onMounted(async () => { onMounted(async () => {
await initRecipes(); await initRecipes();
ready.value = true; ready.value = true;
}); });
let lastQuery: string | undefined = JSON.stringify(props.query); let lastQuery: string | undefined = JSON.stringify(props.query);
watch( watch(
() => props.query, () => props.query,
async (newValue: RecipeSearchQuery | undefined) => { async (newValue: RecipeSearchQuery | undefined | null) => {
const newValueString = JSON.stringify(newValue); const newValueString = JSON.stringify(newValue);
if (lastQuery !== newValueString) { if (lastQuery !== newValueString) {
lastQuery = newValueString; lastQuery = newValueString;
@ -299,9 +285,9 @@ export default defineNuxtComponent({
ready.value = true; ready.value = true;
} }
}, },
); );
async function initRecipes() { async function initRecipes() {
page.value = 1; page.value = 1;
hasMore.value = true; hasMore.value = true;
@ -315,10 +301,10 @@ export default defineNuxtComponent({
// since we doubled the first call, we also need to advance the page // since we doubled the first call, we also need to advance the page
page.value = page.value + 1; page.value = page.value + 1;
context.emit(REPLACE_RECIPES_EVENT, newRecipes); emit(REPLACE_RECIPES_EVENT, newRecipes);
} }
const infiniteScroll = useThrottleFn(async () => { const infiniteScroll = useThrottleFn(async () => {
if (!hasMore.value || loading.value) { if (!hasMore.value || loading.value) {
return; return;
} }
@ -331,14 +317,14 @@ export default defineNuxtComponent({
hasMore.value = false; hasMore.value = false;
} }
if (newRecipes.length) { if (newRecipes.length) {
context.emit(APPEND_RECIPES_EVENT, newRecipes); emit(APPEND_RECIPES_EVENT, newRecipes);
} }
loading.value = false; loading.value = false;
}, 500); }, 500);
async function sortRecipes(sortType: string) { async function sortRecipes(sortType: string) {
if (state.sortLoading || loading.value) { if (sortLoading.value || loading.value) {
return; return;
} }
@ -403,45 +389,29 @@ export default defineNuxtComponent({
page.value = 1; page.value = 1;
hasMore.value = true; hasMore.value = true;
state.sortLoading = true; sortLoading.value = true;
loading.value = true; loading.value = true;
// fetch new recipes // fetch new recipes
const newRecipes = await fetchRecipes(); const newRecipes = await fetchRecipes();
context.emit(REPLACE_RECIPES_EVENT, newRecipes); emit(REPLACE_RECIPES_EVENT, newRecipes);
state.sortLoading = false; sortLoading.value = false;
loading.value = false; loading.value = false;
} }
async function navigateRandom() { async function navigateRandom() {
const recipe = await getRandom(props.query, queryFilter.value); const recipe = await getRandom(props.query, queryFilter.value);
if (!recipe?.slug) { if (!recipe?.slug) {
return; return;
} }
router.push(`/g/${groupSlug.value}/r/${recipe.slug}`); router.push(`/g/${groupSlug.value}/r/${recipe.slug}`);
} }
function toggleMobileCards() { function toggleMobileCards() {
preferences.value.useMobileCards = !preferences.value.useMobileCards; preferences.value.useMobileCards = !preferences.value.useMobileCards;
} }
return {
...toRefs(state),
displayTitleIcon,
EVENTS,
infiniteScroll,
ready,
loading,
navigateRandom,
preferences,
sortRecipes,
toggleMobileCards,
useMobileCards,
};
},
});
</script> </script>
<style> <style>