Skip to content

Commit

Permalink
Optimize veggie search and badges
Browse files Browse the repository at this point in the history
  • Loading branch information
ajuvonen committed Sep 6, 2024
1 parent 0772c8b commit e09ea89
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 139 deletions.
20 changes: 12 additions & 8 deletions src/components/AchievementBadge.vue
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,14 @@ const achievementProps: AchievementProps = {
class="badge"
role="img"
>
<div class="badge__background" :class="[`badge__background--${AchievementLevel[level]}`]"></div>
<div aria-hidden="true" class="badge__emoji">
{{ achievementProps[achievement][level]!.emoji }}
<div
class="badge__background"
:class="[`badge__background--${AchievementLevel[level]}`]"
aria-hidden="true"
>
<div class="badge__emoji">
{{ achievementProps[achievement][level]!.emoji }}
</div>
</div>
<div aria-hidden="true" class="badge__text">
{{
Expand All @@ -127,8 +132,7 @@ const achievementProps: AchievementProps = {
</template>
<style lang="scss" scoped>
.badge {
@apply select-none aspect-square;
@apply flex items-center justify-center;
@apply relative select-none aspect-square self-center;
filter: drop-shadow(0px 0px 3px rgba(0, 0, 0, 0.3));
flex: 0 0 calc(33% - 5px);
Expand All @@ -138,13 +142,13 @@ const achievementProps: AchievementProps = {
}
.badge__background {
@apply relative w-full h-full rounded-full border-4;
@apply relative w-full h-full rounded-full border-4 text-[17cqmin] sm:text-[14cqmin];
@apply flex items-center justify-center;
box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.3);
text-shadow: 1px 1px 1px #334155;
}
.badge__emoji {
@apply absolute text-[17cqmin] sm:text-[14cqmin] leading-[1.3];
text-shadow: 1px 1px 1px #334155;
mix-blend-mode: luminosity;
}
Expand Down
45 changes: 18 additions & 27 deletions src/components/VeggieSearch.vue
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
<script setup lang="ts">
import {ref, computed, onMounted, watch} from 'vue';
import {ref, computed, onMounted} from 'vue';
import {useI18n} from 'vue-i18n';
import {difference, first} from 'remeda';
import {
Combobox,
ComboboxInput,
ComboboxButton,
ComboboxOptions,
TransitionRoot,
} from '@headlessui/vue';
import {useElementBounding, useWindowSize} from '@vueuse/core';
import {useElementBounding, useMemoize, useWindowSize} from '@vueuse/core';
import {ALL_VEGGIES} from '@/utils/constants';
import {Category, type TranslatedListing} from '@/utils/types';
import VeggieSearchGroup from '@/components/VeggieSearchGroup.vue';
import {getCategoryForVeggie} from '@/utils/helpers';
const emit = defineEmits(['toggle']);
const props = defineProps<{
selected: string[];
}>();
const model = defineModel<string[]>({
required: true,
});
const {t, locale} = useI18n();
const selected = ref(props.selected);
const query = ref('');
const input = ref<InstanceType<typeof ComboboxInput> | null>(null);
Expand All @@ -34,7 +31,7 @@ onMounted(() => {
input.value?.$el.focus();
});
const allVeggies = computed(() => {
const allVeggies = useMemoize(() => {
const collator = new Intl.Collator(locale.value);
return ALL_VEGGIES.map<TranslatedListing>((veggie) => ({
veggie,
Expand All @@ -43,9 +40,9 @@ const allVeggies = computed(() => {
})).sort((a, b) => collator.compare(a.translation, b.translation));
});
const filteredveggies = computed(
() => (category?: Category) =>
allVeggies.value.filter(
const filteredVeggies = useMemoize(
(category?: Category) =>
allVeggies().filter(
(item) =>
(!category || item.category === category) &&
(!query.value ||
Expand All @@ -54,26 +51,23 @@ const filteredveggies = computed(
.replace(/\s+/g, '')
.includes(query.value.toLowerCase().replace(/\s+/g, ''))),
),
{
getKey: (category?: Category) => `${category}_${query.value}`,
},
);
watch(selected, (newValue, oldValue) => {
const added = difference(newValue, oldValue);
const deleted = difference(oldValue, newValue);
emit('toggle', first(added.concat(deleted)));
});
const getAvailableHeightForOptions = computed(
() => `max-height: calc(${height.value}px - ${top.value}px - 1rem)`,
);
</script>
<template>
<Combobox nullable multiple v-model="selected" as="div" class="relative h-12 z-10">
<Combobox nullable multiple v-model="model" as="div" class="relative h-12 z-10">
<ComboboxInput
ref="input"
class="veggie-search__input"
@change="query = $event.target.value"
:aria-label="$t('veggieSearch.search')"
:placeholder="$t('veggieSearch.search')"
class="veggie-search__input"
@change="query = $event.target.value"
/>
<ComboboxButton class="veggie-search__button">
<IconComponent icon="chevron" aria-hidden="true" />
Expand All @@ -89,17 +83,14 @@ const getAvailableHeightForOptions = computed(
:style="getAvailableHeightForOptions"
class="veggie-search__options"
>
<div
v-if="filteredveggies().length === 0 && query !== ''"
class="veggie-search__no-results"
>
<li v-if="filteredVeggies().length === 0 && query !== ''" class="veggie-search__no-results">
{{ $t('veggieSearch.noResults') }}
</div>
</li>
<VeggieSearchGroup
v-for="category in Category"
:key="category"
:category="category"
:items="filteredveggies(category)"
:items="filteredVeggies(category)"
/>
</ComboboxOptions>
</TransitionRoot>
Expand Down
48 changes: 26 additions & 22 deletions src/components/VeggieSearchGroup.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<script setup lang="ts">
import {useMemoize} from '@vueuse/core';
import {ComboboxOption} from '@headlessui/vue';
import type {Category, TranslatedListing} from '@/utils/types';
import {CATEGORY_EMOJI} from '@/utils/constants';
const props = defineProps<{
defineProps<{
items: TranslatedListing[];
category: Category;
}>();
const getOptionClasses = (active: boolean, selected: boolean) => {
const getOptionClasses = useMemoize((active: boolean, selected: boolean) => {
const textClass = active ? 'text-slate-50' : 'text-slate-900 fill-slate-900';
let bgClass = `bg-slate-50`;
if (active) {
Expand All @@ -18,33 +19,36 @@ const getOptionClasses = (active: boolean, selected: boolean) => {
}
return `${textClass} ${bgClass}`;
};
});
</script>

<template>
<template v-if="items.length">
<li class="veggie-search__group">
<span aria-hidden="true">{{ CATEGORY_EMOJI[props.category] }}</span>
<li v-if="items.length">
<div :id="`veggie-search-heading-${category}`" class="veggie-search__heading">
<span aria-hidden="true">{{ CATEGORY_EMOJI[category] }}</span>
<span>{{ $t(`categories.${category}`) }} ({{ items.length }})</span>
</li>
<ComboboxOption
v-for="{veggie, translation} in items"
:key="veggie"
:value="veggie"
v-slot="{active, selected}"
>
<div :class="[getOptionClasses(active, selected), 'veggie-search__option']">
<span>
{{ translation }}
</span>
<IconComponent v-if="selected" icon="check" />
</div>
</ComboboxOption>
</template>
</div>
<ul role="group" :aria-labelledby="`veggie-search-heading-${category}`">
<ComboboxOption
v-for="{veggie, translation} in items"
v-slot="{active, selected}"
as="template"
:key="veggie"
:value="veggie"
>
<li :class="[getOptionClasses(active, selected), 'veggie-search__option']" role="menuitem">
<span>
{{ translation }}
</span>
<IconComponent v-if="selected" icon="check" />
</li>
</ComboboxOption>
</ul>
</li>
</template>

<style scoped lang="scss">
.veggie-search__group {
.veggie-search__heading {
@apply flex-container justify-start;
@apply select-none p-2;
@apply bg-slate-300 text-slate-900;
Expand Down
16 changes: 8 additions & 8 deletions src/components/__tests__/VeggieSearch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe('VeggieSearch', () => {
it('renders', () => {
const wrapper = mount(VeggieSearch, {
props: {
selected: [],
modelValue: [],
},
});
expect(wrapper).toBeTruthy();
Expand All @@ -16,32 +16,32 @@ describe('VeggieSearch', () => {
it('filters veggies', async () => {
const wrapper = mount(VeggieSearch, {
props: {
selected: [],
modelValue: [],
},
});
const input = wrapper.find('.veggie-search__input');
await input.setValue('tomato');
expect(wrapper.find('.veggie-search__options').isVisible()).toBe(true);
expect(wrapper.findAll('.veggie-search__group').length).toBe(1);
expect(wrapper.findAll('.veggie-search__heading').length).toBe(1);
expect(wrapper.find('.veggie-search__option').text()).toContain('tomato');
});

it('shows all categories with matches', async () => {
const wrapper = mount(VeggieSearch, {
props: {
selected: [],
modelValue: [],
},
});
const input = wrapper.find('.veggie-search__input');
await input.setValue('bar');
expect(wrapper.findAll('.veggie-search__option').length).toBe(2);
expect(wrapper.findAll('.veggie-search__group').length).toBe(2);
expect(wrapper.findAll('.veggie-search__heading').length).toBe(2);
});

it('displays no results', async () => {
const wrapper = mount(VeggieSearch, {
props: {
selected: [],
modelValue: [],
},
});
const input = wrapper.find('.veggie-search__input');
Expand All @@ -53,7 +53,7 @@ describe('VeggieSearch', () => {
it('shows selection', async () => {
const wrapper = mount(VeggieSearch, {
props: {
selected: ['tomato'],
modelValue: ['tomato'],
},
});
const input = wrapper.find('.veggie-search__input');
Expand All @@ -67,7 +67,7 @@ describe('VeggieSearch', () => {
it('shows list on button click', async () => {
const wrapper = mount(VeggieSearch, {
props: {
selected: [],
modelValue: [],
},
});
await wrapper.find('.veggie-search__button').trigger('click');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@

exports[`AchievementBadge > renders active 1`] = `
"<div data-v-06381b3c="" aria-disabled="false" title="150 unique veggies" aria-label="150 unique veggies" class="badge" role="img">
<div data-v-06381b3c="" class="badge__background badge__background--Gold"></div>
<div data-v-06381b3c="" aria-hidden="true" class="badge__emoji">🦅</div>
<div data-v-06381b3c="" class="badge__background badge__background--Gold" aria-hidden="true">
<div data-v-06381b3c="" class="badge__emoji">🦅</div>
</div>
<div data-v-06381b3c="" aria-hidden="true" class="badge__text">150 veggies</div>
</div>"
`;

exports[`AchievementBadge > renders inactive 1`] = `
"<div data-v-06381b3c="" aria-disabled="true" title="10 week streak of over 30 veggies" aria-label="10 week streak of over 30 veggies" class="badge" role="img">
<div data-v-06381b3c="" class="badge__background badge__background--Silver"></div>
<div data-v-06381b3c="" aria-hidden="true" class="badge__emoji">🔥</div>
<div data-v-06381b3c="" class="badge__background badge__background--Silver" aria-hidden="true">
<div data-v-06381b3c="" class="badge__emoji">🔥</div>
</div>
<div data-v-06381b3c="" aria-hidden="true" class="badge__text">10 Weeks</div>
</div>"
`;
Loading

0 comments on commit e09ea89

Please sign in to comment.