diff --git a/test/api/v3/integration/members/GET-achievements.test.js b/test/api/v3/integration/members/GET-achievements.test.js new file mode 100644 index 0000000000..cb7930adba --- /dev/null +++ b/test/api/v3/integration/members/GET-achievements.test.js @@ -0,0 +1,44 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-v3-integration.helper'; +import { v4 as generateUUID } from 'uuid'; + +describe('GET /members/:memberId/achievements', () => { + let user; + + before(async () => { + user = await generateUser(); + }); + + it('validates req.params.memberId', async () => { + await expect(user.get('/members/invalidUUID/achievements')).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('invalidReqParams'), + }); + }); + + it('returns achievements based on given user', async () => { + let member = await generateUser({ + contributor: {level: 1}, + backer: {tier: 3}, + }); + let achievementsRes = await user.get(`/members/${member._id}/achievements`); + + expect(achievementsRes.special.achievements.contributor.earned).to.equal(true); + expect(achievementsRes.special.achievements.contributor.value).to.equal(1); + + expect(achievementsRes.special.achievements.kickstarter.earned).to.equal(true); + expect(achievementsRes.special.achievements.kickstarter.value).to.equal(3); + }); + + it('handles non-existing members', async () => { + let dummyId = generateUUID(); + await expect(user.get(`/members/${dummyId}/achievements`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('userWithIDNotFound', {userId: dummyId}), + }); + }); +}); diff --git a/test/common/libs/achievements.test.js b/test/common/libs/achievements.test.js new file mode 100644 index 0000000000..ecccfdfbee --- /dev/null +++ b/test/common/libs/achievements.test.js @@ -0,0 +1,348 @@ +import shared from '../../../website/common'; +import { + generateUser, +} from '../../helpers/common.helper'; + +describe('achievements', () => { + describe('general well-formedness', () => { + let user = generateUser(); + let achievements = shared.achievements.getAchievementsForProfile(user); + + it('each category has \'label\' and \'achievements\' fields', () => { + _.each(achievements, (category) => { + expect(category).to.have.property('label') + .that.is.a('string'); + expect(category).to.have.property('achievements') + .that.is.a('object'); + }); + }); + + it('each achievement has all required fields of correct types', () => { + _.each(achievements, (category) => { + _.each(category.achievements, (achiev) => { + // May have additional fields (such as 'value' and 'optionalCount'). + expect(achiev).to.contain.all.keys(['title', 'text', 'icon', 'earned', 'index']); + expect(achiev.title).to.be.a('string'); + expect(achiev.text).to.be.a('string'); + expect(achiev.icon).to.be.a('string'); + expect(achiev.earned).to.be.a('boolean'); + expect(achiev.index).to.be.a('number'); + }); + }); + }); + + it('categories have unique labels', () => { + let achievementsArray = _.values(achievements).map(cat => cat.label); + let labels = _.uniq(achievementsArray); + + expect(labels.length).to.be.greaterThan(0); + expect(labels.length).to.eql(_.size(achievements)); + }); + + it('achievements have unique keys', () => { + let keysSoFar = {}; + + _.each(achievements, (category) => { + _.keys(category.achievements).forEach((key) => { + expect(keysSoFar[key]).to.be.undefined; + keysSoFar[key] = key; + }); + }); + }); + + it('achievements have unique indices', () => { + let indicesSoFar = {}; + + _.each(achievements, (category) => { + _.each(category.achievements, (achiev) => { + let i = achiev.index; + expect(indicesSoFar[i]).to.be.undefined; + indicesSoFar[i] = i; + }); + }); + }); + + it('all categories have at least 1 achievement', () => { + _.each(achievements, (category) => { + expect(_.size(category.achievements)).to.be.greaterThan(0); + }); + }); + }); + + describe('unearned basic achievements', () => { + let user = generateUser(); + let basicAchievs = shared.achievements.getAchievementsForProfile(user).basic.achievements; + + it('streak and perfect day achievements exist with counts', () => { + let streak = basicAchievs.streak; + let perfect = basicAchievs.perfect; + + expect(streak).to.exist; + expect(streak).to.have.property('optionalCount') + .that.is.a('number'); + expect(perfect).to.exist; + expect(perfect).to.have.property('optionalCount') + .that.is.a('number'); + }); + + it('party up/on achievements exist with no counts', () => { + let partyUp = basicAchievs.partyUp; + let partyOn = basicAchievs.partyOn; + + expect(partyUp).to.exist; + expect(partyUp.optionalCount).to.be.undefined; + expect(partyOn).to.exist; + expect(partyOn.optionalCount).to.be.undefined; + }); + + it('pet/mount master and triad bingo achievements exist with counts', () => { + let beastMaster = basicAchievs.beastMaster; + let mountMaster = basicAchievs.mountMaster; + let triadBingo = basicAchievs.triadBingo; + + expect(beastMaster).to.exist; + expect(beastMaster).to.have.property('optionalCount') + .that.is.a('number'); + expect(mountMaster).to.exist; + expect(mountMaster).to.have.property('optionalCount') + .that.is.a('number'); + expect(triadBingo).to.exist; + expect(triadBingo).to.have.property('optionalCount') + .that.is.a('number'); + }); + + it('ultimate gear achievements exist with no counts', () => { + let gearTypes = ['healer', 'rogue', 'warrior', 'mage']; + gearTypes.forEach((gear) => { + let gearAchiev = basicAchievs[`${gear}UltimateGear`]; + + expect(gearAchiev).to.exist; + expect(gearAchiev.optionalCount).to.be.undefined; + }); + }); + + it('rebirth achievement exists with no count', () => { + let rebirth = basicAchievs.rebirth; + + expect(rebirth).to.exist; + expect(rebirth.optionalCount).to.be.undefined; + }); + }); + + describe('unearned seasonal achievements', () => { + let user = generateUser(); + let seasonalAchievs = shared.achievements.getAchievementsForProfile(user).seasonal.achievements; + + it('habiticaDays and habitBirthdays achievements exist with counts', () => { + let habiticaDays = seasonalAchievs.habiticaDays; + let habitBirthdays = seasonalAchievs.habitBirthdays; + + expect(habiticaDays).to.exist; + expect(habiticaDays).to.have.property('optionalCount') + .that.is.a('number'); + expect(habitBirthdays).to.exist; + expect(habitBirthdays).to.have.property('optionalCount') + .that.is.a('number'); + }); + + it('spell achievements exist with counts', () => { + let spellTypes = ['snowball', 'spookySparkles', 'shinySeed', 'seafoam']; + spellTypes.forEach((spell) => { + let spellAchiev = seasonalAchievs[spell]; + + expect(spellAchiev).to.exist; + expect(spellAchiev).to.have.property('optionalCount') + .that.is.a('number'); + }); + }); + + it('quest achievements exist with counts', () => { + let quests = ['dilatory', 'stressbeast', 'burnout', 'bewilder']; + quests.forEach((quest) => { + let questAchiev = seasonalAchievs[`${quest}Quest`]; + + expect(questAchiev).to.exist; + expect(questAchiev.optionalCount).to.be.undefined; + }); + }); + + it('costumeContests achievement exists with count', () => { + let costumeContests = seasonalAchievs.costumeContests; + + expect(costumeContests).to.exist; + expect(costumeContests).to.have.property('optionalCount') + .that.is.a('number'); + }); + + it('card achievements exist with counts', () => { + let cardTypes = ['greeting', 'thankyou', 'nye', 'valentine', 'birthday']; + cardTypes.forEach((card) => { + let cardAchiev = seasonalAchievs[`${card}Cards`]; + + expect(cardAchiev).to.exist; + expect(cardAchiev).to.have.property('optionalCount') + .that.is.a('number'); + }); + }); + }); + + describe('unearned special achievements', () => { + let user = generateUser(); + let specialAchievs = shared.achievements.getAchievementsForProfile(user).special.achievements; + + it('habitSurveys achievement exists with count', () => { + let habitSurveys = specialAchievs.habitSurveys; + + expect(habitSurveys).to.exist; + expect(habitSurveys).to.have.property('optionalCount') + .that.is.a('number'); + }); + + it('contributor achievement exists with value and no count', () => { + let contributor = specialAchievs.contributor; + + expect(contributor).to.exist; + expect(contributor).to.have.property('value') + .that.is.a('number'); + expect(contributor.optionalCount).to.be.undefined; + }); + + it('npc achievement is hidden if unachieved', () => { + let npc = specialAchievs.npc; + expect(npc).to.not.exist; + }); + + it('kickstarter achievement is hidden if unachieved', () => { + let kickstarter = specialAchievs.kickstarter; + expect(kickstarter).to.not.exist; + }); + + it('veteran achievement is hidden if unachieved', () => { + let veteran = specialAchievs.veteran; + expect(veteran).to.not.exist; + }); + + it('originalUser achievement is hidden if unachieved', () => { + let originalUser = specialAchievs.originalUser; + expect(originalUser).to.not.exist; + }); + }); + + describe('earned special achievements', () => { + let user = generateUser({ + achievements: { + habitSurveys: 2, + veteran: true, + originalUser: true, + }, + backer: {tier: 3}, + contributor: {level: 1}, + }); + let specialAchievs = shared.achievements.getAchievementsForProfile(user).special.achievements; + + it('habitSurveys achievement is earned with correct value', () => { + let habitSurveys = specialAchievs.habitSurveys; + + expect(habitSurveys).to.exist; + expect(habitSurveys.earned).to.eql(true); + expect(habitSurveys.value).to.eql(2); + }); + + it('contributor achievement is earned with correct value', () => { + let contributor = specialAchievs.contributor; + + expect(contributor).to.exist; + expect(contributor.earned).to.eql(true); + expect(contributor.value).to.eql(1); + }); + + it('npc achievement is earned with correct value', () => { + let npcUser = generateUser({ + backer: {npc: 'test'}, + }); + let npc = shared.achievements.getAchievementsForProfile(npcUser).special.achievements.npc; + + expect(npc).to.exist; + expect(npc.earned).to.eql(true); + expect(npc.value).to.eql('test'); + }); + + it('kickstarter achievement is earned with correct value', () => { + let kickstarter = specialAchievs.kickstarter; + + expect(kickstarter).to.exist; + expect(kickstarter.earned).to.eql(true); + expect(kickstarter.value).to.eql(3); + }); + + it('veteran achievement is earned', () => { + let veteran = specialAchievs.veteran; + + expect(veteran).to.exist; + expect(veteran.earned).to.eql(true); + }); + + it('originalUser achievement is earned', () => { + let originalUser = specialAchievs.originalUser; + + expect(originalUser).to.exist; + expect(originalUser.earned).to.eql(true); + }); + }); + + describe('mountMaster, beastMaster, and triadBingo achievements', () => { + it('master and triad bingo achievements do not include *Text2 strings if no keys have been used', () => { + let user = generateUser(); + let basicAchievs = shared.achievements.getAchievementsForProfile(user).basic.achievements; + + let beastMaster = basicAchievs.beastMaster; + let mountMaster = basicAchievs.mountMaster; + let triadBingo = basicAchievs.triadBingo; + + expect(beastMaster.text).to.not.match(/released/); + expect(beastMaster.text).to.not.match(/0 time\(s\)/); + expect(mountMaster.text).to.not.match(/released/); + expect(mountMaster.text).to.not.match(/0 time\(s\)/); + expect(triadBingo.text).to.not.match(/released/); + expect(triadBingo.text).to.not.match(/0 time\(s\)/); + }); + + it('master and triad bingo achievements includes *Text2 strings if keys have been used', () => { + let user = generateUser({ + achievements: { + beastMasterCount: 1, + mountMasterCount: 2, + triadBingoCount: 3, + }, + }); + let basicAchievs = shared.achievements.getAchievementsForProfile(user).basic.achievements; + + let beastMaster = basicAchievs.beastMaster; + let mountMaster = basicAchievs.mountMaster; + let triadBingo = basicAchievs.triadBingo; + + expect(beastMaster.text).to.match(/released/); + expect(beastMaster.text).to.match(/1 time\(s\)/); + expect(mountMaster.text).to.match(/released/); + expect(mountMaster.text).to.match(/2 time\(s\)/); + expect(triadBingo.text).to.match(/released/); + expect(triadBingo.text).to.match(/3 time\(s\)/); + }); + }); + + describe('ultimateGear achievements', () => { + it('title and text contain localized class info', () => { + let user = generateUser(); + let basicAchievs = shared.achievements.getAchievementsForProfile(user).basic.achievements; + let gearTypes = ['healer', 'rogue', 'warrior', 'mage']; + + gearTypes.forEach((gear) => { + let gearAchiev = basicAchievs[`${gear}UltimateGear`]; + let classNameRegex = new RegExp(gear.charAt(0).toUpperCase() + gear.slice(1)); + + expect(gearAchiev.title).to.match(classNameRegex); + expect(gearAchiev.text).to.match(classNameRegex); + }); + }); + }); +}); diff --git a/website/assets/sprites/css/index.css b/website/assets/sprites/css/index.css index 5954342b57..d85a66e838 100644 --- a/website/assets/sprites/css/index.css +++ b/website/assets/sprites/css/index.css @@ -50,6 +50,11 @@ margin-right: 10px; } +.achievement .counter { + bottom: 0; + right: 10px; +} + .multi-achievement { margin: auto; padding-left: 0.5em; diff --git a/website/assets/sprites/spritesmith/achievements/achievement-alpha2x.png b/website/assets/sprites/spritesmith/achievements/achievement-alpha2x.png new file mode 100644 index 0000000000..a55d68a66f Binary files /dev/null and b/website/assets/sprites/spritesmith/achievements/achievement-alpha2x.png differ diff --git a/website/assets/sprites/spritesmith/achievements/achievement-dilatory2x.png b/website/assets/sprites/spritesmith/achievements/achievement-dilatory2x.png new file mode 100644 index 0000000000..3b01c3d72e Binary files /dev/null and b/website/assets/sprites/spritesmith/achievements/achievement-dilatory2x.png differ diff --git a/website/assets/sprites/spritesmith/achievements/achievement-partyOn2x.png b/website/assets/sprites/spritesmith/achievements/achievement-partyOn2x.png index ca224b2e7e..6e5fa9c91c 100644 Binary files a/website/assets/sprites/spritesmith/achievements/achievement-partyOn2x.png and b/website/assets/sprites/spritesmith/achievements/achievement-partyOn2x.png differ diff --git a/website/assets/sprites/spritesmith/achievements/achievement-stoikalm2x.png b/website/assets/sprites/spritesmith/achievements/achievement-stoikalm2x.png new file mode 100644 index 0000000000..52976cf1ba Binary files /dev/null and b/website/assets/sprites/spritesmith/achievements/achievement-stoikalm2x.png differ diff --git a/website/assets/sprites/spritesmith/achievements/achievement-unearned2x.png b/website/assets/sprites/spritesmith/achievements/achievement-unearned2x.png new file mode 100644 index 0000000000..e9d2b1e729 Binary files /dev/null and b/website/assets/sprites/spritesmith/achievements/achievement-unearned2x.png differ diff --git a/website/client-old/js/controllers/memberModalCtrl.js b/website/client-old/js/controllers/memberModalCtrl.js index f1d20b1041..2d48f973ab 100644 --- a/website/client-old/js/controllers/memberModalCtrl.js +++ b/website/client-old/js/controllers/memberModalCtrl.js @@ -17,6 +17,10 @@ habitrpg $scope.$watch( function() { return Members.selectedMember; }, function (member) { if(member) { $scope.profile = member; + + $scope.achievements = Shared.achievements.getAchievementsForProfile($scope.profile); + $scope.achievPopoverPlacement = 'left'; + $scope.achievAppendToBody = 'false'; // append-to-body breaks popovers in modal windows } }); diff --git a/website/client-old/js/controllers/userCtrl.js b/website/client-old/js/controllers/userCtrl.js index 92079716cc..a90406e6d0 100644 --- a/website/client-old/js/controllers/userCtrl.js +++ b/website/client-old/js/controllers/userCtrl.js @@ -104,5 +104,9 @@ habitrpg.controller("UserCtrl", ['$rootScope', '$scope', '$location', 'User', '$ var nextRewardAt = currentLoginDay.nextRewardAt; return ($scope.profile.loginIncentives - previousRewardDay)/(nextRewardAt - previousRewardDay) * 100; }; + + $scope.achievements = Shared.achievements.getAchievementsForProfile($scope.profile); + $scope.achievPopoverPlacement = 'right'; + $scope.achievAppendToBody = 'true'; // append-to-body breaks popovers in modal windows } ]); diff --git a/website/client-old/js/services/notificationServices.js b/website/client-old/js/services/notificationServices.js index 7b5d512280..edb08cd4cc 100644 --- a/website/client-old/js/services/notificationServices.js +++ b/website/client-old/js/services/notificationServices.js @@ -94,7 +94,7 @@ angular.module("habitrpg").factory("Notification", } function streak(val) { - _notify(window.env.t('streakName') + ': ' + val, 'streak', 'glyphicon glyphicon-repeat'); + _notify(window.env.t('streaks') + ': ' + val, 'streak', 'glyphicon glyphicon-repeat'); } function text(val, onClick){ diff --git a/website/common/locales/en/character.json b/website/common/locales/en/character.json index ea0b2f0a7d..6164b205e1 100644 --- a/website/common/locales/en/character.json +++ b/website/common/locales/en/character.json @@ -62,8 +62,8 @@ "gearAchievement": "You have earned the \"Ultimate Gear\" Achievement for upgrading to the maximum gear set for a class! You have attained the following complete sets:", "moreGearAchievements": "To attain more Ultimate Gear badges, change classes on your stats page and buy up your new class's gear!", "armoireUnlocked": "For more equipment, check out the Enchanted Armoire! Click on the Enchanted Armoire Reward for a random chance at special Equipment! It may also give you random XP or food items.", - "ultimGearName": "Ultimate Gear", - "ultimGearText": "Has upgraded to the maximum weapon and armor set for the following classes:", + "ultimGearName": "Ultimate Gear - <%= ultClass %>", + "ultimGearText": "Has upgraded to the maximum weapon and armor set for the <%= ultClass %> class.", "level": "Level", "levelUp": "Level Up!", "gainedLevel": "You gained a level!", diff --git a/website/common/locales/en/contrib.json b/website/common/locales/en/contrib.json index f00382d04a..19bfc1b2b6 100644 --- a/website/common/locales/en/contrib.json +++ b/website/common/locales/en/contrib.json @@ -21,11 +21,11 @@ "contribModal": "<%= name %>, you awesome person! You're now a tier <%= level %> contributor for helping Habitica. See", "contribLink": "what prizes you've earned for your contribution!", "contribName": "Contributor", - "contribText": "Has contributed to Habitica (code, design, pixel art, legal advice, docs, etc). Want this badge? ", + "contribText": "Has contributed to Habitica (code, design, pixel art, legal advice, docs, etc). Want this badge? Read more.", "readMore": "Read More", - "kickstartName": "Kickstarter Backer - $<%= tier %> Tier", + "kickstartName": "Kickstarter Backer - $<%= key %> Tier", "kickstartText": "Backed the Kickstarter Project", - "helped": "Helped Habit Grow", + "helped": "Helped Habitica Grow", "helpedText1": "Helped Habitica grow by filling out", "helpedText2": "this survey.", "hall": "Hall of Heroes", @@ -59,7 +59,7 @@ "conLearnURL": "http://habitica.wikia.com/wiki/Contributing_to_Habitica", "conRewardsURL": "http://habitica.wikia.com/wiki/Contributor_Rewards", "surveysSingle": "Helped Habitica grow, either by filling out a survey or helping with a major testing effort. Thank you!", - "surveysMultiple": "Helped Habitica grow on <%= surveys %> occasions, either by filling out a survey or helping with a major testing effort. Thank you!", + "surveysMultiple": "Helped Habitica grow on <%= count %> occasions, either by filling out a survey or helping with a major testing effort. Thank you!", "currentSurvey": "Current Survey", "surveyWhen": "The badge will be awarded to all participants when surveys have been processed, in late March.", "blurbInbox": "This is where your private messages are stored! You can send someone a message by clicking on the envelope icon next to their name in Tavern, Party, or Guild Chat. If you've received an inappropriate PM, you should email a screenshot of it to Lemoness (leslie@habitica.com)", diff --git a/website/common/locales/en/generic.json b/website/common/locales/en/generic.json index 9e4ff7d418..ffa2270a31 100644 --- a/website/common/locales/en/generic.json +++ b/website/common/locales/en/generic.json @@ -44,6 +44,9 @@ "unorderedListMarkdown": "+ First item\n+ Second item\n+ Third item", "code": "`code`", "achievements": "Achievements", + "basicAchievs": "Basic Achievements", + "seasonalAchievs": "Seasonal Achievements", + "specialAchievs": "Special Achievements", "modalAchievement": "Achievement!", "special": "Special", "site": "Site", @@ -89,15 +92,15 @@ "originalUserText": "One of the very original early adopters. Talk about alpha tester!", "habitBirthday": "Habitica Birthday Bash", "habitBirthdayText": "Celebrated the Habitica Birthday Bash!", - "habitBirthdayPluralText": "Celebrated <%= number %> Habitica Birthday Bashes!", + "habitBirthdayPluralText": "Celebrated <%= count %> Habitica Birthday Bashes!", "habiticaDay": "Habitica Naming Day", "habiticaDaySingularText": "Celebrated Habitica's Naming Day! Thanks for being a fantastic user.", - "habiticaDayPluralText": "Celebrated <%= number %> Naming Days! Thanks for being a fantastic user.", + "habiticaDayPluralText": "Celebrated <%= count %> Naming Days! Thanks for being a fantastic user.", "achievementDilatory": "Savior of Dilatory", "achievementDilatoryText": "Helped defeat the Dread Drag'on of Dilatory during the 2014 Summer Splash Event!", "costumeContest": "Costume Contestant", "costumeContestText": "Participated in the Habitoween Costume Contest. See some of the entries on the Habitica blog!", - "costumeContestTextPlural": "Participated in <%= number %> Habitoween Costume Contests. See some of the entries on the Habitica blog!", + "costumeContestTextPlural": "Participated in <%= count %> Habitoween Costume Contests. See some of the entries on the Habitica blog!", "memberSince": "- Member since", "lastLoggedIn": "- Last logged in", "notPorted": "This feature is not yet ported from the original site.", @@ -159,7 +162,7 @@ "greeting2": "`waves frantically`", "greeting3": "Hey you!", "greetingCardAchievementTitle": "Cheery Chum", - "greetingCardAchievementText": "Hey! Hi! Hello! Sent or received <%= cards %> greeting cards.", + "greetingCardAchievementText": "Hey! Hi! Hello! Sent or received <%= count %> greeting cards.", "thankyouCard": "Thank-You Card", "thankyouCardExplanation": "You both receive the Greatly Grateful achievement!", "thankyouCardNotes": "Send a Thank-You card to a party member.", @@ -168,13 +171,13 @@ "thankyou2": "Sending you a thousand thanks.", "thankyou3": "I'm very grateful - thank you!", "thankyouCardAchievementTitle": "Greatly Grateful", - "thankyouCardAchievementText": "Thanks for being thankful! Sent or received <%= cards %> Thank-You cards.", + "thankyouCardAchievementText": "Thanks for being thankful! Sent or received <%= count %> Thank-You cards.", "birthdayCard": "Birthday Card", "birthdayCardExplanation": "You both receive the Birthday Bonanza achievement!", "birthdayCardNotes": "Send a birthday card to a party member.", "birthday0": "Happy birthday to you!", "birthdayCardAchievementTitle": "Birthday Bonanza", - "birthdayCardAchievementText": "Many happy returns! Sent or received <%= cards %> birthday cards.", + "birthdayCardAchievementText": "Many happy returns! Sent or received <%= count %> birthday cards.", "streakAchievement": "You earned a streak achievement!", "firstStreakAchievement": "21-Day Streak", "streakAchievementCount": "<%= streaks %> 21-Day Streaks", diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json index 8f126775f0..f0c27c1cc5 100644 --- a/website/common/locales/en/groups.json +++ b/website/common/locales/en/groups.json @@ -172,8 +172,8 @@ "requestAcceptGuidelines": "If you would like to post messages in the Tavern or any party or guild chat, please first read our <%= linkStart %>Community Guidelines<%= linkEnd %> and then click the button below to indicate that you accept them.", "partyUpName": "Party Up", "partyOnName": "Party On", - "partyUpAchievement": "Joined a Party with another person! Have fun battling monsters and supporting each other.", - "partyOnAchievement": "Joined a Party with at least four people! Enjoy your increased accountability as you unite with your friends to vanquish your foes!", + "partyUpText": "Joined a Party with another person! Have fun battling monsters and supporting each other.", + "partyOnText": "Joined a Party with at least four people! Enjoy your increased accountability as you unite with your friends to vanquish your foes!", "largeGroupNote": "Note: This Guild is now too large to support notifications! Be sure to check back every day to see new messages.", "groupIdRequired": "\"groupId\" must be a valid UUID", "groupNotFound": "Group not found or you don't have access.", diff --git a/website/common/locales/en/limited.json b/website/common/locales/en/limited.json index 8cf71a2052..04acbe1c71 100644 --- a/website/common/locales/en/limited.json +++ b/website/common/locales/en/limited.json @@ -3,13 +3,13 @@ "seasonalEdition": "Seasonal Edition", "winterColors": "Winter Colors", "annoyingFriends": "Annoying Friends", - "annoyingFriendsText": "Got snowballed <%= snowballs %> times by party members.", + "annoyingFriendsText": "Got snowballed <%= count %> times by party members.", "alarmingFriends": "Alarming Friends", - "alarmingFriendsText": "Got spooked <%= spookySparkles %> times by party members.", + "alarmingFriendsText": "Got spooked <%= count %> times by party members.", "agriculturalFriends": "Agricultural Friends", - "agriculturalFriendsText": "Got transformed into a flower <%= seeds %> times by party members.", + "agriculturalFriendsText": "Got transformed into a flower <%= count %> times by party members.", "aquaticFriends": "Aquatic Friends", - "aquaticFriendsText": "Got splashed <%= seafoam %> times by party members.", + "aquaticFriendsText": "Got splashed <%= count %> times by party members.", "valentineCard": "Valentine's Day Card", "valentineCardExplanation": "For enduring such a saccharine poem, you both receive the \"Adoring Friends\" badge!", "valentineCardNotes": "Send a Valentine's Day card to a party member.", @@ -18,7 +18,7 @@ "valentine2": "\"Roses are red\n\nThis poem style is old\n\nI hope that you like this\n\n'Cause it cost ten Gold.\"", "valentine3": "\"Roses are red\n\nIce Drakes are blue\n\nNo treasure is better\n\nThan time spent with you!\"", "valentineCardAchievementTitle": "Adoring Friends", - "valentineCardAchievementText": "Aww, you and your friend must really care about each other! Sent or received <%= cards %> Valentine's Day cards.", + "valentineCardAchievementText": "Aww, you and your friend must really care about each other! Sent or received <%= count %> Valentine's Day cards.", "polarBear": "Polar Bear", "turkey": "Turkey", "gildedTurkey": "Gilded Turkey", @@ -49,7 +49,7 @@ "nyeCardNotes": "Send a New Year's card to a party member.", "seasonalItems": "Seasonal Items", "nyeCardAchievementTitle": "Auld Acquaintance", - "nyeCardAchievementText": "Happy New Year! Sent or received <%= cards %> New Year's cards.", + "nyeCardAchievementText": "Happy New Year! Sent or received <%= count %> New Year's cards.", "nye0": "Happy New Year! May you slay many a bad Habit.", "nye1": "Happy New Year! May you reap many Rewards.", "nye2": "Happy New Year! May you earn many a Perfect Day.", diff --git a/website/common/locales/en/npc.json b/website/common/locales/en/npc.json index e6164e0986..aab6ad8e15 100644 --- a/website/common/locales/en/npc.json +++ b/website/common/locales/en/npc.json @@ -1,5 +1,6 @@ { "npc": "NPC", + "npcAchievementName": "<%= key %> NPC", "npcAchievementText": "Backed the Kickstarter project at the maximum level!", "mattBoch": "Matt Boch", "mattShall": "Shall I bring you your steed, <%= name %>? Once you've fed a pet enough food to turn it into a mount, it will appear here. Click a mount to saddle up!", diff --git a/website/common/locales/en/rebirth.json b/website/common/locales/en/rebirth.json index 58ea71dfa5..2a22a2ed6e 100644 --- a/website/common/locales/en/rebirth.json +++ b/website/common/locales/en/rebirth.json @@ -18,8 +18,9 @@ "rebirthAchievement100": "You've begun a new adventure! This is Rebirth <%= number %> for you, and the highest Level you've attained is 100 or higher. To stack this Achievement, begin your next new adventure when you've reached at least 100!", "rebirthBegan": "Began a New Adventure", "rebirthText": "Began <%= rebirths %> New Adventures", - "rebirthOrb": "Used an Orb of Rebirth to start over after attaining Level", - "rebirthOrb100": "Used an Orb of Rebirth to start over after attaining Level 100 or higher", + "rebirthOrb": "Used an Orb of Rebirth to start over after attaining Level <%= level %>.", + "rebirthOrb100": "Used an Orb of Rebirth to start over after attaining Level 100 or higher.", + "rebirthOrbNoLevel": "Used an Orb of Rebirth to start over.", "rebirthPop": "Begin a new character at Level 1 while retaining achievements, collectibles, equipment, and tasks with history.", "rebirthName": "Orb of Rebirth", "reborn": "Reborn, max level <%= reLevel %>", diff --git a/website/common/locales/en/tasks.json b/website/common/locales/en/tasks.json index 0793668e95..6fbd35f688 100644 --- a/website/common/locales/en/tasks.json +++ b/website/common/locales/en/tasks.json @@ -78,12 +78,13 @@ "startDate": "Start Date", "startDateHelpTitle": "When should this task start?", "startDateHelp": "Set the date for which this task takes effect. Will not be due on earlier days.", - "streakName": "Streak Achievements", - "streakText": "Has performed <%= streaks %> 21-day streaks on Dailies", + "streaks": "Streak Achievements", + "streakName": "<%= count %> Streak Achievements", + "streakText": "Has performed <%= count %> 21-day streaks on Dailies", "streakSingular": "Streaker", "streakSingularText": "Has performed a 21-day streak on a Daily", - "perfectName": "Perfect Days", - "perfectText": "Completed all active Dailies on <%= perfects %> days. With this achievement you get a +level/2 buff to all attributes for the next day. Levels greater than 100 don't have any additional effects on buffs.", + "perfectName": "<%= count %> Perfect Days", + "perfectText": "Completed all active Dailies on <%= count %> days. With this achievement you get a +level/2 buff to all attributes for the next day. Levels greater than 100 don't have any additional effects on buffs.", "perfectSingular": "Perfect Day", "perfectSingularText": "Completed all active Dailies in one day. With this achievement you get a +level/2 buff to all attributes for the next day. Levels greater than 100 don't have any additional effects on buffs.", "streakerAchievement": "You have attained the \"Streaker\" Achievement! The 21-day mark is a milestone for habit formation. You can continue to stack this Achievement for every additional 21 days, on this Daily or any other!", diff --git a/website/common/script/content/achievements.js b/website/common/script/content/achievements.js new file mode 100644 index 0000000000..4bee61a9eb --- /dev/null +++ b/website/common/script/content/achievements.js @@ -0,0 +1,188 @@ +import { each } from 'lodash'; + +let achievementsData = {}; + +let worldQuestAchievs = { + dilatoryQuest: { + icon: 'achievement-dilatory', + titleKey: 'achievementDilatory', + textKey: 'achievementDilatoryText', + }, + stressbeastQuest: { + icon: 'achievement-stoikalm', + titleKey: 'achievementStressbeast', + textKey: 'achievementStressbeastText', + }, + burnoutQuest: { + icon: 'achievement-burnout', + titleKey: 'achievementBurnout', + textKey: 'achievementBurnoutText', + }, + bewilderQuest: { + icon: 'achievement-bewilder', + titleKey: 'achievementBewilder', + textKey: 'achievementBewilderText', + }, +}; +Object.assign(achievementsData, worldQuestAchievs); + +let seasonalSpellAchievs = { + snowball: { + icon: 'achievement-snowball', + titleKey: 'annoyingFriends', + textKey: 'annoyingFriendsText', + }, + spookySparkles: { + icon: 'achievement-spookySparkles', + titleKey: 'alarmingFriends', + textKey: 'alarmingFriendsText', + }, + shinySeed: { + icon: 'achievement-shinySeed', + titleKey: 'agriculturalFriends', + textKey: 'agriculturalFriendsText', + }, + seafoam: { + icon: 'achievement-seafoam', + titleKey: 'aquaticFriends', + textKey: 'aquaticFriendsText', + }, +}; +Object.assign(achievementsData, seasonalSpellAchievs); + +let masterAchievs = { + beastMaster: { + icon: 'achievement-rat', + titleKey: 'beastMasterName', + textKey: 'beastMasterText', + text2Key: 'beastMasterText2', + }, + mountMaster: { + icon: 'achievement-wolf', + titleKey: 'mountMasterName', + textKey: 'mountMasterText', + text2Key: 'mountMasterText2', + }, + triadBingo: { + icon: 'achievement-triadbingo', + titleKey: 'triadBingoName', + textKey: 'triadBingoText', + text2Key: 'triadBingoText2', + }, +}; +Object.assign(achievementsData, masterAchievs); + +let basicAchievs = { + partyUp: { + icon: 'achievement-partyUp', + titleKey: 'partyUpName', + textKey: 'partyUpText', + }, + partyOn: { + icon: 'achievement-partyOn', + titleKey: 'partyOnName', + textKey: 'partyOnText', + }, + streak: { + icon: 'achievement-thermometer', + singularTitleKey: 'streakSingular', + singularTextKey: 'streakSingularText', + pluralTitleKey: 'streakName', + pluralTextKey: 'streakText', + }, + perfect: { + icon: 'achievement-perfect', + singularTitleKey: 'perfectSingular', + singularTextKey: 'perfectSingularText', + pluralTitleKey: 'perfectName', + pluralTextKey: 'perfectText', + }, +}; +Object.assign(achievementsData, basicAchievs); + +let specialAchievs = { + contributor: { + icon: 'achievement-boot', + titleKey: 'contribName', + textKey: 'contribText', + }, + npc: { + icon: 'achievement-ultimate-warrior', + titleKey: 'npcAchievementName', + textKey: 'npcAchievementText', + }, + kickstarter: { + icon: 'achievement-heart', + titleKey: 'kickstartName', + textKey: 'kickstartText', + }, + veteran: { + icon: 'achievement-cake', + titleKey: 'veteran', + textKey: 'veteranText', + }, + originalUser: { + icon: 'achievement-alpha', + titleKey: 'originalUser', + textKey: 'originalUserText', + }, + habitSurveys: { + icon: 'achievement-tree', + singularTitleKey: 'helped', + singularTextKey: 'surveysSingle', + pluralTitleKey: 'helped', + pluralTextKey: 'surveysMultiple', + }, +}; +Object.assign(achievementsData, specialAchievs); + +let holidayAchievs = { + habiticaDays: { + icon: 'achievement-habiticaDay', + singularTitleKey: 'habiticaDay', + singularTextKey: 'habiticaDaySingularText', + pluralTitleKey: 'habiticaDay', + pluralTextKey: 'habiticaDayPluralText', + }, + habitBirthdays: { + icon: 'achievement-habitBirthday', + singularTitleKey: 'habitBirthday', + singularTextKey: 'habitBirthdayText', + pluralTitleKey: 'habitBirthday', + pluralTextKey: 'habitBirthdayPluralText', + }, + costumeContests: { + icon: 'achievement-costumeContest', + singularTitleKey: 'costumeContest', + singularTextKey: 'costumeContestText', + pluralTitleKey: 'costumeContest', + pluralTextKey: 'costumeContestTextPlural', + }, +}; +Object.assign(achievementsData, holidayAchievs); + +let ultimateGearAchievs = ['healer', 'rogue', 'warrior', 'mage'].reduce((achievs, type) => { + achievs[`${type}UltimateGear`] = { + icon: `achievement-ultimate-${type}`, + titleKey: 'ultimGearName', + textKey: 'ultimGearText', + }; + return achievs; +}, {}); +Object.assign(achievementsData, ultimateGearAchievs); + +let cardAchievs = ['greeting', 'thankyou', 'nye', 'valentine', 'birthday'].reduce((achievs, type) => { + achievs[`${type}Cards`] = { + icon: `achievement-${type}`, + titleKey: `${type}CardAchievementTitle`, + textKey: `${type}CardAchievementText`, + }; + return achievs; +}, {}); +Object.assign(achievementsData, cardAchievs); + +each(achievementsData, (value, key) => { + value.key = key; +}); + +module.exports = achievementsData; diff --git a/website/common/script/content/index.js b/website/common/script/content/index.js index 1758a971e2..b807f7f18d 100644 --- a/website/common/script/content/index.js +++ b/website/common/script/content/index.js @@ -12,6 +12,8 @@ import { let api = module.exports; +import achievements from './achievements'; + import mysterySets from './mystery-sets'; import eggs from './eggs'; @@ -25,6 +27,8 @@ import spells from './spells'; import faq from './faq'; import loginIncentives from './loginIncentives'; +api.achievements = achievements; + api.mystery = mysterySets; api.itemList = ITEM_LIST; diff --git a/website/common/script/index.js b/website/common/script/index.js index 1d0d90c6f1..146ca35841 100644 --- a/website/common/script/index.js +++ b/website/common/script/index.js @@ -93,6 +93,9 @@ api.statsComputed = statsComputed; import shops from './libs/shops'; api.shops = shops; +import achievements from './libs/achievements'; +api.achievements = achievements; + import randomVal from './libs/randomVal'; api.randomVal = randomVal; diff --git a/website/common/script/libs/achievements.js b/website/common/script/libs/achievements.js new file mode 100644 index 0000000000..1e5583f7be --- /dev/null +++ b/website/common/script/libs/achievements.js @@ -0,0 +1,306 @@ +import content from '../content/index'; +import i18n from '../i18n'; +import { get } from 'lodash'; + +let achievs = {}; +let achievsContent = content.achievements; +let index = 0; + +function contribText (contrib, backer, language) { + if (!contrib && !backer) return; + if (backer && backer.npc) return backer.npc; + let lvl = contrib && contrib.level; + if (lvl && lvl > 0) { + let contribTitle = ''; + + if (lvl < 3) { + contribTitle = i18n.t('friend', language); + } else if (lvl < 5) { + contribTitle = i18n.t('elite', language); + } else if (lvl < 7) { + contribTitle = i18n.t('champion', language); + } else if (lvl < 8) { + contribTitle = i18n.t('legendary', language); + } else if (lvl < 9) { + contribTitle = i18n.t('guardian', language); + } else { + contribTitle = i18n.t('heroic', language); + } + + return `${contribTitle} ${contrib.text}`; + } +} + +function _add (result, data) { + result[data.key] = { + title: data.title, + text: data.text, + icon: data.icon, + earned: data.earned, + value: data.value, + index: index++, + optionalCount: data.optionalCount, + }; +} + +function _addSimpleWithCustomPath (result, user, data) { + let value = get(user, data.path); + let thisContent = achievsContent[data.key]; + + _add(result, { + title: i18n.t(thisContent.titleKey, {key: value}, data.language), + text: i18n.t(thisContent.textKey, data.language), + icon: thisContent.icon, + key: data.key, + value, + earned: Boolean(value), + }); +} + +function _addQuest (result, user, data) { + data.key = `${data.path}Quest`; + data.path = `achievements.quests.${data.path}`; + _addSimpleWithCustomPath(result, user, data); +} + +function _addSimple (result, user, data) { + let value = user.achievements[data.path]; + + let key = data.key || data.path; + let thisContent = achievsContent[key]; + + _add(result, { + title: i18n.t(thisContent.titleKey, data.language), + text: i18n.t(thisContent.textKey, data.language), + icon: thisContent.icon, + key, + value, + earned: Boolean(value), + }); +} + +function _addSimpleWithMasterCount (result, user, data) { + let language = data.language; + let value = user.achievements[`${data.path}Count`] || 0; + + let thisContent = achievsContent[data.path]; + + let text = i18n.t(thisContent.textKey, language); + if (value > 0) { + text += i18n.t(thisContent.text2Key, {count: value}, language); + } + + _add(result, { + title: i18n.t(thisContent.titleKey, language), + text, + icon: thisContent.icon, + key: data.path, + value, + optionalCount: value, + earned: Boolean(value), + }); +} + +function _addSimpleWithCount (result, user, data) { + let value = user.achievements[data.path] || 0; + + let key = data.key || data.path; + let thisContent = achievsContent[key]; + + _add(result, { + title: i18n.t(thisContent.titleKey, data.language), + text: i18n.t(thisContent.textKey, {count: value}, data.language), + icon: thisContent.icon, + key, + value, + optionalCount: value, + earned: Boolean(value), + }); +} + +function _addPlural (result, user, data) { + let value = user.achievements[data.path] || 0; + + let key = data.key || data.path; + let thisContent = achievsContent[key]; + + let titleKey; + let textKey; + // If value === 1, use singular versions of strings. + // If value !== 1, use plural versions of strings. + if (value === 1) { + titleKey = thisContent.singularTitleKey; + textKey = thisContent.singularTextKey; + } else { + titleKey = thisContent.pluralTitleKey; + textKey = thisContent.pluralTextKey; + } + + _add(result, { + title: i18n.t(titleKey, {count: value}, data.language), + text: i18n.t(textKey, {count: value}, data.language), + icon: thisContent.icon, + key, + value, + optionalCount: value, + earned: Boolean(value), + }); +} + +function _addUltimateGear (result, user, data) { + if (!data.altPath) { + data.altPath = data.path; + } + + let value = user.achievements.ultimateGearSets[data.altPath]; + + let key = `${data.path}UltimateGear`; + let thisContent = achievsContent[key]; + + let localizedClass = i18n.t(data.path, data.language); + let title = i18n.t(thisContent.titleKey, {ultClass: localizedClass}, data.language); + let text = i18n.t(thisContent.textKey, {ultClass: localizedClass}, data.language); + + _add(result, { + title, + text, + icon: thisContent.icon, + key, + value, + earned: Boolean(value), + }); +} + +function _getBasicAchievements (user, language) { + let result = {}; + + _addPlural(result, user, {path: 'streak', language}); + _addPlural(result, user, {path: 'perfect', language}); + + _addSimple(result, user, {path: 'partyUp', language}); + _addSimple(result, user, {path: 'partyOn', language}); + + _addSimpleWithMasterCount(result, user, {path: 'beastMaster', language}); + _addSimpleWithMasterCount(result, user, {path: 'mountMaster', language}); + _addSimpleWithMasterCount(result, user, {path: 'triadBingo', language}); + + _addUltimateGear(result, user, {path: 'healer', language}); + _addUltimateGear(result, user, {path: 'rogue', language}); + _addUltimateGear(result, user, {path: 'warrior', language}); + _addUltimateGear(result, user, {path: 'mage', altpath: 'wizard', language}); + + let rebirthTitle; + let rebirthText; + + if (user.achievements.rebirths > 1) { + rebirthTitle = i18n.t('rebirthText', {rebirths: user.achievements.rebirths}, language); + } else { + rebirthTitle = i18n.t('rebirthBegan', language); + } + + if (!user.achievements.rebirthLevel) { + rebirthText = i18n.t('rebirthOrbNoLevel', language); + } else if (user.achievements.rebirthLevel < 100) { + rebirthText = i18n.t('rebirthOrb', {level: user.achievements.rebirthLevel}, language); + } else { + rebirthText = i18n.t('rebirthOrb100', language); + } + + _add(result, { + key: 'rebirth', + title: rebirthTitle, + text: rebirthText, + icon: 'achievement-sun', + earned: Boolean(user.achievements.rebirths), + }); + + return result; +} + +function _getSeasonalAchievements (user, language) { + let result = {}; + + _addPlural(result, user, {path: 'habiticaDays', language}); + _addPlural(result, user, {path: 'habitBirthdays', language}); + + let spellAchievements = ['snowball', 'spookySparkles', 'shinySeed', 'seafoam']; + spellAchievements.forEach(path => { + _addSimpleWithCount(result, user, {path, language}); + }); + + let questAchievements = ['dilatory', 'stressbeast', 'burnout', 'bewilder']; + questAchievements.forEach(path => { + _addQuest(result, user, {path, language}); + }); + + _addPlural(result, user, {path: 'costumeContests', language}); + + let cardAchievements = ['greeting', 'thankyou', 'nye', 'valentine', 'birthday']; + cardAchievements.forEach(path => { + _addSimpleWithCount(result, user, {path, key: `${path}Cards`, language}); + }); + + return result; +} + +function _getSpecialAchievements (user, language) { + let result = {}; + + _addPlural(result, user, {path: 'habitSurveys', language}); + + let contribKey = 'contributor'; + let contribContent = achievsContent[contribKey]; + let contributorAchiev = { + key: contribKey, + text: i18n.t(contribContent.textKey, language), + icon: contribContent.icon, + earned: Boolean(user.contributor && user.contributor.level), + }; + if (user.contributor && user.contributor.level) { + contributorAchiev.value = user.contributor.level; + contributorAchiev.title = contribText(user.contributor, user.backer, language); + } else { + contributorAchiev.value = 0; + contributorAchiev.title = i18n.t(contribContent.titleKey, language); + } + _add(result, contributorAchiev); + + if (user.backer && user.backer.npc) { + _addSimpleWithCustomPath(result, user, {key: 'npc', path: 'backer.npc', language}); + } + + if (user.backer && user.backer.tier) { + _addSimpleWithCustomPath(result, user, {key: 'kickstarter', path: 'backer.tier', language}); + } + + if (user.achievements.veteran) { + _addSimple(result, user, {path: 'veteran', language}); + } + + if (user.achievements.originalUser) { + _addSimple(result, user, {path: 'originalUser', language}); + } + + return result; +} + +// Build and return the given user's achievement data. +achievs.getAchievementsForProfile = function getAchievementsForProfile (user, language) { + let result = { + basic: { + label: 'Basic', + achievements: _getBasicAchievements(user, language), + }, + seasonal: { + label: 'Seasonal', + achievements: _getSeasonalAchievements(user, language), + }, + special: { + label: 'Special', + achievements: _getSpecialAchievements(user, language), + }, + }; + return result; +}; + +module.exports = achievs; diff --git a/website/server/controllers/api-v3/members.js b/website/server/controllers/api-v3/members.js index 3e5ed25927..40208a8f9f 100644 --- a/website/server/controllers/api-v3/members.js +++ b/website/server/controllers/api-v3/members.js @@ -17,6 +17,7 @@ import { } from '../../libs/email'; import Bluebird from 'bluebird'; import { sendNotification as sendPushNotification } from '../../libs/pushNotifications'; +import { achievements } from '../../../../website/common/'; let api = {}; @@ -58,6 +59,113 @@ api.getMember = { }, }; +/** + * @api {get} /api/v3/members/:memberId/achievements Get member achievements object + * @apiName GetMemberAchievements + * @apiGroup Member + * @apiDescription Get a list of achievements of the requested member, grouped by basic / seasonal / special. + * + * @apiParam (Path) {UUID} memberId The member's id + * + * @apiSuccess {Object} data The achievements object + * + * @apiSuccess {Object} data.basic The basic achievements object + * @apiSuccess {Object} data.seasonal The seasonal achievements object + * @apiSuccess {Object} data.special The special achievements object + * + * @apiSuccess {String} data.*.label The label for that category + * @apiSuccess {Object} data.*.achievements The achievements in that category + * + * @apiSuccess {String} data.*.achievements.title The localized title string + * @apiSuccess {String} data.*.achievements.text The localized description string + * @apiSuccess {Boolean} data.*.achievements.earned Whether the user has earned the achievement + * @apiSuccess {Number} data.*.achievements.index The unique index assigned to the achievement (only for sorting purposes) + * @apiSuccess {Anything} data.*.achievements.value The value related to the achievement (if applicable) + * @apiSuccess {Number} data.*.achievements.optionalCount The count related to the achievement (if applicable) + * + * @apiSuccessExample {json} Successful Response + * { + * basic: { + * label: "Basic", + * achievements: { + * streak: { + * title: "0 Streak Achievements", + * text: "Has performed 0 21-day streaks on Dailies", + * icon: "achievement-thermometer", + * earned: false, + * value: 0, + * index: 60, + * optionalCount: 0 + * }, + * perfect: { + * title: "5 Perfect Days", + * text: "Completed all active Dailies on 5 days. With this achievement you get a +level/2 buff to all attributes for the next day. Levels greater than 100 don't have any additional effects on buffs.", + * icon: "achievement-perfect", + * earned: true, + * value: 5, + * index: 61, + * optionalCount: 5 + * } + * } + * }, + * seasonal: { + * label: "Seasonal", + * achievements: { + * habiticaDays: { + * title: "Habitica Naming Day", + * text: "Celebrated 0 Naming Days! Thanks for being a fantastic user.", + * icon: "achievement-habiticaDay", + * earned: false, + * value: 0, + * index: 72, + * optionalCount: 0 + * } + * } + * }, + * special: { + * label: "Special", + * achievements: { + * habitSurveys: { + * title: "Helped Habitica Grow", + * text: "Helped Habitica grow on 0 occasions, either by filling out a survey or helping with a major testing effort. Thank you!", + * icon: "achievement-tree", + * earned: false, + * value: 0, + * index: 88, + * optionalCount: 0 + * } + * } + * } + * } + * + * @apiError (400) {BadRequest} MemberIdRequired The `id` param is required and must be a valid `UUID` + * @apiError (404) {NotFound} UserWithIdNotFound The `id` param did not belong to an existing member + */ +api.getMemberAchievements = { + method: 'GET', + url: '/members/:memberId/achievements', + middlewares: [], + async handler (req, res) { + req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID(); + + let validationErrors = req.validationErrors(); + if (validationErrors) throw validationErrors; + + let memberId = req.params.memberId; + + let member = await User + .findById(memberId) + .select(memberFields) + .exec(); + + if (!member) throw new NotFound(res.t('userWithIDNotFound', {userId: memberId})); + + let achievsObject = achievements.getAchievementsForProfile(member, req.language); + + res.respond(200, achievsObject); + }, +}; + // Return a request handler for getMembersForGroup / getInvitesForGroup / getMembersForChallenge // type is `invites` or `members` function _getMembersForItem (type) { diff --git a/website/views/shared/profiles/achievements.jade b/website/views/shared/profiles/achievements.jade index cf6f35fb3d..085167a07c 100644 --- a/website/views/shared/profiles/achievements.jade +++ b/website/views/shared/profiles/achievements.jade @@ -1,268 +1,31 @@ -if mobile - .item.item-divider=env.t('achievements') +mixin simpleAchiev(achiev) + - var popoverHtml = '