diff --git a/src/components/AchievementBadge.vue b/src/components/AchievementBadge.vue index bd725b9..fc3e6a0 100644 --- a/src/components/AchievementBadge.vue +++ b/src/components/AchievementBadge.vue @@ -22,6 +22,20 @@ type AchievementProps = Record< >; const achievementProps: AchievementProps = { + challengeAccepted: { + [AchievementLevel.Bronze]: { + textProps: [5], + emoji: 'πŸ›΄', + }, + [AchievementLevel.Silver]: { + textProps: [10], + emoji: 'πŸ›΅', + }, + [AchievementLevel.Gold]: { + textProps: [20], + emoji: 'πŸš€', + }, + }, committed: { [AchievementLevel.Bronze]: { textProps: [3], @@ -43,7 +57,7 @@ const achievementProps: AchievementProps = { }, [AchievementLevel.Silver]: { textProps: [80], - emoji: 'πŸ₯', + emoji: '🐧', }, [AchievementLevel.Gold]: { textProps: [150], diff --git a/src/components/AchievementList.vue b/src/components/AchievementList.vue index 3fc76a4..a845a25 100644 --- a/src/components/AchievementList.vue +++ b/src/components/AchievementList.vue @@ -28,6 +28,26 @@ const {achievements} = storeToRefs(useAppStateStore()); /> +
+

{{ $t('achievements.challengeAccepted.title') }}

+
+ + + +
+

{{ $t('achievements.committed.title') }}

diff --git a/src/components/__tests__/AchievementList.spec.ts b/src/components/__tests__/AchievementList.spec.ts index 5ee79aa..12cbde4 100644 --- a/src/components/__tests__/AchievementList.spec.ts +++ b/src/components/__tests__/AchievementList.spec.ts @@ -16,7 +16,7 @@ describe('AchievementList', () => { it('renders', () => { const wrapper = mount(AchievementList); expect(wrapper.html()).toMatchSnapshot(); - expect(wrapper.findAll('.badge[aria-disabled="true"]').length).toBe(15); + expect(wrapper.findAll('.badge[aria-disabled="true"]').length).toBe(18); }); it('renders with badges enabled', () => { @@ -36,6 +36,6 @@ describe('AchievementList', () => { const wrapper = mount(AchievementList); expect(wrapper.html()).toMatchSnapshot(); - expect(wrapper.findAll('.badge[aria-disabled="true"]').length).toBe(7); + expect(wrapper.findAll('.badge[aria-disabled="true"]').length).toBe(10); }); }); diff --git a/src/components/__tests__/__snapshots__/AchievementList.spec.ts.snap b/src/components/__tests__/__snapshots__/AchievementList.spec.ts.snap index 29b2acc..2451f58 100644 --- a/src/components/__tests__/__snapshots__/AchievementList.spec.ts.snap +++ b/src/components/__tests__/__snapshots__/AchievementList.spec.ts.snap @@ -13,7 +13,7 @@ exports[`AchievementList > renders 1`] = `
@@ -25,6 +25,29 @@ exports[`AchievementList > renders 1`] = `
+
+

Challenge Accepted

+
+ + + +
+

The Committed

@@ -128,7 +151,7 @@ exports[`AchievementList > renders with badges enabled 1`] = `
@@ -140,6 +163,29 @@ exports[`AchievementList > renders with badges enabled 1`] = `
+
+

Challenge Accepted

+
+ + + +
+

The Committed

diff --git a/src/components/__tests__/achievements.spec.ts b/src/components/__tests__/achievements.spec.ts index 8d60ee3..0531148 100644 --- a/src/components/__tests__/achievements.spec.ts +++ b/src/components/__tests__/achievements.spec.ts @@ -17,6 +17,7 @@ const withSetup = (hook: () => T) => }); const defaultAchievements: Achievements = { + challengeAccepted: AchievementLevel.NoAchievement, committed: AchievementLevel.NoAchievement, completionist: AchievementLevel.NoAchievement, hotStreak: AchievementLevel.NoAchievement, @@ -36,110 +37,123 @@ describe('achievements', () => { it('advances completionist', async () => { const {advanceAchievements, achievements} = await withSetup(useAchievements); - advanceAchievements([...Array(39)], 0, 0); + advanceAchievements([...Array(39)], 0, 0, 0); expect(achievements.value.completionist).toEqual(AchievementLevel.NoAchievement); - advanceAchievements([...Array(40)], 0, 0); + advanceAchievements([...Array(40)], 0, 0, 0); expect(achievements.value.completionist).toBe(AchievementLevel.Bronze); - advanceAchievements([...Array(80)], 0, 0); + advanceAchievements([...Array(80)], 0, 0, 0); expect(achievements.value.completionist).toBe(AchievementLevel.Silver); - advanceAchievements([...Array(150)], 0, 0); + advanceAchievements([...Array(150)], 0, 0, 0); expect(achievements.value.completionist).toBe(AchievementLevel.Gold); }); it('goes straight to silver', async () => { const {advanceAchievements, achievements} = await withSetup(useAchievements); - advanceAchievements([...Array(80)], 0, 0); + advanceAchievements([...Array(80)], 0, 0, 0); expect(achievements.value.completionist).toBe(AchievementLevel.Silver); }); it('goes straight to gold', async () => { const {advanceAchievements, achievements} = await withSetup(useAchievements); - advanceAchievements([...Array(150)], 0, 0); + advanceAchievements([...Array(150)], 0, 0, 0); expect(achievements.value.completionist).toBe(AchievementLevel.Gold); }); it('advances experimenterFruit', async () => { const {advanceAchievements, achievements} = await withSetup(useAchievements); - advanceAchievements(take(FRUITS, 14), 0, 0); + advanceAchievements(take(FRUITS, 14), 0, 0, 0); expect(achievements.value.experimenterFruit).toEqual(AchievementLevel.NoAchievement); - advanceAchievements(take(FRUITS, 15), 0, 0); + advanceAchievements(take(FRUITS, 15), 0, 0, 0); expect(achievements.value.experimenterFruit).toBe(AchievementLevel.Gold); }); it('goes straight to gold', async () => { const {advanceAchievements, achievements} = await withSetup(useAchievements); - advanceAchievements(take(FRUITS, 15), 0, 0); + advanceAchievements(take(FRUITS, 15), 0, 0, 0); expect(achievements.value.experimenterFruit).toBe(AchievementLevel.Gold); }); it('advances experimenterVegetable', async () => { const {advanceAchievements, achievements} = await withSetup(useAchievements); - advanceAchievements(take(VEGETABLES, 14), 0, 0); + advanceAchievements(take(VEGETABLES, 14), 0, 0, 0); expect(achievements.value.experimenterVegetable).toEqual(AchievementLevel.NoAchievement); - advanceAchievements(take(VEGETABLES, 15), 0, 0); + advanceAchievements(take(VEGETABLES, 15), 0, 0, 0); expect(achievements.value.experimenterVegetable).toBe(AchievementLevel.Gold); }); it('advances experimenterLeafy', async () => { const {advanceAchievements, achievements} = await withSetup(useAchievements); - advanceAchievements(take(LEAFIES, 14), 0, 0); + advanceAchievements(take(LEAFIES, 14), 0, 0, 0); expect(achievements.value.experimenterLeafy).toEqual(AchievementLevel.NoAchievement); - advanceAchievements(take(LEAFIES, 15), 0, 0); + advanceAchievements(take(LEAFIES, 15), 0, 0, 0); expect(achievements.value.experimenterLeafy).toBe(AchievementLevel.Gold); }); it('advances experimenterBean', async () => { const {advanceAchievements, achievements} = await withSetup(useAchievements); - advanceAchievements(take(BEANS, 14), 0, 0); + advanceAchievements(take(BEANS, 14), 0, 0, 0); expect(achievements.value.experimenterBean).toEqual(AchievementLevel.NoAchievement); - advanceAchievements(take(BEANS, 15), 0, 0); + advanceAchievements(take(BEANS, 15), 0, 0, 0); expect(achievements.value.experimenterBean).toBe(AchievementLevel.Gold); }); it('advances experimenterRoot', async () => { const {advanceAchievements, achievements} = await withSetup(useAchievements); - advanceAchievements(take(ROOTS, 14), 0, 0); + advanceAchievements(take(ROOTS, 14), 0, 0, 0); expect(achievements.value.experimenterRoot).toEqual(AchievementLevel.NoAchievement); - advanceAchievements(take(ROOTS, 15), 0, 0); + advanceAchievements(take(ROOTS, 15), 0, 0, 0); expect(achievements.value.experimenterRoot).toBe(AchievementLevel.Gold); }); it('advances experimenterGrain', async () => { const {advanceAchievements, achievements} = await withSetup(useAchievements); - advanceAchievements(take(GRAINS, 14), 0, 0); + advanceAchievements(take(GRAINS, 14), 0, 0, 0); expect(achievements.value.experimenterGrain).toEqual(AchievementLevel.NoAchievement); - advanceAchievements(take(GRAINS, 15), 0, 0); + advanceAchievements(take(GRAINS, 15), 0, 0, 0); expect(achievements.value.experimenterGrain).toBe(AchievementLevel.Gold); }); it('advances hot streak', async () => { const {advanceAchievements, achievements} = await withSetup(useAchievements); - advanceAchievements([], 4, 0); + advanceAchievements([], 4, 0, 0); expect(achievements.value.hotStreak).toEqual(AchievementLevel.NoAchievement); - advanceAchievements([], 5, 0); + advanceAchievements([], 5, 0, 0); expect(achievements.value.hotStreak).toEqual(AchievementLevel.Bronze); - advanceAchievements([], 10, 0); + advanceAchievements([], 10, 0, 0); expect(achievements.value.hotStreak).toEqual(AchievementLevel.Silver); - advanceAchievements([], 20, 0); + advanceAchievements([], 20, 0, 0); expect(achievements.value.hotStreak).toEqual(AchievementLevel.Gold); }); it('advances committed', async () => { const {advanceAchievements, achievements} = await withSetup(useAchievements); - advanceAchievements([], 0, 11); + advanceAchievements([], 0, 11, 0); expect(achievements.value.committed).toEqual(AchievementLevel.NoAchievement); - advanceAchievements([], 0, 12); + advanceAchievements([], 0, 12, 0); expect(achievements.value.committed).toEqual(AchievementLevel.Bronze); - advanceAchievements([], 0, 26); + advanceAchievements([], 0, 26, 0); expect(achievements.value.committed).toEqual(AchievementLevel.Silver); - advanceAchievements([], 0, 52); + advanceAchievements([], 0, 52, 0); expect(achievements.value.committed).toEqual(AchievementLevel.Gold); }); + it('advances challengeAccepted', async () => { + const {advanceAchievements, achievements} = await withSetup(useAchievements); + advanceAchievements([], 0, 0, 4); + expect(achievements.value.challengeAccepted).toEqual(AchievementLevel.NoAchievement); + advanceAchievements([], 0, 0, 5); + expect(achievements.value.challengeAccepted).toEqual(AchievementLevel.Bronze); + advanceAchievements([], 0, 0, 10); + expect(achievements.value.challengeAccepted).toEqual(AchievementLevel.Silver); + advanceAchievements([], 0, 0, 20); + expect(achievements.value.challengeAccepted).toEqual(AchievementLevel.Gold); + }); + it('resets achievements', async () => { const {advanceAchievements, achievements, resetAchievements} = await withSetup(useAchievements); - advanceAchievements(ALL_VEGGIES, 30, 52); + advanceAchievements(ALL_VEGGIES, 30, 52, 20); expect(achievements.value).toEqual({ + challengeAccepted: 3, committed: 3, completionist: 3, experimenterBean: 3, diff --git a/src/components/__tests__/activityStore.spec.ts b/src/components/__tests__/activityStore.spec.ts index 5e812c0..b92fc47 100644 --- a/src/components/__tests__/activityStore.spec.ts +++ b/src/components/__tests__/activityStore.spec.ts @@ -81,6 +81,40 @@ describe('activityStore', () => { expect(activityStore.currentVeggies).toEqual(['cucumber', 'tomato']); }); + it('returns completed challenges', () => { + activityStore.startDate = twoWeeksAgo; + activityStore.weeks.push( + { + startDate: twoWeeksAgo, + veggies: ['cucumber'], + }, + { + startDate: lastWeek, + veggies: ['wheat', 'rye', 'strawberry'], + }, + { + startDate: thisWeek, + veggies: ['rice', 'leek'], + }, + ); + activityStore.challenges.push( + { + startDate: twoWeeksAgo, + veggie: 'cucumber', + }, + { + startDate: lastWeek, + veggie: 'leek', + }, + { + startDate: thisWeek, + veggie: 'rice', + }, + ); + + expect(activityStore.completedChallenges).toEqual(2); + }); + // it("sets this week's veggies", () => { // activityStore.startDate = lastWeek; // activityStore.weeks.push( diff --git a/src/hooks/achievements.ts b/src/hooks/achievements.ts index 94d292b..a151dd3 100644 --- a/src/hooks/achievements.ts +++ b/src/hooks/achievements.ts @@ -8,6 +8,7 @@ import type {Achievements} from '@/utils/types'; type AdvanceEvent = { type: 'ADVANCE'; uniqueVeggies: string[]; + completedChallenges: number; hotStreakLength: number; totalWeeks: number; }; @@ -17,6 +18,10 @@ type ResetEvent = { }; const guards = { + challengeAccepted: + (threshold: number) => + ({event}: GuardArgs) => + event.completedChallenges >= threshold, committed: (threshold: number) => ({event}: GuardArgs) => @@ -46,6 +51,9 @@ export function useAchievements() { type: 'parallel', on: { RESET: [ + { + target: '.challengeAccepted.0', + }, { target: `.committed.0`, }, @@ -73,6 +81,52 @@ export function useAchievements() { ], }, states: { + challengeAccepted: { + initial: '0', + states: { + '0': { + on: { + ADVANCE: [ + { + target: '3', + guard: guards.challengeAccepted(20), + }, + { + target: '2', + guard: guards.challengeAccepted(10), + }, + { + target: '1', + guard: guards.challengeAccepted(5), + }, + ], + }, + }, + '1': { + on: { + ADVANCE: [ + { + target: '3', + guard: guards.challengeAccepted(20), + }, + { + target: '2', + guard: guards.challengeAccepted(10), + }, + ], + }, + }, + '2': { + on: { + ADVANCE: { + target: '3', + guard: guards.challengeAccepted(20), + }, + }, + }, + '3': {}, + }, + }, committed: { initial: '0', states: { @@ -315,8 +369,19 @@ export function useAchievements() { return { achievements, - advanceAchievements: (uniqueVeggies: string[], hotStreakLength: number, totalWeeks: number) => - actor.send({type: 'ADVANCE', uniqueVeggies, hotStreakLength, totalWeeks}), + advanceAchievements: ( + uniqueVeggies: string[], + hotStreakLength: number, + totalWeeks: number, + completedChallenges: number, + ) => + actor.send({ + type: 'ADVANCE', + uniqueVeggies, + hotStreakLength, + totalWeeks, + completedChallenges, + }), resetAchievements: () => actor.send({type: 'RESET'}), }; } diff --git a/src/i18n/en.json b/src/i18n/en.json index e43e4cc..9937d9f 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1,5 +1,13 @@ { "achievements": { + "challengeAccepted": { + "title": "Challenge Accepted", + "ariaLabel": "{0} completed weekly challenges", + "badgeText": "{0} challenges", + "1": "You have completed five weekly challenges, earning you the bronze achievement!", + "2": "You have completed ten weekly challenges, earning you the silver achievement!", + "3": "You have completed twenty weekly challenges, earning you the gold achievement!" + }, "committed": { "title": "The Committed", "ariaLabel": "{0} months of active use", diff --git a/src/i18n/fi.json b/src/i18n/fi.json index b7f5ddf..86e11e5 100644 --- a/src/i18n/fi.json +++ b/src/i18n/fi.json @@ -1,5 +1,13 @@ { "achievements": { + "challengeAccepted": { + "title": "Haaste vastaanotettu", + "ariaLabel": "{0} suoritettua viikkohaastetta", + "badgeText": "{0} haastetta", + "1": "Olet suorittanut viisi viikkohastetta ja ansainnut pronssisen palkinnon!", + "2": "Olet suorittanut kymmenen viikkohastetta ja ansainnut hopeisen palkinnon!", + "3": "Olet suorittanut 20 viikkohastetta ja ansainnut kultaisen palkinnon!" + }, "committed": { "title": "Sitoutunut", "ariaLabel": "{0} kuukauden aktiivinen kΓ€yttΓΆ", diff --git a/src/stores/activityStore.ts b/src/stores/activityStore.ts index 682f031..de477a0 100644 --- a/src/stores/activityStore.ts +++ b/src/stores/activityStore.ts @@ -67,6 +67,13 @@ export const useActivityStore = defineStore('activity', () => { const uniqueVeggies = computed(() => unique(allVeggies.value)); + const completedChallenges = computed( + () => + challenges.value.filter(({startDate, veggie}) => + veggiesForWeek.value(startDate).includes(veggie), + ).length, + ); + const veggiesForWeek = computed( () => (weekStart: DateTime) => weeks.value.find(({startDate}) => startDate.equals(weekStart))?.veggies ?? [], @@ -143,6 +150,7 @@ export const useActivityStore = defineStore('activity', () => { getWeekStarts, getTotalWeeks, hotStreak, + completedChallenges, currentVeggies, currentChallenge, allVeggies, diff --git a/src/stores/appStateStore.ts b/src/stores/appStateStore.ts index f0e3532..2ce45fb 100644 --- a/src/stores/appStateStore.ts +++ b/src/stores/appStateStore.ts @@ -12,10 +12,15 @@ type Message = { export const useAppStateStore = defineStore('appState', () => { const {advanceAchievements, achievements, resetAchievements} = useAchievements(); - const {uniqueVeggies, hotStreak, weeks} = storeToRefs(useActivityStore()); + const {uniqueVeggies, hotStreak, weeks, completedChallenges} = storeToRefs(useActivityStore()); watchEffect(() => { - advanceAchievements(uniqueVeggies.value, hotStreak.value, weeks.value.length); + advanceAchievements( + uniqueVeggies.value, + hotStreak.value, + weeks.value.length, + completedChallenges.value, + ); }); // State refs diff --git a/src/utils/types.ts b/src/utils/types.ts index 2ef43c9..f772346 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -42,6 +42,7 @@ export enum AchievementLevel { } export type Achievements = { + challengeAccepted: AchievementLevel; committed: AchievementLevel; completionist: AchievementLevel; hotStreak: AchievementLevel;