From e04d4e8bea8c55a10f8eeca45c676f37cf3d206d Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Fri, 16 Oct 2020 19:50:54 +0200 Subject: [PATCH] Drop Cap Notification, Modal and A/B Test (#12651) * add drop cap notification * add drop cap notification * add dismissible notification * fix(notification): correct remove icon positioning * track events * add modal * add back files * fix links and add missing analytics * fix rounded borders and hide sub info for subscribers * a/b test * fix comparison * Translated using Weblate (Spanish) Currently translated at 98.2% (55 of 56 strings) Translation: Habitica/Messages Translate-URL: https://translate.habitica.com/projects/habitica/messages/es/ Translated using Weblate (Spanish) Currently translated at 99.4% (179 of 180 strings) Translation: Habitica/Settings Translate-URL: https://translate.habitica.com/projects/habitica/settings/es/ Merge branch 'origin/develop' into Weblate. Translated using Weblate (Spanish) Currently translated at 99.4% (175 of 176 strings) Translation: Habitica/Subscriber Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/ Translated using Weblate (Spanish (Latin America)) Currently translated at 98.6% (359 of 364 strings) Translation: Habitica/Groups Translate-URL: https://translate.habitica.com/projects/habitica/groups/es_419/ Translated using Weblate (Spanish) Currently translated at 85.7% (151 of 176 strings) Translation: Habitica/Subscriber Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/ Translated using Weblate (Spanish) Currently translated at 95.3% (538 of 564 strings) Translation: Habitica/Backgrounds Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/ Translated using Weblate (Spanish (Latin America)) Currently translated at 98.6% (359 of 364 strings) Translation: Habitica/Groups Translate-URL: https://translate.habitica.com/projects/habitica/groups/es_419/ Translated using Weblate (French) Currently translated at 100.0% (56 of 56 strings) Translation: Habitica/Messages Translate-URL: https://translate.habitica.com/projects/habitica/messages/fr/ Translated using Weblate (German) Currently translated at 100.0% (56 of 56 strings) Translation: Habitica/Messages Translate-URL: https://translate.habitica.com/projects/habitica/messages/de/ Translated using Weblate (French) Currently translated at 100.0% (718 of 718 strings) Translation: Habitica/Questscontent Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/ Translated using Weblate (German) Currently translated at 100.0% (718 of 718 strings) Translation: Habitica/Questscontent Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/ Translated using Weblate (Czech) Currently translated at 100.0% (56 of 56 strings) Translation: Habitica/Spells Translate-URL: https://translate.habitica.com/projects/habitica/spells/cs/ Translated using Weblate (Japanese) Currently translated at 100.0% (175 of 175 strings) Translation: Habitica/Subscriber Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/ Translated using Weblate (Italian) Currently translated at 100.0% (56 of 56 strings) Translation: Habitica/Messages Translate-URL: https://translate.habitica.com/projects/habitica/messages/it/ Translated using Weblate (Italian) Currently translated at 100.0% (718 of 718 strings) Translation: Habitica/Questscontent Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/it/ Translated using Weblate (Czech) Currently translated at 100.0% (180 of 180 strings) Translation: Habitica/Settings Translate-URL: https://translate.habitica.com/projects/habitica/settings/cs/ Translated using Weblate (Basque) Currently translated at 100.0% (2 of 2 strings) Translation: Habitica/Noscript Translate-URL: https://translate.habitica.com/projects/habitica/noscript/eu/ Translated using Weblate (Basque) Currently translated at 6.5% (8 of 123 strings) Translation: Habitica/Communityguidelines Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/eu/ Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (56 of 56 strings) Translation: Habitica/Messages Translate-URL: https://translate.habitica.com/projects/habitica/messages/zh_Hans/ Translated using Weblate (Japanese) Currently translated at 100.0% (56 of 56 strings) Translation: Habitica/Messages Translate-URL: https://translate.habitica.com/projects/habitica/messages/ja/ Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (718 of 718 strings) Translation: Habitica/Questscontent Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/ Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (718 of 718 strings) Translation: Habitica/Questscontent Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/ Translated using Weblate (Portuguese (Brazil)) Currently translated at 99.8% (717 of 718 strings) Translation: Habitica/Questscontent Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/ * clarify a/b test values * add tests * refactor user dropdown * fix hover state * fix user dropdown * fix user menu hierarchy * restore i18n files to release version Co-authored-by: Melior --- test/api/unit/middlewares/cronMiddleware.js | 103 ++++++++ test/common/fns/randomDrop.test.js | 144 ++++++++++ website/client/src/assets/images/swords.png | Bin 0 -> 6289 bytes .../client/src/assets/images/swords@2x.png | Bin 0 -> 7494 bytes .../client/src/assets/images/swords@3x.png | Bin 0 -> 12365 bytes website/client/src/assets/scss/dropdown.scss | 10 +- website/client/src/assets/scss/modal.scss | 5 + .../achievements/dropCapReached.vue | 250 ++++++++++++++++++ .../components/categories/categoryTags.vue | 3 + .../src/components/header/messageCount.vue | 3 +- .../components/header/notifications/base.vue | 10 +- .../header/notifications/dropCapReached.vue | 43 +++ .../header/notificationsDropdown.vue | 4 +- .../src/components/header/userDropdown.vue | 101 +++---- .../src/components/members/classBadge.vue | 2 +- .../client/src/components/notifications.vue | 3 + .../client/src/components/shops/buyModal.vue | 6 +- .../client/src/components/shops/shopItem.vue | 2 +- .../client/src/components/tasks/column.vue | 2 +- website/client/src/components/tasks/task.vue | 16 +- website/common/script/fns/randomDrop.js | 41 ++- website/server/libs/taskManager.js | 6 +- website/server/middlewares/cron.js | 21 ++ website/server/models/userNotification.js | 1 + 24 files changed, 689 insertions(+), 87 deletions(-) create mode 100644 website/client/src/assets/images/swords.png create mode 100644 website/client/src/assets/images/swords@2x.png create mode 100644 website/client/src/assets/images/swords@3x.png create mode 100644 website/client/src/components/achievements/dropCapReached.vue create mode 100644 website/client/src/components/header/notifications/dropCapReached.vue diff --git a/test/api/unit/middlewares/cronMiddleware.js b/test/api/unit/middlewares/cronMiddleware.js index 873bbb70a7..6e2ab0e18a 100644 --- a/test/api/unit/middlewares/cronMiddleware.js +++ b/test/api/unit/middlewares/cronMiddleware.js @@ -293,4 +293,107 @@ describe('cron middleware', () => { }); }); }); + + context('Drop Cap A/B Test', async () => { + it('enrolls web users', async () => { + user.lastCron = moment(new Date()).subtract({ days: 2 }); + await user.save(); + req.headers['x-client'] = 'habitica-web'; + + await new Promise((resolve, reject) => { + cronMiddleware(req, res, async err => { + if (err) return reject(err); + user = await User.findById(user._id).exec(); + expect(user._ABtests.dropCapNotif).to.be.a.string; + + return resolve(); + }); + }); + }); + + it('does not enroll 80% of users', async () => { + sandbox.stub(Math, 'random').returns(0.5); + user.lastCron = moment(new Date()).subtract({ days: 2 }); + await user.save(); + req.headers['x-client'] = 'habitica-web'; + + await new Promise((resolve, reject) => { + cronMiddleware(req, res, async err => { + if (err) return reject(err); + user = await User.findById(user._id).exec(); + expect(user._ABtests.dropCapNotif).to.be.equal('drop-cap-notif-not-enrolled'); + + return resolve(); + }); + }); + }); + + it('enables the new notification for 10% of users', async () => { + sandbox.stub(Math, 'random').returns(0.1); + user.lastCron = moment(new Date()).subtract({ days: 2 }); + await user.save(); + req.headers['x-client'] = 'habitica-web'; + + await new Promise((resolve, reject) => { + cronMiddleware(req, res, async err => { + if (err) return reject(err); + user = await User.findById(user._id).exec(); + expect(user._ABtests.dropCapNotif).to.be.equal('drop-cap-notif-enabled'); + + return resolve(); + }); + }); + }); + + it('disables the new notification for 10% of users', async () => { + sandbox.stub(Math, 'random').returns(0.2); + user.lastCron = moment(new Date()).subtract({ days: 2 }); + await user.save(); + req.headers['x-client'] = 'habitica-web'; + + await new Promise((resolve, reject) => { + cronMiddleware(req, res, async err => { + if (err) return reject(err); + user = await User.findById(user._id).exec(); + expect(user._ABtests.dropCapNotif).to.be.equal('drop-cap-notif-disabled'); + + return resolve(); + }); + }); + }); + + it('does not affect subscribers', async () => { + sandbox.stub(Math, 'random').returns(0.2); + user.lastCron = moment(new Date()).subtract({ days: 2 }); + await user.save(); + req.headers['x-client'] = 'habitica-web'; + sandbox.stub(User.prototype, 'isSubscribed').returns(true); + + await new Promise((resolve, reject) => { + cronMiddleware(req, res, async err => { + if (err) return reject(err); + user = await User.findById(user._id).exec(); + expect(user._ABtests.dropCapNotif).to.not.exist; + + return resolve(); + }); + }); + }); + + it('does not affect mobile users', async () => { + user.lastCron = moment(new Date()).subtract({ days: 2 }); + await user.save(); + req.headers['x-client'] = 'habitica-ios'; + + await new Promise((resolve, reject) => { + cronMiddleware(req, res, async err => { + if (err) return reject(err); + user = await User.findById(user._id).exec(); + expect(user._ABtests.dropCapNotif).to.not.exist; + + return resolve(); + }); + }); + }); + }); }); diff --git a/test/common/fns/randomDrop.test.js b/test/common/fns/randomDrop.test.js index 3ce03286b1..440bb4506d 100644 --- a/test/common/fns/randomDrop.test.js +++ b/test/common/fns/randomDrop.test.js @@ -1,4 +1,5 @@ import randomDrop from '../../../website/common/script/fns/randomDrop'; +import i18n from '../../../website/common/script/i18n'; import { generateUser, generateTodo, @@ -144,5 +145,148 @@ describe('common.fns.randomDrop', () => { expect(acceptableDrops).to.contain(user._tmp.drop.key); // always Desert }); }); + + context('drop cap notification', () => { + let analytics; + const req = {}; + let isSubscribedStub; + + beforeEach(() => { + user.addNotification = () => {}; + sandbox.stub(user, 'addNotification'); + user.isSubscribed = () => {}; + isSubscribedStub = sandbox.stub(user, 'isSubscribed'); + isSubscribedStub.returns(false); + analytics = { track () {} }; + sandbox.stub(analytics, 'track'); + }); + + it('sends a notification if A/B test is enabled when drop cap is reached', () => { + user._ABtests.dropCapNotif = 'drop-cap-notif-enabled'; + predictableRandom.returns(0.1); + + // Max Drop Count is 5 + expect(user.items.lastDrop.count).to.equal(0); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + expect(user.items.lastDrop.count).to.equal(5); + expect(user.addNotification).to.be.calledOnce; + expect(user.addNotification).to.be.calledWith('DROP_CAP_REACHED', { + message: i18n.t('dropCapReached'), + items: 5, + }); + }); + + it('does not send a notification if user is enrolled in disabled A/B test group', () => { + user._ABtests.dropCapNotif = 'drop-cap-notif-disabled'; + predictableRandom.returns(0.1); + + // Max Drop Count is 5 + expect(user.items.lastDrop.count).to.equal(0); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + expect(user.items.lastDrop.count).to.equal(5); + expect(user.addNotification).to.not.be.called; + }); + + it('does not send a notification if user is enrolled in disabled A/B test group', () => { + user._ABtests.dropCapNotif = 'drop-cap-notif-not-enrolled'; + predictableRandom.returns(0.1); + + // Max Drop Count is 5 + expect(user.items.lastDrop.count).to.equal(0); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + expect(user.items.lastDrop.count).to.equal(5); + expect(user.addNotification).to.not.be.called; + }); + + it('does not send a notification if drop cap is not reached', () => { + user._ABtests.dropCapNotif = 'drop-cap-notif-enabled'; + predictableRandom.returns(0.1); + + // Max Drop Count is 5 + expect(user.items.lastDrop.count).to.equal(0); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + expect(user.items.lastDrop.count).to.equal(4); + expect(user.addNotification).to.not.be.called; + }); + + it('does not send a notification if user is subscribed', () => { + user._ABtests.dropCapNotif = 'drop-cap-notif-enabled'; + predictableRandom.returns(0.1); + isSubscribedStub.returns(true); + + // Max Drop Count is 5 + expect(user.items.lastDrop.count).to.equal(0); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + expect(user.items.lastDrop.count).to.equal(5); + expect(user.addNotification).to.not.be.called; + }); + + it('tracks drop cap reached event for enrolled users (notification enabled)', () => { + user._ABtests.dropCapNotif = 'drop-cap-notif-enabled'; + predictableRandom.returns(0.1); + isSubscribedStub.returns(true); + + // Max Drop Count is 5 + expect(user.items.lastDrop.count).to.equal(0); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + expect(user.items.lastDrop.count).to.equal(5); + expect(analytics.track).to.be.calledWith('drop cap reached'); + }); + + it('tracks drop cap reached event for enrolled users (notification disabled)', () => { + user._ABtests.dropCapNotif = 'drop-cap-notif-disabled'; + predictableRandom.returns(0.1); + isSubscribedStub.returns(true); + + // Max Drop Count is 5 + expect(user.items.lastDrop.count).to.equal(0); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + expect(user.items.lastDrop.count).to.equal(5); + expect(analytics.track).to.be.calledWith('drop cap reached'); + }); + + it('does not track drop cap reached event for users not enrolled in A/B test', () => { + user._ABtests.dropCapNotif = 'drop-cap-notif-not-enrolled'; + predictableRandom.returns(0.1); + isSubscribedStub.returns(true); + + // Max Drop Count is 5 + expect(user.items.lastDrop.count).to.equal(0); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + randomDrop(user, { task, predictableRandom }, req, analytics); + expect(user.items.lastDrop.count).to.equal(5); + expect(analytics.track).to.not.be.calledWith('drop cap reached'); + }); + }); }); }); diff --git a/website/client/src/assets/images/swords.png b/website/client/src/assets/images/swords.png new file mode 100644 index 0000000000000000000000000000000000000000..81f057ec387260dc89ff7c7f9e2c5cf6570e5161 GIT binary patch literal 6289 zcmV;C7;fi@P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91aG(PK1ONa40RR91FaQ7m0ETf1_y7PG`$jc(VKGT-hb|Q|Ns7NMxtRZ z^XAR7FGPYY%Ve77f9=|}YsH1?UG8gh=gxh{;c(n&S=I%c-nnz9bioRPd{G(X_Ajn^ zNg^X2CsQ{>ecdv9?&|_DaiQnz;O_*3K?49%GJp<80xLyPN!6T0_66tO;1h`+Mg)Ki z-c^ehQt{L)Nj5FIdi0o{hnE0uV`F3K#*G`F^?JSHJfFk1>En+--q}-e06Pm8E}UoE z_TAV-D*%tyu3cN2pPx^*)ddo;t3Dkf2}5@L*go2|^IcL^fH=%$L;z^&x&UwPjW>{D zn6#{@q}Q;NmzNhk^UO1A7&sne=1N`HccIc{0njjvFQ}@z2BI1hgLrb#oH=u7=FFMB z=s0vNifb+c0rbiCcm5y1?$O}^3x{IV-sUF-92m}z3+d!mRg!Vdz`$nYj2@z(-7D)w{*(k*GRJBxP+btuj+^ZkRK1&^lT~y3sapGh$=43g*E`S z9em7dl0>msj6$IhU)K3we@Y}0(YcLSEGlm2CD67_A*PugF08mXRGA#0i6sSZOxs8) zBL+Yo5FVRo+@ue7G=T$clHw}mX1XbkbukDW~Ew|i4KA&&gLk~Ul zq08l>cszdZo;`bxU1)G;&z?P5)3oQK(I`Fk*kj`|Gc(EU%AlH4m<(vYJs20aA3NnI zhbB{~B}AvUR|^2msDb5g_!XDr=NmA9zr52wKSfUc?C#A{Vj3s6ei8eukg!r9I~0>&a_2^7(3$+lBI#cz!?g-513L%G=@4hl@^bqT$hvT>gqh;#%sA900?w1T)OI9 z%N#s;@?;YT;A22gs{< zcU~$dc6(UnN%WMz;=>0dcPbTr<-wkDVG!FpV)VhzCURme%3P{pKmoXsE9&Is<x!(*EPDL$$9r9hL*w*?<0Ju)`VUW$6*s70 z=|s{}KNpw0_Wk+DYZNj%SOUCNzGvUJdo^;A2R!L9;pdSpFR*M&4cg&vU@YJN)@FD0 zMK;l^TE76?cw0wi+|ZX`(J18Bm=Kce`5`M1!(qQff}}b%R#qfM>qQ_R@Ocg3sn}R- zNir)kXP0h{K%MByl`DIxiS_mMQReRMIzkuXCa~^tL9=)SaGI7F5}~XlF7^CZAwD(R zg*j`VO=M543-4fzN-c@z!$Tm%J;tsOYy1+7>6>eRm*Knc-Dh9z7l2(IZTjV1fvK3Z zD`iPO?R9uwxm4~uo1D+j@L;FAyp-)uBj;E*Wu&t>lj*BduOb!ep(^*nN8Gq!!v+Bm z?82L&*fhg)gH;TsKrrZj?5B@;7sA`);QNyC$9_(>lNMy;!1haAVE1Qcnu|Oxl^kx3 zYFRzKOoi7xtbhAdSJrI|9wc+w123di^tXA|%llm(cpzbqKM^qEo{YJE8C}SN1I_DJhhemWKMO76=5g09;lu6wKm%RaG>; zmf}rGa2?;?&3$N#O&7NHfUq!E6!fV&v2p(jXu+nSU#1bo-M8;cIexILFv4TTW5;87 zDfjlR+rxpZ6>-CTEBv)%)1klloz32kzgF+BT?cna{{z324kCV%EUBrc;b_Ks%Z>Sv#$Yb-yb2qHF$*L59+4ghIWs4 z_ut=NUh~$k_PyDk`;)N}bMTov0;5b@E|@yOb709jsauooPul~&Xa4;8S+)^xv-B8^ zT6QnlSw*yH(rEgZugyDW+W{EiZyY^D94N?krvpHR8V>BE-#qbip=EB}x>ekFran$> ztuRlZ-L{2v0pQ%3*>KNwiQ;Z4*~OLb{dzyu>~E!}a3%d=|C53PRawj4v13R3ISCn} zlUvu>O2x_W)egz3`_@aT%?hK*;E}`fo~(Ki&Ij`C?Id1pK>yQ}*fSzZteCjCVJ&v=nP#m)IP3@cNFA zjjph!XQVa3J6$0MmEGnxQ!BoUWP3zUUB;PLQQR#q;$=bn3h!QwCPi>t_#RJwXf z4i${frp7=6#mip>Xfb-|yWgRLF=K=#%JP!EDC0`txdB=Sk9JUNb3|yH<8GJ&5GBfi za-_jjXglen!*3iyyE?snaFe(%&>8q1`u+$s1e0kRY5Nxq;L~eu5e~n9qcmg>1ba?6 zrr|Ej!$5+=Yzj>FlMd(4PG}Xoo2d4XMz$WMFMs*VBD_SFivoUwbhyUCFcp{s^K;$8 zhnkzo`iBi|6@apy4bjmvt*a2?jojS-Yth5kqxwiQ}9k_0Lb? z(`%c(mf!OlRuz)dqO0!TIBLkaurF0}W|g+W#K?B%Pb4eYO5?|krk%FOnv^+sgNRmeaS- zp*rr*ICtsJ=O^*$)xH^K!2J`68ifm~PS6EY=XZcVQcMbmk z{*LnnMd;Ia1Xo*zeOEG@LLZoF8OnNGd*S|*DHI-iBQyQyEjm0q?M*j znL=^cgjyo$?d%Q*`OHAZ`GOqC03ne1eJE@_{yN_0bHf2%Sy`DG4F_G3aL@x^qRdfv zw>y!$!jS|UtY^a{VAH{h>SV=C#QFdzHgK7Mf&Hrrf(g9wnrKT%WHNO*8VY-2;joMQ z7-YQ8YJMQ&oXlKjHQaq7A)8%T10kLUc@11V>F;we?i+zYqOj<5!9|+^j*=8<)#`_C zmL!LE&DB>?(U?4VSS&hK7l17%Q_H~y(*04AJpc%)7zj>L(Zn1$#vK%`Z6l*ILam)G zRJP?M#1@@2>*o2CnUzjDux$*q0(CpS<=M?|Ns!6MOb@jaq7mJ6;8(X|YiaK`R2-br zpK-pR2<;}5m6i>6_+RJa!B$}ONJz6BDJ$97RuFlRu*TVhGz$DN$CS=qLMezUsIdqI zIt_|qEn}sHIV=hhYP1GPE6yU%w0s0;Ey_qmB*CT7!H~uwdFOlM(Oc6b}(Y#Cl4W{yiRp!7BY3=NUMgj5s!(qR5enha;3xFa}%)?@uGR9vW?`*F%$v3WV8F+jyq5Y3}v;WLPb<_r2Ny zKlC{oxR`NH#!86L?!as!xHz!*)H!Ins8jPwR&8=f&efyGX430_SugIn|F$2|_V+%e z>5JyjT@QVSVv#6vSvcsQ5U@6DnZXzZw^s`HSo#;QAUPeuUvOM;wwH8dxb1lPQ?cH? zUGZCR-d@Tn98YO;{z16WX3cgZ8o_HO3tJSlr*aGuYwL!ezexLc?5FF#G@su8)nkI5 z313}JRZXoV8OCOF@4I)4lSBNkFIF<6$x(HtgMX3RvUrGT65T<&ja#+>Zwp)5iNc9X zdR#}Ooa1*8Yh;Q5D-SYntdOW2V8y_*Pm^;;Em-;DeFnBAu&GFP;!Q(5kAt!jR1{Pd zh3^&TVUDm#BHDsU!XlK@{V`j_OE+EQhyj9w6%E*l+{2y=L$h-(ti+@AMRjP%>D|lx z@vEN2dbcLSn@K;uZv%j^$X|@mTSknIAF86V-~J9^Ar_IJ7r479GbfwAzGgMD)C?*v z$fOixlKJ3asohlrv1-w4A0puiPx*yqOKHY}t0{_hn~azMYx8f)kk0P|pa~7NyJZW0 z)g^21qT(4-DC#Hj*Lg`nKF;rd_8^7)9he~IbLO7^xv#taQ2zGCO00n#)(7Y14m7-f zIUa-!?pPbYZYERyf?P^NAikxcgQ||4Kstm69Gd9G>4b)eb&rb!ii!ztaRWRCFbKs* z_k&}Rrtgoo2~$DCy%Q%)AYUj<{xenJJO={2K5A-Y?Gs!T>t-sTjdvfsxmh&6WD+&{ zB2;mtl{nXmeX+w``eN0i#C-eZ)NR8}9QV&$)z;PtaftU6ww@&Y(@qNbqO@YwO#n=z zG=%M#VGbYxyXs^e40s$s;o()RwW^JR$j&`oQw{gnC&I&`q~}5KT5=GE`u>xx2m)gb zMLrnYihQpG@FtcN)57UfQIF^d6dGPjnoiZwU)p_0zi?7pXTAtIaky$Elw_=W032?@ zFrHY(zPlP@vk37O@F#T4Z5*?P=$z;wqy(^0(zKKcz4{XspKilCj~Uf zSTk9PLgC~sD?uwC_P<=VaqB+`MBNEC+{VBYqSnbPsX{taOhtyORYqZxJ-q%=x^nhp zidG|&Y#TaJY&vqN5|}vXgQ`E!;pW|xmpPhlyJ3|Op=EnFP)i6I8Mgg8-g#KXbafZJ zL-86o{8l9+kH%jAP1Lt&%Udr~XImpV$CZ+2=2y_XL8VttVhKvq=Xof<&_l;}AE0ON z`!`ZNE=&-sgo@K^scb5sU)OJWdO63f-yF!vP!*Md8IUF%Xrm+5k=aPH+1LO4&~+99 zezeH3k`u=uvhaR_+YPS^);MHgMqp=fMm7|GL>W{lIVM6!VT!wB5S1zbK|R!AK18wm z`ZO2Ln`3hd2D`=-Xe11v4)JLPLoihU4c1o+;+EG%?%#>NJqnxx01g+#i#5V6BYdSy zY5&UQ9=8F>@@LoVQT6bZ)A&&a&Vh*mSv2c!ilB7Vn5ID!ix!aElSVmmAx&4Wg99W^ zTMoYhB{4wr;m4dXYb>=Pck{hh-xf)?4V_h}11Gj^m+d_A=GT*lIA_jve*iiwC9JKk z+#+FHF1qU`${d|T$MzkkkKf!*N=ho_ExZ+C6?Iy6kP{J!UA>)^Ud-oIS=q`DG_*|4^4)r69Afb@_;SL6g~!j+2QrBQ0Dun~ol6reFNcjpmZc=-ha7Muy^Hd5)X)m2co^^e z4%60IMLfZ_! zyIvUVQ?(dnD)883<{DJU3#{)AlgUqS!C}ko^*z#n| zQC!Y4_&=feL>I`WS&z4SZ-(>xpEyp4H-U?jdKvQFr1?AKeTxnwkx&r>h(YPtz58(O z;>C;oVZ$%LL56k9-cR=?Ykkt(lE3q^m!`{F5N+6SILk0CbUS)xZ)I6>{m&n2@7n^< zw$f#zngej@Ft=g)CT4KgJT?t4?fG*xPTJkd$~D(qGhTtn=12!#TRXDtqmMdfe&MPP zNmGjWyE4OFh8@7=H+)09SOWj$a&Q%Yuz*7j&#`9(N=<$80Ij2S(x2$)QxIn<$>({W zV}pthKi3m0Afu{U_x}2=B?S>ZHe&h%1?U^%Y*bu30BXvNSwz)G(?b{#!iM;`c{@g~6 z;xLzC=RkH)bKyvP*Zb@6iH_J31dUk+LQgtmxE>A-JE8vzAvo;71;6Sv00000NkvXX Hu0mjfPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91;GhEl1ONa40RR91U;qFB029BIrT_pOut`KgRCodHT?uepN16WLn^U7> zELpM@%eE9B2@r^E94EGMB01S4wILL>6b3ff!CM@~CcuVD5vbbQwaikrfh|G;MfM7m z$*~lhf~^8#*er)kCAJ|^d?Y3T$JSvdmUYdHH21uDZ~uQZ?{)W^(TprhGb2rRmGrv* zKHoR}zwZ9K|GyQea<^^UHeBCu2tkPNs~eo6TrixL6JfQ#~HlZ{>~2soF@i_F)1D5?r|%lv@rIjcwq z(mamhP-&Xhx?{(Vb}M5ZkK64|_xAR_g;Ddi_RLewTu4fHcXwMdnRFD&+|<<6>U0*z zB9$^g*V!xLx)CNP$C0KND!%ICiX&NNkP!3j{?dcssj0EvyxCU^w_Ls63eV(u=%I(6 z7#kZ)&lFGeL?SV?Wy_YE`}+Dqh0>Ke4%^g#Z99aFvbIp&Yu2oBeBEiJyJpS!3LohgI(WD1JGN7#&(8x#3=kYNH^w#>IuCC6OGH}Hdj|yL35!NzoO+KNR8F9SLI}^tYCs1g@WGS$AZSGn)fob9i zap@bKh(Qc-G##x04Jm*z;jo^P0WBz|J=Ecj9UlSJ<+Wm38a-e*H7iV>cdxt(u3OP6 z(C)tXUf9Vopi@;~Yg-$5olLMeJUraGckkYxW9PT>qMuIBbX$ZHc<@8;C|1+OSJkt> z2U=U%J83A%>^~bu$t6ow<74k)QZJ3hVd$-h8O%K2Ix+~cF-A*janRx-hC+ra4Y1E$ zl>#7NTOv?S1SA8N6Mb2C)gvGou}bsz^~(|nDJ-? z;u>4$knm+F6a<&b)=lxW4tw_Q1uez)$U>n|2WIcVKi>7Cra&)Gn5!Q(KDse4{aJ4J z?%f^!21^D5E)*yw6D}AVWr7n;(;=En3nyAqhMu$D8}1Ll#Mz)#HhHvYQV7dHegq@~ z=Ep^#M&fCB-Nt|w?F&rv-{0( z_DNOr+GukPlCVW6e^>V|*t~hORUQdiNZ>LaVT?l(;V=qLgbxk!XE4mtPyxhz6R%&g z%95VhBTx}j(8unb+BN$o%ajsD|O(5{w#-jcuZ_*QAH1>!Oq^rQ|lY*bIRxA zO@9g{v`LQHQYZn9k4GV$x_mf@`7Va~&7$d<#$#^B5br^rSW17T^f~^WOU{7PUm&%c zjeJ~l3}`_n{KR8>CI?kT?=2-Ga;=cwC5AvH_qmd;0ZH+Ray6^0n z>Mop4#!3VVBT(smuB0R2-#uCXTl^XgOfP;5F`-fMBcOt7INImqodef=H0?B<3&U;) z(zuOyS4P`_r7_dmpJ=)wpAv8#YOY(;&_Eynjg5^$R?=z^2pFKUX5$<pvnPCKn1NP$rm(j{F3T6JHhr^fAp z<+b%ft{dM2xaMOgT-*)^-1&};K&nfoqpmv5geE2b^Ftr{5ZrOc9YW-R0|%ZsapHuv z$ZTn8iDsF>XC~aRVS{J?Kkhe!ndfXY*opD3d&e?C^(doagbmg-!z#~>4A|&n??Vw$ z8!d6lAK_3-f?w>Y9xoU_IaN}4esjp{T~@-AmUWhd{q`qrcBgR~H-vOHtuqWyIvNtDe~D;4OxaHSyD|b7wmx#A zz=fu&#hi|t;PD&_Vxo|THp*+zJR+|9_~INi<>bvsG&VM&rUjcaQew)>ndr>qce}7{ zMLxeYwe)y!VZcp6RcdS4T83uIipC-kPKeqNSSxL~^9d&j+f0Ts|Er`vm(x2)I8x

gPPI>0LXZ@mu*{{&+rBUZWPU`APtRa)zPWw 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 Q%m4rY07*qoM6N<$f*v6``Tzg` literal 0 HcmV?d00001 diff --git a/website/client/src/assets/images/swords@3x.png b/website/client/src/assets/images/swords@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3bca6d2326f5d4962cd22c2d4c0ce1122db98e27 GIT binary patch literal 12365 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 + +

+ +

+ {{ $t('dropCapReached') }} +

+
+
+
+

+ {{ maxItems }} +

+ {{ $t('items') }} +
+
+
+

+ {{ $t('dropCapExplanation') }} +

+ + {{ $t('dropCapLearnMore') }} + +
+ + + + + + + + + diff --git a/website/client/src/components/categories/categoryTags.vue b/website/client/src/components/categories/categoryTags.vue index f49bc7985e..6878185b52 100644 --- a/website/client/src/components/categories/categoryTags.vue +++ b/website/client/src/components/categories/categoryTags.vue @@ -22,12 +22,15 @@ export default { props: { categories: { + type: Array, required: true, }, owner: { + type: Boolean, default: false, }, member: { + type: Boolean, default: false, }, }, diff --git a/website/client/src/components/header/messageCount.vue b/website/client/src/components/header/messageCount.vue index f63685ba3b..90cea5d5e6 100644 --- a/website/client/src/components/header/messageCount.vue +++ b/website/client/src/components/header/messageCount.vue @@ -10,7 +10,7 @@ @import '~@/assets/scss/colors.scss'; .message-count { - background-color: $blue-50; + background-color: $red-50; border-radius: 50%; height: 20px; width: 20px; @@ -31,7 +31,6 @@ right: 0.3em; top: -0.8em; padding: 0.2em; - background-color: $red-50; } .message-count.top-count-gray { diff --git a/website/client/src/components/header/notifications/base.vue b/website/client/src/components/header/notifications/base.vue index e1e79896c7..1a0c03b9c1 100644 --- a/website/client/src/components/header/notifications/base.vue +++ b/website/client/src/components/header/notifications/base.vue @@ -118,11 +118,11 @@ } .notification-remove { - position: absolute; - width: 18px; - height: 18px; - padding: 4px; - right: 24px; + position: relative; + width: 10px; + height: 10px; + right: 0px; + top: 10.5px; .svg-icon { width: 10px; diff --git a/website/client/src/components/header/notifications/dropCapReached.vue b/website/client/src/components/header/notifications/dropCapReached.vue new file mode 100644 index 0000000000..34a133b64e --- /dev/null +++ b/website/client/src/components/header/notifications/dropCapReached.vue @@ -0,0 +1,43 @@ + + + diff --git a/website/client/src/components/header/notificationsDropdown.vue b/website/client/src/components/header/notificationsDropdown.vue index a8e600dfe8..f5b9a3b807 100644 --- a/website/client/src/components/header/notificationsDropdown.vue +++ b/website/client/src/components/header/notificationsDropdown.vue @@ -149,6 +149,7 @@ import ACHIEVEMENT_MIND_OVER_MATTER from './notifications/mindOverMatter'; import ONBOARDING_COMPLETE from './notifications/onboardingComplete'; import GIFT_ONE_GET_ONE from './notifications/g1g1'; import OnboardingGuide from './onboardingGuide'; +import DROP_CAP_REACHED from './notifications/dropCapReached'; export default { components: { @@ -178,6 +179,7 @@ export default { OnboardingGuide, ONBOARDING_COMPLETE, GIFT_ONE_GET_ONE, + DROP_CAP_REACHED, }, data () { return { @@ -203,7 +205,7 @@ export default { 'GROUP_TASK_CLAIMED', 'NEW_MYSTERY_ITEMS', 'CARD_RECEIVED', 'NEW_INBOX_MESSAGE', 'NEW_CHAT_MESSAGE', 'UNALLOCATED_STATS_POINTS', 'ACHIEVEMENT_JUST_ADD_WATER', 'ACHIEVEMENT_LOST_MASTERCLASSER', 'ACHIEVEMENT_MIND_OVER_MATTER', - 'VERIFY_USERNAME', 'ONBOARDING_COMPLETE', + 'VERIFY_USERNAME', 'ONBOARDING_COMPLETE', 'DROP_CAP_REACHED', ], }; }, diff --git a/website/client/src/components/header/userDropdown.vue b/website/client/src/components/header/userDropdown.vue index ecfbf2a4ad..ec0bc2547a 100644 --- a/website/client/src/components/header/userDropdown.vue +++ b/website/client/src/components/header/userDropdown.vue @@ -23,13 +23,6 @@ slot="dropdown-content" class="user-dropdown" > - -

{{ user.profile.name }}

- {{ $t('editAvatar') }} -
{{ $t('backgrounds') }} + @click="showAvatar('body', 'size')" + >{{ $t('editAvatar') }} + {{ $t('profile') }} {{ $t('stats') }} {{ $t('achievements') }} - {{ $t('profile') }} {{ $t('logout') }} @@ -96,39 +106,30 @@ diff --git a/website/client/src/components/members/classBadge.vue b/website/client/src/components/members/classBadge.vue index 2666952e6a..b6ce43d7d7 100644 --- a/website/client/src/components/members/classBadge.vue +++ b/website/client/src/components/members/classBadge.vue @@ -2,8 +2,8 @@
diff --git a/website/client/src/components/notifications.vue b/website/client/src/components/notifications.vue index 206ebabd3d..d6c8e4c563 100644 --- a/website/client/src/components/notifications.vue +++ b/website/client/src/components/notifications.vue @@ -35,6 +35,7 @@ + @@ -145,6 +146,7 @@ import loginIncentives from './achievements/login-incentives'; import onboardingComplete from './achievements/onboardingComplete'; import verifyUsername from './settings/verifyUsername'; import firstDrops from './achievements/firstDrops'; +import DropCapReachedModal from '@/components/achievements/dropCapReached'; const NOTIFICATIONS = { CHALLENGE_JOINED_ACHIEVEMENT: { @@ -384,6 +386,7 @@ export default { justAddWater, onboardingComplete, firstDrops, + DropCapReachedModal, }, mixins: [notifications, guide], data () { diff --git a/website/client/src/components/shops/buyModal.vue b/website/client/src/components/shops/buyModal.vue index df11994c8f..170b5436b8 100644 --- a/website/client/src/components/shops/buyModal.vue +++ b/website/client/src/components/shops/buyModal.vue @@ -7,9 +7,9 @@ @@ -154,8 +154,8 @@ !enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)" :class="{'notEnough': !preventHealthPotion || !enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}" - @click="buyItem()" tabindex="0" + @click="buyItem()" > {{ $t('buyNow') }} diff --git a/website/client/src/components/shops/shopItem.vue b/website/client/src/components/shops/shopItem.vue index 4c0608a63a..f3bc30e77c 100644 --- a/website/client/src/components/shops/shopItem.vue +++ b/website/client/src/components/shops/shopItem.vue @@ -3,9 +3,9 @@
{{ $t(filter) }}
diff --git a/website/client/src/components/tasks/task.vue b/website/client/src/components/tasks/task.vue index a434bc6a8f..4ddc668f2d 100644 --- a/website/client/src/components/tasks/task.vue +++ b/website/client/src/components/tasks/task.vue @@ -36,11 +36,11 @@ 'task-not-scoreable': isUser !== true || (task.group.approval.requested && !task.group.approval.approved), }, controlClass.up.inner]" + tabindex="0" @click="(isUser && task.up && (!task.group.approval.requested || task.group.approval.approved)) ? score('up') : null" @keypress.enter="(isUser && task.up && (!task.group.approval.requested || task.group.approval.approved)) ? score('up') : null" - tabindex="0" >