feat: restore frontend sorting for all recipes (#1497)

* fixed typo

* merged "all recipes" pagination into recipe card
created custom sort card for all recipes
refactored backend calls for all recipes to sort/paginate

* frontend lint fixes

* restored recipes reference

* replaced "this" with reference

* fix linting errors

* re-order context menu

* add todo

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
This commit is contained in:
Michael Genson 2022-07-26 21:08:56 -05:00 committed by GitHub
parent 703ee32653
commit 07fef8af9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 211 additions and 53 deletions

View File

@ -14,18 +14,50 @@
</v-icon>
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.random") }}
</v-btn>
<ContextMenu
v-if="!$vuetify.breakpoint.xsOnly"
:items="[
{
title: 'Toggle View',
icon: $globals.icons.eye,
event: 'toggle-dense-view',
},
]"
@toggle-dense-view="mobileCards = !mobileCards"
/>
<v-menu v-if="$listeners.sort" offset-y left>
<template #activator="{ on, attrs }">
<v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" :loading="sortLoading" v-on="on">
<v-icon :left="!$vuetify.breakpoint.xsOnly">
{{ $globals.icons.sort }}
</v-icon>
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.sort") }}
</v-btn>
</template>
<v-list>
<v-list-item @click="sortRecipesFrontend(EVENTS.az)">
<v-icon left>
{{ $globals.icons.orderAlphabeticalAscending }}
</v-icon>
<v-list-item-title>{{ $t("general.sort-alphabetically") }}</v-list-item-title>
</v-list-item>
<v-list-item @click="sortRecipesFrontend(EVENTS.rating)">
<v-icon left>
{{ $globals.icons.star }}
</v-icon>
<v-list-item-title>{{ $t("general.rating") }}</v-list-item-title>
</v-list-item>
<v-list-item @click="sortRecipesFrontend(EVENTS.created)">
<v-icon left>
{{ $globals.icons.newBox }}
</v-icon>
<v-list-item-title>{{ $t("general.created") }}</v-list-item-title>
</v-list-item>
<v-list-item @click="sortRecipesFrontend(EVENTS.updated)">
<v-icon left>
{{ $globals.icons.update }}
</v-icon>
<v-list-item-title>{{ $t("general.updated") }}</v-list-item-title>
</v-list-item>
<v-list-item @click="sortRecipesFrontend(EVENTS.shuffle)">
<v-icon left>
{{ $globals.icons.shuffleVariant }}
</v-icon>
<v-list-item-title>{{ $t("general.shuffle") }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-menu v-if="$listeners.sortRecipes" offset-y left>
<template #activator="{ on, attrs }">
<v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" :loading="sortLoading" v-on="on">
<v-icon :left="!$vuetify.breakpoint.xsOnly">
@ -59,14 +91,19 @@
</v-icon>
<v-list-item-title>{{ $t("general.updated") }}</v-list-item-title>
</v-list-item>
<v-list-item @click="sortRecipes(EVENTS.shuffle)">
<v-icon left>
{{ $globals.icons.shuffleVariant }}
</v-icon>
<v-list-item-title>{{ $t("general.shuffle") }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<ContextMenu
v-if="!$vuetify.breakpoint.xsOnly"
:items="[
{
title: 'Toggle View',
icon: $globals.icons.eye,
event: 'toggle-dense-view',
},
]"
@toggle-dense-view="mobileCards = !mobileCards"
/>
</v-app-bar>
<div v-if="recipes" class="mt-2">
<v-row v-if="!viewScale">
@ -110,17 +147,37 @@
</v-col>
</v-row>
</div>
<div v-if="usePagination">
<v-card v-intersect="infiniteScroll"></v-card>
<v-fade-transition>
<AppLoader v-if="loading" :loading="loading" />
</v-fade-transition>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, toRefs, useContext, useRouter, ref } from "@nuxtjs/composition-api";
import {
computed,
defineComponent,
onMounted,
reactive,
ref,
toRefs,
useAsync,
useContext,
useRouter,
} from "@nuxtjs/composition-api";
import { useThrottleFn } from "@vueuse/core";
import RecipeCard from "./RecipeCard.vue";
import RecipeCardMobile from "./RecipeCardMobile.vue";
import { useSorter } from "~/composables/recipes";
import { useAsyncKey } from "~/composables/use-utils";
import { useLazyRecipes, useSorter } from "~/composables/recipes";
import { Recipe } from "~/types/api-types/recipe";
const SORT_EVENT = "sort";
const REPLACE_RECIPES_EVENT = "replaceRecipes";
const APPEND_RECIPES_EVENT = "appendRecipes";
export default defineComponent({
components: {
@ -148,6 +205,10 @@ export default defineComponent({
type: Array as () => Recipe[],
default: () => [],
},
usePagination: {
type: Boolean,
default: false,
},
},
setup(props, context) {
const mobileCards = ref(false);
@ -184,7 +245,114 @@ export default defineComponent({
}
}
const page = ref(1);
const perPage = ref(30);
const orderBy = ref("name");
const orderDirection = ref("asc");
const hasMore = ref(true);
const ready = ref(false);
const loading = ref(false);
const { recipes, fetchMore } = useLazyRecipes();
onMounted(async () => {
if (props.usePagination) {
const newRecipes = await fetchMore(page.value, perPage.value, orderBy.value, orderDirection.value);
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
ready.value = true;
}
});
const infiniteScroll = useThrottleFn(() => {
useAsync(async () => {
if (!ready.value || !hasMore.value || loading.value) {
return;
}
loading.value = true;
page.value = page.value + 1;
const newRecipes = await fetchMore(page.value, perPage.value, orderBy.value, orderDirection.value);
if (!newRecipes.length) {
hasMore.value = false;
} else {
context.emit(APPEND_RECIPES_EVENT, newRecipes);
}
loading.value = false;
}, useAsyncKey());
}, 500);
/*
sortRecipes helps filter using the API. This will eventually replace the sortRecipesFrontend function which pulls all recipes
(without pagination) and does the sorting in the frontend.
TODO: remove sortRecipesFrontend and remove duplicate "sortRecipes" section in the template (above)
TODO: use indicator to show asc / desc order
*/
function sortRecipes(sortType: string) {
if (state.sortLoading || loading.value) {
return;
}
switch (sortType) {
case EVENTS.az:
if (orderBy.value !== "name") {
orderBy.value = "name";
orderDirection.value = "asc";
} else {
orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc";
}
break;
case EVENTS.rating:
if (orderBy.value !== "rating") {
orderBy.value = "rating";
orderDirection.value = "desc";
} else {
orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc";
}
break;
case EVENTS.created:
if (orderBy.value !== "created_at") {
orderBy.value = "created_at";
orderDirection.value = "desc";
} else {
orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc";
}
break;
case EVENTS.updated:
if (orderBy.value !== "update_at") {
orderBy.value = "update_at";
orderDirection.value = "desc";
} else {
orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc";
}
break;
default:
console.log("Unknown Event", sortType);
return;
}
useAsync(async () => {
// reset pagination
page.value = 1;
hasMore.value = true;
state.sortLoading = true;
loading.value = true;
// fetch new recipes
const newRecipes = await fetchMore(page.value, perPage.value, orderBy.value, orderDirection.value);
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
state.sortLoading = false;
loading.value = false;
}, useAsyncKey());
}
function sortRecipesFrontend(sortType: string) {
state.sortLoading = true;
const sortTarget = [...props.recipes];
switch (sortType) {
@ -217,8 +385,11 @@ export default defineComponent({
EVENTS,
viewScale,
displayTitleIcon,
infiniteScroll,
loading,
navigateRandom,
sortRecipes,
sortRecipesFrontend,
};
},
});

View File

@ -63,11 +63,7 @@ export const useLazyRecipes = function () {
async function fetchMore(page: number, perPage: number, orderBy: string | null = null, orderDirection = "desc") {
const { data } = await api.recipes.getAll(page, perPage, { orderBy, orderDirection });
if (data) {
data.items.forEach((recipe) => {
recipes.value?.push(recipe);
});
}
return data ? data.items : [];
}
return {

View File

@ -4,48 +4,35 @@
:icon="$globals.icons.primary"
:title="$t('page.all-recipes')"
:recipes="recipes"
:use-pagination="true"
@sortRecipes="assignSorted"
@replaceRecipes="replaceRecipes"
@appendRecipes="appendRecipes"
@delete="removeRecipe"
></RecipeCardSection>
<v-card v-intersect="infiniteScroll"></v-card>
<v-fade-transition>
<AppLoader v-if="loading" :loading="loading" />
</v-fade-transition>
</v-container>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from "@nuxtjs/composition-api";
import { useThrottleFn } from "@vueuse/core";
import { defineComponent } from "@nuxtjs/composition-api";
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
import { useLazyRecipes } from "~/composables/recipes";
import { Recipe } from "~/types/api-types/recipe";
export default defineComponent({
components: { RecipeCardSection },
setup() {
const page = ref(1);
const perPage = ref(30);
const orderBy = "name";
const orderDirection = "asc";
const ready = ref(false);
const loading = ref(false);
const { recipes, fetchMore } = useLazyRecipes();
onMounted(async () => {
await fetchMore(page.value, perPage.value, orderBy, orderDirection);
ready.value = true;
});
function appendRecipes(val: Array<Recipe>) {
val.forEach((recipe) => {
recipes.value.push(recipe);
});
}
const infiniteScroll = useThrottleFn(() => {
if (!ready.value) {
return;
}
loading.value = true;
page.value = page.value + 1;
fetchMore(page.value, perPage.value, orderBy, orderDirection);
loading.value = false;
}, 500);
function assignSorted(val: Array<Recipe>) {
recipes.value = val;
}
function removeRecipe(slug: string) {
for (let i = 0; i < recipes?.value?.length; i++) {
@ -56,7 +43,11 @@ export default defineComponent({
}
}
return { recipes, infiniteScroll, loading, removeRecipe };
function replaceRecipes(val: Array<Recipe>) {
recipes.value = val;
}
return { appendRecipes, assignSorted, recipes, removeRecipe, replaceRecipes };
},
head() {
return {

View File

@ -30,7 +30,7 @@ export default defineComponent({
};
},
head: {
title: "Tags",
title: "Categories",
},
});
</script>