mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 14:17:22 +01:00
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:
@@ -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 () => {
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`, () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'filterGroup',
|
||||
name: 'FilterGroup',
|
||||
props: ['title'],
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
</div>
|
||||
<show-more-button
|
||||
v-if="items.length > itemsPerRow"
|
||||
@click="toggleItemsToShow()"
|
||||
:show-all="showAll"
|
||||
@click="toggleItemsToShow()"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
|
||||
@@ -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') }}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user