Challenge Won Notification improvements (#12762)

* challenge won notification: add more info

* update tests

* use new notification on web, fixes #7716

* wip design

* finalize design

* fix markdown rendering
This commit is contained in:
Matteo Pagliazzi
2020-11-10 18:47:13 +01:00
committed by GitHub
parent 4319bd5ad1
commit 181b33101e
19 changed files with 261 additions and 124 deletions

View File

@@ -103,7 +103,15 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => {
await expect(winningUser.sync()).to.eventually.have.nested.property('achievements.challenges').to.include(challenge.name); await expect(winningUser.sync()).to.eventually.have.nested.property('achievements.challenges').to.include(challenge.name);
// 2 because winningUser just joined the challenge, which now awards an achievement // 2 because winningUser just joined the challenge, which now awards an achievement
expect(winningUser.notifications.length).to.equal(2); expect(winningUser.notifications.length).to.equal(2);
expect(winningUser.notifications[1].type).to.equal('WON_CHALLENGE');
const notif = winningUser.notifications[1];
expect(notif.type).to.equal('WON_CHALLENGE');
expect(notif.data).to.eql({
id: challenge._id,
name: challenge.name,
prize: challenge.prize,
leader: challenge.leader,
});
}); });
it('gives winner gems as reward', async () => { it('gives winner gems as reward', async () => {

View File

@@ -2,101 +2,186 @@
<b-modal <b-modal
id="won-challenge" id="won-challenge"
:title="$t('wonChallenge')" :title="$t('wonChallenge')"
size="md" size="sm"
:hide-footer="true" :hide-header="true"
> >
<div class="modal-body text-center"> <close-icon @click="close()" />
<h4 v-markdown="user.achievements.challenges[user.achievements.challenges.length - 1]"></h4> <div
<div class="row"> class="text-center"
<div class="col-4"> >
<h1
v-once
class="header purple"
>
{{ $t('wonChallenge') }}
</h1>
<div class="d-flex align-items-center justify-content-center mb-4">
<div
v-once
class="svg-icon sparkles sparkles-rotate"
v-html="icons.sparkles"
></div>
<div class="achievement-karaoke-2x"></div> <div class="achievement-karaoke-2x"></div>
<div
v-once
class="svg-icon sparkles"
v-html="icons.sparkles"
></div>
</div> </div>
<div class="col-4"> <p
<!-- @TODO: +generatedAvatar({sleep: false})--> class="mb-4 chal-desc"
<avatar v-html="$t('wonChallengeDesc', {challengeName: challengeName})"
class="avatar" >
:member="user" </p>
:avatar-only="true"
/>
</div> </div>
<div class="col-4"> <div
<div class="achievement-karaoke-2x"></div> v-if="notification"
slot="modal-footer"
class="pt-3 w-100"
>
<div class="d-flex align-items-center justify-content-center mb-3">
<div
v-once
class="svg-icon stars"
v-html="icons.stars"
></div>
<strong v-once>{{ $t('yourReward') }}</strong>
<div
v-once
class="svg-icon stars stars-rotate"
v-html="icons.stars"
></div>
</div> </div>
<div class="d-flex align-items-center justify-content-center mb-4">
<div
v-once
class="svg-icon gem mr-1"
v-html="icons.gem"
></div>
<strong>{{ notification.data.prize }}</strong>
</div> </div>
<p>{{ $t('congratulations') }}</p>
<br>
<button <button
v-once
class="btn btn-primary" class="btn btn-primary"
@click="close()" @click="close()"
> >
{{ $t('hurray') }} {{ $t('onwards') }}
</button> </button>
</div> </div>
<div class="modal-footer">
<div class="col-3">
<a
class="twitter-share-button"
href="https://twitter.com/intent/tweet?text=#{tweet}&via=habitica&url=#{env.BASE_URL}/social/won-challenge&count=none"
>{{ $t('tweet') }}</a>
</div>
<div
class="col-4"
style="margin-left:.8em"
>
<div
class="fb-share-button"
data-href="#{env.BASE_URL}/social/won-challenge"
data-layout="button"
></div>
</div>
<div
class="col-4"
style="margin-left:.8em"
>
<a
class="tumblr-share-button"
data-href="#{env.BASE_URL}/social/won-challenge"
data-notes="none"
></a>
</div>
</div>
</b-modal> </b-modal>
</template> </template>
<style scoped> <style lang="scss">
.achievement-karaoke-2x { @import '~@/assets/scss/colors.scss';
margin: 0 auto;
margin-top: 6em; #won-challenge {
.modal-body {
padding: 0 1.5rem;
} }
.avatar { .modal-footer {
width: 140px; background: $gray-700;
margin: 0 auto; border-top: none;
margin-bottom: 1.5em; padding: 0 1.5rem 2rem 1.5rem;
margin-top: 1.5em; }
.modal-dialog {
width: 20.625rem;
font-size: 0.875rem;
line-height: 1.71;
text-align: center;
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.purple {
color: $purple-300;
}
.header {
font-size: 1.25rem;
line-height: 1.4;
text-align: center;
margin-top: 2rem;
}
.sparkles {
width: 2.5rem;
height: 4rem;
margin-left: 2rem;
&.sparkles-rotate {
transform: rotate(180deg);
margin-right: 2rem;
margin-left: 0rem;
}
}
.stars {
width: 2rem;
height: 1.063rem;
margin-right: 1.25rem;
&.stars-rotate {
transform: rotate(180deg);
margin-left: 1.25rem;
margin-right: 0rem;
}
}
.gem {
width: 1.5rem;
height: 1.5rem;
}
.chal-desc ::v-deep p {
display: inline;
} }
</style> </style>
<script> <script>
import habiticaMarkdown from 'habitica-markdown';
import closeIcon from '@/components/shared/closeIcon';
import sparkles from '@/assets/svg/star-group.svg';
import gem from '@/assets/svg/gem.svg';
import stars from '@/assets/svg/sparkles-left.svg';
import { mapState } from '@/libs/store'; import { mapState } from '@/libs/store';
import markdownDirective from '@/directives/markdown';
import Avatar from '../avatar';
export default { export default {
components: { components: {
Avatar, closeIcon,
},
directives: {
markdown: markdownDirective,
}, },
data () { data () {
const tweet = this.$t('wonChallengeShare'); // const tweet = this.$t('wonChallengeShare');
return { return {
tweet, // tweet,
notification: null,
icons: Object.freeze({
sparkles,
gem,
stars,
}),
}; };
}, },
computed: { computed: {
...mapState({ user: 'user.data' }), ...mapState({ user: 'user.data' }),
challengeName () {
if (!this.notification) return null;
return habiticaMarkdown.render(String(this.notification.data.name));
},
},
mounted () {
this.$root.$on('habitica:won-challenge', notification => {
this.notification = notification;
this.$root.$emit('bv::show::modal', 'won-challenge');
});
},
beforeDestroy () {
this.$root.$off('habitica:won-challenge');
}, },
methods: { methods: {
close () { close () {

View File

@@ -2,7 +2,10 @@
<div class="row"> <div class="row">
<div class="standard-sidebar d-none d-sm-block"> <div class="standard-sidebar d-none d-sm-block">
<filter-sidebar> <filter-sidebar>
<div class="form-group" slot="search"> <div
slot="search"
class="form-group"
>
<input <input
v-model="searchText" v-model="searchText"
class="form-control input-search" class="form-control input-search"
@@ -13,11 +16,13 @@
<div class="form"> <div class="form">
<filter-group :title="groupBy === 'type' ? $t('equipmentType') : $t('class')"> <filter-group :title="groupBy === 'type' ? $t('equipmentType') : $t('class')">
<checkbox v-for="group in itemsGroups" <checkbox
:key="group.key" v-for="group in itemsGroups"
:id="groupBy + group.key" :id="groupBy + group.key"
:key="group.key"
:checked.sync="viewOptions[group.key].selected" :checked.sync="viewOptions[group.key].selected"
:text="group.label"/> :text="group.label"
/>
</filter-group> </filter-group>
</div> </div>
</filter-sidebar> </filter-sidebar>
@@ -47,9 +52,9 @@
:right="true" :right="true"
:items="sortGearBy" :items="sortGearBy"
:value="selectedSortGearBy" :value="selectedSortGearBy"
@select="selectedSortGearBy = $event"
class="inline" class="inline"
:inlineDropdown="false" :inline-dropdown="false"
@select="selectedSortGearBy = $event"
/> />
<span class="dropdown-label">{{ $t('groupBy2') }}</span> <span class="dropdown-label">{{ $t('groupBy2') }}</span>
@@ -81,7 +86,10 @@
:open-status="openStatus" :open-status="openStatus"
@toggled="drawerToggled" @toggled="drawerToggled"
> >
<div slot="drawer-title-row" class="title-row-tabs"> <div
slot="drawer-title-row"
class="title-row-tabs"
>
<div class="drawer-tab"> <div class="drawer-tab">
<a <a
class="drawer-tab-text" class="drawer-tab-text"

View File

@@ -4,10 +4,15 @@
toggle-class="with-icon" toggle-class="with-icon"
> >
<template v-slot:button-content> <template v-slot:button-content>
<span class="svg-icon inline color" <span
v-html="icons.unequipIcon"> class="svg-icon inline color"
v-html="icons.unequipIcon"
>
</span> </span>
<span class="button-label" v-once>{{ $t('unequip') }}</span> <span
v-once
class="button-label"
>{{ $t('unequip') }}</span>
</template> </template>
<b-dropdown-item <b-dropdown-item
@click="unequipBattleGear()" @click="unequipBattleGear()"
@@ -49,7 +54,7 @@ import {
import unequipIcon from '@/assets/svg/unequip.svg'; import unequipIcon from '@/assets/svg/unequip.svg';
export default { export default {
name: 'unequipDropdown', name: 'UnequipDropdown',
data () { data () {
return { return {
icons: Object.freeze({ icons: Object.freeze({

View File

@@ -6,7 +6,10 @@
> >
<div class="standard-sidebar d-none d-sm-block"> <div class="standard-sidebar d-none d-sm-block">
<filter-sidebar> <filter-sidebar>
<div class="form-group" slot="search"> <div
slot="search"
class="form-group"
>
<input <input
v-model="searchText" v-model="searchText"
class="form-control input-search" class="form-control input-search"
@@ -17,11 +20,13 @@
<div class="form"> <div class="form">
<filter-group :title="$t('equipmentType')"> <filter-group :title="$t('equipmentType')">
<checkbox v-for="group in groups" <checkbox
:key="group.key" v-for="group in groups"
:id="group.key" :id="group.key"
:key="group.key"
:checked.sync="group.selected" :checked.sync="group.selected"
:text="$t(group.key)"/> :text="$t(group.key)"
/>
</filter-group> </filter-group>
</div> </div>
</filter-sidebar> </filter-sidebar>
@@ -40,9 +45,9 @@
:right="true" :right="true"
:items="['quantity', 'AZ']" :items="['quantity', 'AZ']"
:value="sortBy" :value="sortBy"
@select="sortBy = $event"
class="inline" class="inline"
:inlineDropdown="false" :inline-dropdown="false"
@select="sortBy = $event"
/> />
</div> </div>
</div> </div>

View File

@@ -117,9 +117,9 @@
:right="true" :right="true"
:items="sortByItems" :items="sortByItems"
:value="selectedSortBy" :value="selectedSortBy"
@select="selectedSortBy = $event"
class="inline" class="inline"
:inlineDropdown="false" :inline-dropdown="false"
@select="selectedSortBy = $event"
/> />
</div> </div>
</div> </div>

View File

@@ -811,7 +811,7 @@ export default {
this.$root.$emit('bv::show::modal', 'rebirth-enabled'); this.$root.$emit('bv::show::modal', 'rebirth-enabled');
break; break;
case 'WON_CHALLENGE': case 'WON_CHALLENGE':
this.$root.$emit('bv::show::modal', 'won-challenge'); this.$root.$emit('habitica:won-challenge', notification);
break; break;
case 'STREAK_ACHIEVEMENT': case 'STREAK_ACHIEVEMENT':
this.text(`${this.$t('streaks')}: ${this.user.achievements.streak}`, () => { this.text(`${this.$t('streaks')}: ${this.user.achievements.streak}`, () => {

View File

@@ -32,7 +32,9 @@
<br> <br>
{{ $t('beeminderDesc') }} {{ $t('beeminderDesc') }}
</li> </li>
<li><div v-html="$t('chatExtension')"> </div> <li>
<div v-html="$t('chatExtension')">
</div>
<span>{{ $t('chatExtensionDesc') }}</span> <span>{{ $t('chatExtensionDesc') }}</span>
</li> </li>
<li> <li>
@@ -43,8 +45,10 @@
<br> <br>
{{ $t('dataToolDesc') }} {{ $t('dataToolDesc') }}
</li> </li>
<li><div v-html="$t('otherExtensions')"></div> <li>
<span>{{ $t('otherDesc') }}</span></li> <div v-html="$t('otherExtensions')"></div>
<span>{{ $t('otherDesc') }}</span>
</li>
</ul> </ul>
<hr> <hr>
</div> </div>

View File

@@ -1,7 +1,10 @@
<template> <template>
<button title="close dialog" <button
@click="$emit('click', $event)"> title="close dialog"
<div v-once @click="$emit('click', $event)"
>
<div
v-once
class="svg-icon" class="svg-icon"
v-html="icons.close" v-html="icons.close"
></div> ></div>
@@ -12,7 +15,7 @@
import svgClose from '@/assets/svg/close.svg'; import svgClose from '@/assets/svg/close.svg';
export default { export default {
name: 'closeIcon', name: 'CloseIcon',
data () { data () {
return { return {
icons: Object.freeze({ icons: Object.freeze({

View File

@@ -1,13 +1,19 @@
<template> <template>
<drawer <drawer
ref="drawer"
class="inventoryDrawer" class="inventoryDrawer"
:no-title-bottom-padding="true" :no-title-bottom-padding="true"
:error-message="inventoryDrawerErrorMessage(selectedDrawerItemType)" :error-message="inventoryDrawerErrorMessage(selectedDrawerItemType)"
ref="drawer"
> >
<div slot="drawer-title-row" class="title-row-tabs"> <div
<div class="drawer-tab" v-for="(tab, index) of filteredTabs" slot="drawer-title-row"
:key="tab.key"> class="title-row-tabs"
>
<div
v-for="(tab, index) of filteredTabs"
:key="tab.key"
class="drawer-tab"
>
<a <a
class="drawer-tab-text" class="drawer-tab-text"
:class="{'drawer-tab-text-active': filteredTabs[selectedDrawerTab].key === tab.key}" :class="{'drawer-tab-text-active': filteredTabs[selectedDrawerTab].key === tab.key}"

View File

@@ -62,15 +62,15 @@ export default {
user: 'user.data', user: 'user.data',
}), }),
}, },
methods: {
modForm () {
goToModForm(this.user);
},
},
mounted () { mounted () {
this.$store.dispatch('common:setTitle', { this.$store.dispatch('common:setTitle', {
section: this.$t('contactUs'), section: this.$t('contactUs'),
}); });
}, },
methods: {
modForm () {
goToModForm(this.user);
},
},
}; };
</script> </script>

View File

@@ -7,7 +7,9 @@
> >
<div class="title-row"> <div class="title-row">
<slot name="drawer-title-row"> <slot name="drawer-title-row">
<div class="text-only">{{ title }}</div> <div class="text-only">
{{ title }}
</div>
</slot> </slot>
</div> </div>
<div <div

View File

@@ -6,15 +6,15 @@
@click.stop="click" @click.stop="click"
> >
<div <div
v-once
class="svg-icon color equip-icon" class="svg-icon color equip-icon"
v-html="icons.equip" v-html="icons.equip"
v-once
> >
</div> </div>
<div <div
v-once
class="svg-icon color unequip-icon" class="svg-icon color unequip-icon"
v-html="icons.unEquip" v-html="icons.unEquip"
v-once
> >
</div> </div>
</span> </span>

View File

@@ -11,7 +11,7 @@
<script> <script>
export default { export default {
name: 'filterGroup', name: 'FilterGroup',
props: ['title'], props: ['title'],
}; };
</script> </script>

View File

@@ -3,7 +3,10 @@
<slot name="header"></slot> <slot name="header"></slot>
<slot name="search"></slot> <slot name="search"></slot>
<div class="form"> <div class="form">
<h3 v-once class="filter-label"> <h3
v-once
class="filter-label"
>
{{ $t('filters') }} {{ $t('filters') }}
</h3> </h3>
<slot></slot> <slot></slot>
@@ -13,7 +16,7 @@
<script> <script>
export default { export default {
name: 'filterSidebar', name: 'FilterSidebar',
}; };
</script> </script>

View File

@@ -19,8 +19,8 @@
</div> </div>
<show-more-button <show-more-button
v-if="items.length > itemsPerRow" v-if="items.length > itemsPerRow"
@click="toggleItemsToShow()"
:show-all="showAll" :show-all="showAll"
@click="toggleItemsToShow()"
/> />
<div <div
v-else v-else

View File

@@ -1,5 +1,6 @@
<template> <template>
<button class="btn btn-flat btn-show-more mb-4" <button
class="btn btn-flat btn-show-more mb-4"
@click="$emit('click')" @click="$emit('click')"
> >
<span class="button-text"> <span class="button-text">

View File

@@ -98,5 +98,7 @@
"viewProgress": "View Progress", "viewProgress": "View Progress",
"selectMember": "Select Member", "selectMember": "Select Member",
"confirmKeepChallengeTasks": "Do you want to keep challenge tasks?", "confirmKeepChallengeTasks": "Do you want to keep challenge tasks?",
"selectParticipant": "Select a Participant" "selectParticipant": "Select a Participant",
"yourReward": "Your Reward",
"wonChallengeDesc": "<%= challengeName %> selected you as the winner! Your win has been recorded in your Achievements."
} }

View File

@@ -379,7 +379,12 @@ schema.methods.closeChal = async function closeChal (broken = {}) {
winner.balance += challenge.prize / 4; winner.balance += challenge.prize / 4;
} }
winner.addNotification('WON_CHALLENGE'); winner.addNotification('WON_CHALLENGE', {
id: challenge._id,
name: challenge.name,
prize: challenge.prize,
leader: challenge.leader,
});
const savedWinner = await winner.save(); const savedWinner = await winner.save();