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);
// 2 because winningUser just joined the challenge, which now awards an achievement
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 () => {

View File

@@ -2,101 +2,186 @@
<b-modal
id="won-challenge"
:title="$t('wonChallenge')"
size="md"
:hide-footer="true"
size="sm"
:hide-header="true"
>
<div class="modal-body text-center">
<h4 v-markdown="user.achievements.challenges[user.achievements.challenges.length - 1]"></h4>
<div class="row">
<div class="col-4">
<div class="achievement-karaoke-2x"></div>
</div>
<div class="col-4">
<!-- @TODO: +generatedAvatar({sleep: false})-->
<avatar
class="avatar"
:member="user"
:avatar-only="true"
/>
</div>
<div class="col-4">
<div class="achievement-karaoke-2x"></div>
</div>
<close-icon @click="close()" />
<div
class="text-center"
>
<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
v-once
class="svg-icon sparkles"
v-html="icons.sparkles"
></div>
</div>
<p
class="mb-4 chal-desc"
v-html="$t('wonChallengeDesc', {challengeName: challengeName})"
>
</p>
</div>
<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 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>
<p>{{ $t('congratulations') }}</p>
<br>
<button
v-once
class="btn btn-primary"
@click="close()"
>
{{ $t('hurray') }}
{{ $t('onwards') }}
</button>
</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>
</template>
<style scoped>
.achievement-karaoke-2x {
margin: 0 auto;
margin-top: 6em;
<style lang="scss">
@import '~@/assets/scss/colors.scss';
#won-challenge {
.modal-body {
padding: 0 1.5rem;
}
.modal-footer {
background: $gray-700;
border-top: none;
padding: 0 1.5rem 2rem 1.5rem;
}
.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;
}
.avatar {
width: 140px;
margin: 0 auto;
margin-bottom: 1.5em;
margin-top: 1.5em;
.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>
<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 markdownDirective from '@/directives/markdown';
import Avatar from '../avatar';
export default {
components: {
Avatar,
},
directives: {
markdown: markdownDirective,
closeIcon,
},
data () {
const tweet = this.$t('wonChallengeShare');
// const tweet = this.$t('wonChallengeShare');
return {
tweet,
// tweet,
notification: null,
icons: Object.freeze({
sparkles,
gem,
stars,
}),
};
},
computed: {
...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: {
close () {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,9 +5,11 @@
:class="{'no-padding': noTitleBottomPadding}"
@click="toggle()"
>
<div class="title-row">
<div class="title-row">
<slot name="drawer-title-row">
<div class="text-only">{{ title }}</div>
<div class="text-only">
{{ title }}
</div>
</slot>
</div>
<div

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
<template>
<button class="btn btn-flat btn-show-more mb-4"
@click="$emit('click')"
<button
class="btn btn-flat btn-show-more mb-4"
@click="$emit('click')"
>
<span class="button-text">
{{ showAll ? $t('showLess') : $t('showMore') }}

View File

@@ -98,5 +98,7 @@
"viewProgress": "View Progress",
"selectMember": "Select Member",
"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.addNotification('WON_CHALLENGE');
winner.addNotification('WON_CHALLENGE', {
id: challenge._id,
name: challenge.name,
prize: challenge.prize,
leader: challenge.leader,
});
const savedWinner = await winner.save();