diff --git a/src/components/PopularityStatistics.vue b/src/components/PopularityStatistics.vue new file mode 100644 index 0000000..4dccf41 --- /dev/null +++ b/src/components/PopularityStatistics.vue @@ -0,0 +1,56 @@ + + + diff --git a/src/components/__tests__/PopularityStatistics.spec.ts b/src/components/__tests__/PopularityStatistics.spec.ts new file mode 100644 index 0000000..935f3df --- /dev/null +++ b/src/components/__tests__/PopularityStatistics.spec.ts @@ -0,0 +1,88 @@ +import {describe, it, expect, beforeEach} from 'vitest'; +import {mount} from '@vue/test-utils'; +import {DateTime} from 'luxon'; +import {useActivityStore} from '@/stores/activityStore'; +import PopularityStatistics from '@/components/PopularityStatistics.vue'; +import {Category} from '@/utils/types'; + +const thisWeek = DateTime.now().startOf('week'); +const lastWeek = DateTime.now().startOf('week').minus({weeks: 1}); +const twoWeeksAgo = DateTime.now().startOf('week').minus({weeks: 2}); + +describe('PopularityStatistics', () => { + let activityStore: ReturnType; + beforeEach(() => { + activityStore = useActivityStore(); + }); + + it('renders empty', () => { + const wrapper = mount(PopularityStatistics); + expect(wrapper.html()).toMatchSnapshot(); + }); + + it('renders with data', () => { + activityStore.startDate = thisWeek; + activityStore.weeks.push({ + startDate: thisWeek, + veggies: ['apple', 'cucumber', 'romaine', 'potato', 'red bean', 'teff'], + }); + + const wrapper = mount(PopularityStatistics); + expect(wrapper.findByTestId('popularity-Fruit-1').find('.popularity__entry-text').text()).toBe( + 'apple (1)', + ); + expect( + wrapper.findByTestId('popularity-Vegetable-1').find('.popularity__entry-text').text(), + ).toBe('cucumber (1)'); + expect(wrapper.findByTestId('popularity-Leafy-1').find('.popularity__entry-text').text()).toBe( + 'romaine (1)', + ); + expect(wrapper.findByTestId('popularity-Root-1').find('.popularity__entry-text').text()).toBe( + 'potato (1)', + ); + expect(wrapper.findByTestId('popularity-Bean-1').find('.popularity__entry-text').text()).toBe( + 'red bean (1)', + ); + expect(wrapper.findByTestId('popularity-Grain-1').find('.popularity__entry-text').text()).toBe( + 'teff (1)', + ); + + Object.values(Category).forEach((category) => { + expect( + wrapper.findByTestId(`popularity-${category}-2`).find('.popularity__entry-text').text(), + ).toBe('No Entry'); + expect( + wrapper.findByTestId(`popularity-${category}-3`).find('.popularity__entry-text').text(), + ).toBe('No Entry'); + }); + }); + + it('renders entries in correct order', () => { + activityStore.startDate = twoWeeksAgo; + activityStore.weeks.push( + { + startDate: twoWeeksAgo, + veggies: ['apple', 'lychee', 'pineapple'], + }, + { + startDate: lastWeek, + veggies: ['apple', 'lychee', 'longan'], + }, + { + startDate: thisWeek, + veggies: ['apple', 'blueberry', 'cloudberry'], + }, + ); + + const wrapper = mount(PopularityStatistics); + expect(wrapper.findByTestId('popularity-Fruit-1').find('.popularity__entry-text').text()).toBe( + 'apple (3)', + ); + expect(wrapper.findByTestId('popularity-Fruit-2').find('.popularity__entry-text').text()).toBe( + 'lychee (2)', + ); + expect(wrapper.findByTestId('popularity-Fruit-3').find('.popularity__entry-text').text()).toBe( + 'pineapple (1)', + ); + }); +}); diff --git a/src/components/__tests__/__snapshots__/PopularityStatistics.spec.ts.snap b/src/components/__tests__/__snapshots__/PopularityStatistics.spec.ts.snap new file mode 100644 index 0000000..be9a739 --- /dev/null +++ b/src/components/__tests__/__snapshots__/PopularityStatistics.spec.ts.snap @@ -0,0 +1,108 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`PopularityStatistics > renders empty 1`] = ` +"
+
+ +
    +
  • + +
    No Entry
    +
  • +
  • + +
    No Entry
    +
  • +
  • + +
    No Entry
    +
  • +
+
+
+ +
    +
  • + +
    No Entry
    +
  • +
  • + +
    No Entry
    +
  • +
  • + +
    No Entry
    +
  • +
+
+
+ +
    +
  • + +
    No Entry
    +
  • +
  • + +
    No Entry
    +
  • +
  • + +
    No Entry
    +
  • +
+
+
+ +
    +
  • + +
    No Entry
    +
  • +
  • + +
    No Entry
    +
  • +
  • + +
    No Entry
    +
  • +
+
+
+ +
    +
  • + +
    No Entry
    +
  • +
  • + +
    No Entry
    +
  • +
  • + +
    No Entry
    +
  • +
+
+
+ +
    +
  • + +
    No Entry
    +
  • +
  • + +
    No Entry
    +
  • +
  • + +
    No Entry
    +
  • +
+
+
" +`; diff --git a/src/components/__tests__/activityStore.spec.ts b/src/components/__tests__/activityStore.spec.ts index 004f4bf..06d2aef 100644 --- a/src/components/__tests__/activityStore.spec.ts +++ b/src/components/__tests__/activityStore.spec.ts @@ -2,7 +2,9 @@ import {describe, it, expect, beforeEach} from 'vitest'; import {DateTime} from 'luxon'; import {useActivityStore} from '@/stores/activityStore'; import {createPinia, setActivePinia} from 'pinia'; -import type {Week} from '@/utils/types'; +import {Category, type Week} from '@/utils/types'; +import {take} from 'remeda'; +import {BEANS, FRUITS, GRAINS, LEAFIES, ROOTS, VEGETABLES} from '@/utils/constants'; describe('activityStore', () => { const thisWeek = DateTime.now().startOf('week'); @@ -180,7 +182,7 @@ describe('activityStore', () => { expect(activityStore.currentChallenge).toBe('tomato'); }); - it('returns favorites', () => { + it('returns suggestions', () => { activityStore.startDate = threeWeeksAgo; activityStore.weeks.push( { @@ -197,10 +199,10 @@ describe('activityStore', () => { }, ); - expect(activityStore.favorites).toEqual(['wheat', 'apple', 'cucumber']); + expect(activityStore.suggestions).toEqual(['wheat', 'apple', 'cucumber']); }); - it('excludes this week from favorites', () => { + it('excludes this week from suggestions', () => { activityStore.startDate = twoWeeksAgo; activityStore.weeks.push( { @@ -217,10 +219,10 @@ describe('activityStore', () => { }, ); - expect(activityStore.favorites).toEqual(['cucumber']); + expect(activityStore.suggestions).toEqual(['cucumber']); }); - it('returns only ten most common favorites', () => { + it('returns only ten most common suggestions', () => { const expected = [ 'wheat', 'rye', @@ -243,7 +245,7 @@ describe('activityStore', () => { startDate: lastWeek, }); - expect(activityStore.favorites).toEqual(expected); + expect(activityStore.suggestions).toEqual(expected); }); it('returns unique veggies', () => { @@ -408,6 +410,105 @@ describe('activityStore', () => { expect(activityStore.getWeekStarts.length).toBe(6); }); + it('returns category favorites', () => { + activityStore.startDate = twoWeeksAgo; + activityStore.weeks.push( + { + startDate: twoWeeksAgo, + veggies: [ + ...take(FRUITS, 5), + ...take(VEGETABLES, 5), + ...take(LEAFIES, 5), + ...take(ROOTS, 5), + ...take(BEANS, 5), + ...take(GRAINS, 5), + ], + }, + { + startDate: lastWeek, + veggies: [ + ...take(FRUITS, 2), + ...take(VEGETABLES, 2), + ...take(LEAFIES, 2), + ...take(ROOTS, 2), + ...take(BEANS, 2), + ...take(GRAINS, 2), + ], + }, + { + startDate: thisWeek, + veggies: [FRUITS[0], VEGETABLES[0], LEAFIES[0], ROOTS[0], BEANS[0], GRAINS[0]], + }, + ); + + expect(activityStore.categoryFavorites).toEqual([ + [ + Category.Fruit, + [ + [FRUITS[0], 3], + [FRUITS[1], 2], + [FRUITS[2], 1], + ], + ], + [ + Category.Vegetable, + [ + [VEGETABLES[0], 3], + [VEGETABLES[1], 2], + [VEGETABLES[2], 1], + ], + ], + [ + Category.Leafy, + [ + [LEAFIES[0], 3], + [LEAFIES[1], 2], + [LEAFIES[2], 1], + ], + ], + [ + Category.Root, + [ + [ROOTS[0], 3], + [ROOTS[1], 2], + [ROOTS[2], 1], + ], + ], + [ + Category.Bean, + [ + [BEANS[0], 3], + [BEANS[1], 2], + [BEANS[2], 1], + ], + ], + [ + Category.Grain, + [ + [GRAINS[0], 3], + [GRAINS[1], 2], + [GRAINS[2], 1], + ], + ], + ]); + }); + + it('returns empty category favorites', () => { + activityStore.startDate = thisWeek; + activityStore.weeks.push({ + startDate: thisWeek, + veggies: [], + }); + expect(activityStore.categoryFavorites).toEqual([ + [Category.Fruit, []], + [Category.Vegetable, []], + [Category.Leafy, []], + [Category.Root, []], + [Category.Bean, []], + [Category.Grain, []], + ]); + }); + it('resets the store', () => { activityStore.startDate = thisWeek; activityStore.weeks.push({ diff --git a/src/i18n/en.json b/src/i18n/en.json index e2167e7..cb1456f 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -679,6 +679,16 @@ "en": "English", "fi": "Suomi" }, + "popularity": { + "Bean": "Top-3 @:categories.Bean", + "entry": "{0} ({1})", + "Fruit": "Top-3 @:categories.Fruit", + "Grain": "Top-3 @:categories.Grain", + "Leafy": "Top-3 @:categories.Leafy", + "noEntry": "No Entry", + "Root": "Top-3 @:categories.Root", + "Vegetable": "Top-3 @:categories.Vegetable" + }, "qa": { "appPurpose": { "title": "What's this app for?", @@ -734,6 +744,7 @@ "2": "All time", "3": "Achievements", "4": "Veggie list", + "5": "Popularity", "allTimeCategories": "Comparison By Categories", "chosenStats": "Chosen Statistics", "grid1": { diff --git a/src/i18n/fi.json b/src/i18n/fi.json index 39b50c7..5d3e99f 100644 --- a/src/i18n/fi.json +++ b/src/i18n/fi.json @@ -687,6 +687,17 @@ "en": "English", "fi": "Suomi" }, + "popularity": { + "Bean": "Top-3 @:categories.Bean", + "entry": "{0} ({1})", + "Fruit": "Top-3 @:categories.Fruit", + "Grain": "Top-3 @:categories.Grain", + "Leafy": "Top-3 @:categories.Leafy", + "noEntry": "Ei lisätty", + "Root": "Top-3 @:categories.Root", + "top3Veggies": "Top-3 kasviksesi", + "Vegetable": "Top-3 @:categories.Vegetable" + }, "qa": { "appPurpose": { "title": "Mitä sovelluksella tehdään", @@ -742,6 +753,7 @@ "2": "Kokonaistilastot", "3": "Saavutukset", "4": "Kasvislista", + "5": "Suosio", "allTimeCategories": "Vertailu ryhmittäin", "chosenStats": "Valitut tilastot", "grid1": { diff --git a/src/stores/activityStore.ts b/src/stores/activityStore.ts index 5e65be2..2e5eafd 100644 --- a/src/stores/activityStore.ts +++ b/src/stores/activityStore.ts @@ -3,8 +3,8 @@ import {defineStore} from 'pinia'; import {useNow, useStorage} from '@vueuse/core'; import {DateTime} from 'luxon'; import {difference, entries, filter, groupBy, map, pipe, prop, sortBy, take, unique} from 'remeda'; -import type {Challenge, Week} from '@/utils/types'; -import {dateParser, getRandomVeggie} from '@/utils/helpers'; +import {Category, type Challenge, type Week} from '@/utils/types'; +import {dateParser, getCategoryForVeggie, getRandomVeggie} from '@/utils/helpers'; export const useActivityStore = defineStore('activity', () => { const reactiveNow = useNow({interval: 2000}); @@ -113,15 +113,32 @@ export const useActivityStore = defineStore('activity', () => { ?.veggie, ); - const favorites = computed(() => + const suggestions = computed(() => pipe( allVeggies.value, filter((veggie) => !currentVeggies.value.includes(veggie)), groupBy((veggie) => veggie), entries(), sortBy([([, {length}]) => length, 'desc']), - map(prop(0)), take(10), + map(prop(0)), + ), + ); + + const categoryFavorites = computed(() => + Object.values(Category).map( + (category) => + [ + category, + pipe( + allVeggies.value.filter((veggie) => getCategoryForVeggie(veggie) === category), + groupBy((veggie) => veggie), + entries(), + sortBy([([, {length}]) => length, 'desc']), + take(3), + map(([veggie, group]) => [veggie, group.length]), + ), + ] as [Category, [string, number][]], ), ); @@ -165,7 +182,8 @@ export const useActivityStore = defineStore('activity', () => { over30Veggies, atMostVeggies, veggiesForWeek, - favorites, + suggestions, + categoryFavorites, toggleVeggie, $reset, }; diff --git a/src/views/LogView.vue b/src/views/LogView.vue index aa6bcbe..14baeb2 100644 --- a/src/views/LogView.vue +++ b/src/views/LogView.vue @@ -18,7 +18,7 @@ import AchievementBadge from '@/components/AchievementBadge.vue'; const {t, tm} = useI18n(); const activityStore = useActivityStore(); -const {favorites, currentVeggies, currentChallenge, allVeggies, uniqueVeggies} = +const {suggestions, currentVeggies, currentChallenge, allVeggies, uniqueVeggies} = storeToRefs(activityStore); const {toggleVeggie} = activityStore; const {achievements} = storeToRefs(useAppStateStore()); @@ -74,7 +74,7 @@ provide(KEYS.challenge, currentChallenge); {