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