feat: implement local storage for sorting and dynamic sort icons on the new recipe sort card (#1506)

* added new sort icons

* added dynamic sort icons

* implemented local storage for sorting
and mobile card view

* fixed bug with local storage booleans

* added type hints

* bum vue use to use merge defaults

* use reactive localstorage

* add $vuetify type

* sort returns

* fix type error

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
This commit is contained in:
Michael Genson 2022-07-31 14:39:35 -05:00 committed by GitHub
parent 34f52c06a6
commit 1b83c82997
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 133 additions and 66 deletions

View File

@ -61,7 +61,7 @@
<template #activator="{ on, attrs }"> <template #activator="{ on, attrs }">
<v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" :loading="sortLoading" v-on="on"> <v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" :loading="sortLoading" v-on="on">
<v-icon :left="!$vuetify.breakpoint.xsOnly"> <v-icon :left="!$vuetify.breakpoint.xsOnly">
{{ $globals.icons.sort }} {{ preferences.sortIcon }}
</v-icon> </v-icon>
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.sort") }} {{ $vuetify.breakpoint.xsOnly ? null : $t("general.sort") }}
</v-btn> </v-btn>
@ -102,11 +102,11 @@
event: 'toggle-dense-view', event: 'toggle-dense-view',
}, },
]" ]"
@toggle-dense-view="mobileCards = !mobileCards" @toggle-dense-view="toggleMobileCards()"
/> />
</v-app-bar> </v-app-bar>
<div v-if="recipes" class="mt-2"> <div v-if="recipes" class="mt-2">
<v-row v-if="!viewScale"> <v-row v-if="!useMobileCards">
<v-col v-for="(recipe, index) in recipes" :key="recipe.slug + index" :sm="6" :md="6" :lg="4" :xl="3"> <v-col v-for="(recipe, index) in recipes" :key="recipe.slug + index" :sm="6" :md="6" :lg="4" :xl="3">
<v-lazy> <v-lazy>
<RecipeCard <RecipeCard
@ -174,6 +174,7 @@ import RecipeCardMobile from "./RecipeCardMobile.vue";
import { useAsyncKey } from "~/composables/use-utils"; import { useAsyncKey } from "~/composables/use-utils";
import { useLazyRecipes, useSorter } from "~/composables/recipes"; import { useLazyRecipes, useSorter } from "~/composables/recipes";
import { Recipe } from "~/types/api-types/recipe"; import { Recipe } from "~/types/api-types/recipe";
import { useUserSortPreferences } from "~/composables/use-users/preferences";
const SORT_EVENT = "sort"; const SORT_EVENT = "sort";
const REPLACE_RECIPES_EVENT = "replaceRecipes"; const REPLACE_RECIPES_EVENT = "replaceRecipes";
@ -211,7 +212,8 @@ export default defineComponent({
}, },
}, },
setup(props, context) { setup(props, context) {
const mobileCards = ref(false); const preferences = useUserSortPreferences();
const utils = useSorter(); const utils = useSorter();
const EVENTS = { const EVENTS = {
@ -223,8 +225,8 @@ export default defineComponent({
}; };
const { $globals, $vuetify } = useContext(); const { $globals, $vuetify } = useContext();
const viewScale = computed(() => { const useMobileCards = computed(() => {
return mobileCards.value || $vuetify.breakpoint.smAndDown; return $vuetify.breakpoint.smAndDown || preferences.value.useMobileCards;
}); });
const displayTitleIcon = computed(() => { const displayTitleIcon = computed(() => {
@ -247,18 +249,20 @@ export default defineComponent({
const page = ref(1); const page = ref(1);
const perPage = ref(30); const perPage = ref(30);
const orderBy = ref("name");
const orderDirection = ref("asc");
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 { recipes, fetchMore } = useLazyRecipes(); const { fetchMore } = useLazyRecipes();
onMounted(async () => { onMounted(async () => {
if (props.usePagination) { if (props.usePagination) {
const newRecipes = await fetchMore(page.value, perPage.value, orderBy.value, orderDirection.value); const newRecipes = await fetchMore(
page.value,
perPage.value,
preferences.value.orderBy,
preferences.value.orderDirection
);
context.emit(REPLACE_RECIPES_EVENT, newRecipes); context.emit(REPLACE_RECIPES_EVENT, newRecipes);
ready.value = true; ready.value = true;
} }
@ -273,7 +277,12 @@ export default defineComponent({
loading.value = true; loading.value = true;
page.value = page.value + 1; page.value = page.value + 1;
const newRecipes = await fetchMore(page.value, perPage.value, orderBy.value, orderDirection.value); const newRecipes = await fetchMore(
page.value,
perPage.value,
preferences.value.orderBy,
preferences.value.orderDirection
);
if (!newRecipes.length) { if (!newRecipes.length) {
hasMore.value = false; hasMore.value = false;
} else { } else {
@ -284,51 +293,39 @@ export default defineComponent({
}, useAsyncKey()); }, useAsyncKey());
}, 500); }, 500);
/* /**
sortRecipes helps filter using the API. This will eventually replace the sortRecipesFrontend function which pulls all recipes * 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. * (without pagination) and does the sorting in the frontend.
* TODO: remove sortRecipesFrontend and remove duplicate "sortRecipes" section in the template (above)
TODO: remove sortRecipesFrontend and remove duplicate "sortRecipes" section in the template (above) * @param sortType
TODO: use indicator to show asc / desc order */
*/
function sortRecipes(sortType: string) { function sortRecipes(sortType: string) {
if (state.sortLoading || loading.value) { if (state.sortLoading || loading.value) {
return; return;
} }
function setter(orderBy: string, ascIcon: string, descIcon: string) {
if (preferences.value.orderBy !== orderBy) {
preferences.value.orderBy = orderBy;
preferences.value.orderDirection = "asc";
} else {
preferences.value.orderDirection = preferences.value.orderDirection === "asc" ? "desc" : "asc";
}
preferences.value.sortIcon = preferences.value.orderDirection === "asc" ? ascIcon : descIcon;
}
switch (sortType) { switch (sortType) {
case EVENTS.az: case EVENTS.az:
if (orderBy.value !== "name") { setter("name", $globals.icons.sortAlphabeticalAscending, $globals.icons.sortAlphabeticalDescending);
orderBy.value = "name";
orderDirection.value = "asc";
} else {
orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc";
}
break; break;
case EVENTS.rating: case EVENTS.rating:
if (orderBy.value !== "rating") { setter("rating", $globals.icons.sortAscending, $globals.icons.sortDescending);
orderBy.value = "rating";
orderDirection.value = "desc";
} else {
orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc";
}
break; break;
case EVENTS.created: case EVENTS.created:
if (orderBy.value !== "created_at") { setter("created_at", $globals.icons.sortCalendarAscending, $globals.icons.sortCalendarDescending);
orderBy.value = "created_at";
orderDirection.value = "desc";
} else {
orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc";
}
break; break;
case EVENTS.updated: case EVENTS.updated:
if (orderBy.value !== "update_at") { setter("updated_at", $globals.icons.sortClockAscending, $globals.icons.sortClockDescending);
orderBy.value = "update_at";
orderDirection.value = "desc";
} else {
orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc";
}
break; break;
default: default:
console.log("Unknown Event", sortType); console.log("Unknown Event", sortType);
@ -344,7 +341,12 @@ export default defineComponent({
loading.value = true; loading.value = true;
// fetch new recipes // fetch new recipes
const newRecipes = await fetchMore(page.value, perPage.value, orderBy.value, orderDirection.value); const newRecipes = await fetchMore(
page.value,
perPage.value,
preferences.value.orderBy,
preferences.value.orderDirection
);
context.emit(REPLACE_RECIPES_EVENT, newRecipes); context.emit(REPLACE_RECIPES_EVENT, newRecipes);
state.sortLoading = false; state.sortLoading = false;
@ -379,17 +381,22 @@ export default defineComponent({
state.sortLoading = false; state.sortLoading = false;
} }
function toggleMobileCards() {
preferences.value.useMobileCards = !preferences.value.useMobileCards;
}
return { return {
mobileCards,
...toRefs(state), ...toRefs(state),
EVENTS,
viewScale,
displayTitleIcon, displayTitleIcon,
EVENTS,
infiniteScroll, infiniteScroll,
loading, loading,
navigateRandom, navigateRandom,
preferences,
sortRecipes, sortRecipes,
sortRecipesFrontend, sortRecipesFrontend,
toggleMobileCards,
useMobileCards,
}; };
}, },
}); });

View File

@ -0,0 +1,28 @@
import { Ref, useContext } from "@nuxtjs/composition-api";
import { useLocalStorage } from "@vueuse/core";
export interface UserRecipePreferences {
orderBy: string;
orderDirection: string;
sortIcon: string;
useMobileCards: boolean;
}
export function useUserSortPreferences(): Ref<UserRecipePreferences> {
const { $globals } = useContext();
const fromStorage = useLocalStorage(
"recipe-section-preferences",
{
orderBy: "name",
orderDirection: "asc",
sortIcon: $globals.icons.sortAlphabeticalAscending,
useMobileCards: false,
},
{ mergeDefaults: true }
// we cast to a Ref because by default it will return an optional type ref
// but since we pass defaults we know all properties are set.
) as Ref<UserRecipePreferences>;
return fromStorage;
}

View File

@ -22,7 +22,7 @@
"@nuxtjs/proxy": "^2.1.0", "@nuxtjs/proxy": "^2.1.0",
"@nuxtjs/pwa": "^3.3.5", "@nuxtjs/pwa": "^3.3.5",
"@vue/composition-api": "^1.6.2", "@vue/composition-api": "^1.6.2",
"@vueuse/core": "^8.5.0", "@vueuse/core": "^9.0.2",
"core-js": "^3.23.1", "core-js": "^3.23.1",
"date-fns": "^2.28.0", "date-fns": "^2.28.0",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",

View File

@ -1,4 +1,5 @@
import { Plugin } from "@nuxt/types" import { Plugin } from "@nuxt/types";
import { Framework } from "vuetify";
import { icons } from "~/utils/icons"; import { icons } from "~/utils/icons";
import { Icon } from "~/utils/icons/icon-type"; import { Icon } from "~/utils/icons/icon-type";
@ -15,13 +16,14 @@ declare module "vue/types/vue" {
declare module "@nuxt/types" { declare module "@nuxt/types" {
interface Context { interface Context {
$globals: Globals; $globals: Globals;
$vuetify: Framework;
} }
} }
const globalsPlugin: Plugin = (_, inject) => { const globalsPlugin: Plugin = (_, inject) => {
inject("globals", { inject("globals", {
icons icons,
}); });
}; };
export default globalsPlugin export default globalsPlugin;

View File

@ -78,6 +78,14 @@ export interface Icon {
shareVariant: string; shareVariant: string;
shuffleVariant: string; shuffleVariant: string;
sort: string; sort: string;
sortAscending: string;
sortDescending: string;
sortAlphabeticalAscending: string;
sortAlphabeticalDescending: string;
sortCalendarAscending: string;
sortCalendarDescending: string;
sortClockAscending: string;
sortClockDescending: string;
star: string; star: string;
testTube: string; testTube: string;
tools: string; tools: string;

View File

@ -45,6 +45,14 @@ import {
mdiCodeJson, mdiCodeJson,
mdiCog, mdiCog,
mdiSort, mdiSort,
mdiSortAscending,
mdiSortDescending,
mdiSortAlphabeticalAscending,
mdiSortAlphabeticalDescending,
mdiSortCalendarAscending,
mdiSortCalendarDescending,
mdiSortClockAscending,
mdiSortClockDescending,
mdiOrderAlphabeticalAscending, mdiOrderAlphabeticalAscending,
mdiStar, mdiStar,
mdiNewBox, mdiNewBox,
@ -194,6 +202,14 @@ export const icons = {
shareVariant: mdiShareVariant, shareVariant: mdiShareVariant,
shuffleVariant: mdiShuffleVariant, shuffleVariant: mdiShuffleVariant,
sort: mdiSort, sort: mdiSort,
sortAscending: mdiSortAscending,
sortDescending: mdiSortDescending,
sortAlphabeticalAscending: mdiSortAlphabeticalAscending,
sortAlphabeticalDescending: mdiSortAlphabeticalDescending,
sortCalendarAscending: mdiSortCalendarAscending,
sortCalendarDescending: mdiSortCalendarDescending,
sortClockAscending: mdiSortClockAscending,
sortClockDescending: mdiSortClockDescending,
star: mdiStar, star: mdiStar,
testTube: mdiTestTube, testTube: mdiTestTube,
tools: mdiTools, tools: mdiTools,

View File

@ -2227,6 +2227,11 @@
dependencies: dependencies:
source-map "^0.6.1" source-map "^0.6.1"
"@types/web-bluetooth@^0.0.15":
version "0.0.15"
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.15.tgz#d60330046a6ed8a13b4a53df3813c44942ebdf72"
integrity sha512-w7hEHXnPMEZ+4nGKl/KDRVpxkwYxYExuHOYXyzIzCDzEZ9ZCGMAewulr9IqJu2LR4N37fcnb1XVeuZ09qgOxhA==
"@types/webpack-bundle-analyzer@3.9.3": "@types/webpack-bundle-analyzer@3.9.3":
version "3.9.3" version "3.9.3"
resolved "https://registry.yarnpkg.com/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.9.3.tgz#3a12025eb5d86069c30b47a157e62c0aca6e39a1" resolved "https://registry.yarnpkg.com/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.9.3.tgz#3a12025eb5d86069c30b47a157e62c0aca6e39a1"
@ -2550,24 +2555,25 @@
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.37.tgz#8e6adc3f2759af52f0e85863dfb0b711ecc5c702" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.37.tgz#8e6adc3f2759af52f0e85863dfb0b711ecc5c702"
integrity sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw== integrity sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==
"@vueuse/core@^8.5.0": "@vueuse/core@^9.0.2":
version "8.5.0" version "9.0.2"
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-8.5.0.tgz#2b7548e52165c88e1463756c36188e105d806543" resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-9.0.2.tgz#e6e59c45b43c1ffe4ad5149853a1ea4e8f1e3b12"
integrity sha512-VEJ6sGNsPlUp0o9BGda2YISvDZbhWJSOJu5zlp2TufRGVrLcYUKr31jyFEOj6RXzG3k/H4aCYeZyjpItfU8glw== integrity sha512-kOIqaQPSs7OSByWg1ulEKRUJbsq3FmbJiUr0RhEKpt3O1Uhl4DrDj85DUbQBABVYgPvSaY6AE/fP3/FOcRIOoQ==
dependencies: dependencies:
"@vueuse/metadata" "8.5.0" "@types/web-bluetooth" "^0.0.15"
"@vueuse/shared" "8.5.0" "@vueuse/metadata" "9.0.2"
"@vueuse/shared" "9.0.2"
vue-demi "*" vue-demi "*"
"@vueuse/metadata@8.5.0": "@vueuse/metadata@9.0.2":
version "8.5.0" version "9.0.2"
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-8.5.0.tgz#1aaa3787922cfda0f38243aaa7779366a669b4db" resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-9.0.2.tgz#373fdeb552f2a002ddc1f36c4027c936d78f8cb5"
integrity sha512-WxsD+Cd+bn+HcjpY6Dl9FJ8ywTRTT9pTwk3bCQpzEhXVYAyNczKDSahk50fCfIJKeWHhyI4B2+/ZEOxQAkUr0g== integrity sha512-TRh+TNUYXiodatSAxd0xZc7sh4RfktVVgNFIN7TCQXKyancbCAcWfHvKfgdlX8LcqSBxKoHVa90n0XdUbboTkw==
"@vueuse/shared@8.5.0": "@vueuse/shared@9.0.2":
version "8.5.0" version "9.0.2"
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-8.5.0.tgz#fa01ecd3161933f521dd6428b9ef8015ded1bbd3" resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-9.0.2.tgz#d3fb03594a9482d264702c67efe71d691ce67084"
integrity sha512-qKG+SZb44VvGD4dU5cQ63z4JE2Yk39hQUecR0a9sEdJA01cx+XrxAvFKJfPooxwoiqalAVw/ktWK6xbyc/jS3g== integrity sha512-KwBDefK2ljLESpt0ffe2w8EGUCb3IaMfTzeytB/uHHjHOGOEIHLHHyn8W2C48uGQEvoe5iwaW4Bfp8cRUM6IFA==
dependencies: dependencies:
vue-demi "*" vue-demi "*"