old client structure
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
<template lang="pug">
|
||||
.row
|
||||
.col-12.text-center
|
||||
// @TODO i18n. How to setup the strings with the router-link inside?
|
||||
img.not-found-img(src='~assets/images/404.png')
|
||||
h1.not-found Sometimes even the bravest adventurer gets lost.
|
||||
h2.not-found Looks like this link is broken or the page may have moved, sorry!
|
||||
h2.not-found Head back to the <router-link to="/">Homepage</router-link> or <router-link :to="contactUsLink">Contact Us</router-link> about the issue.
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState(['isUserLoggedIn']),
|
||||
contactUsLink () {
|
||||
if (this.isUserLoggedIn) {
|
||||
return {name: 'guild', params: {groupId: 'a29da26b-37de-4a71-b0c6-48e72a900dac'} };
|
||||
} else {
|
||||
return {name: 'contact'};
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.col-12 {
|
||||
margin-bottom: 120px;
|
||||
}
|
||||
|
||||
.not-found-img {
|
||||
margin-top: 152px;
|
||||
margin-bottom: 42px;
|
||||
}
|
||||
|
||||
h1.not-found {
|
||||
line-height: 1.33;
|
||||
color: $purple-200;
|
||||
margin-bottom: 8px;
|
||||
font-weight: normal;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
h2.not-found {
|
||||
line-height: 1.4;
|
||||
font-weight: normal;
|
||||
color: $gray-200;
|
||||
margin-bottom: 0px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
<template lang="pug">
|
||||
div(:class='achievementClass')
|
||||
//- +generatedAvatar({sleep: false})
|
||||
avatar(:member='user', :avatarOnly='true', :withBackground='true')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import Avatar from '../avatar';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Avatar,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
achievementClass: `achievement-${this.badge}2x`,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
}),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template lang="pug">
|
||||
.modal-footer(style='margin-top:0', ng-init='loadWidgets()')
|
||||
.container-fluid.share-buttons
|
||||
.row
|
||||
.col-12.text-center
|
||||
a.twitter-share-button.share-button(:href='twitterLink', target='_blank')
|
||||
.social-icon.twitter.svg-icon(v-html='icons.twitter')
|
||||
| {{ $t('tweet') }}
|
||||
a.fb-share-button.share-button(:href='facebookLink', target='_blank')
|
||||
.social-icon.facebook.svg-icon(v-html='icons.facebook')
|
||||
| {{ $t('share') }}
|
||||
// @TODO: Still want this? .col-4
|
||||
a.tumblr-share-button(:data-href='socialLevelLink', data-notes='none')
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.share-buttons {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.share-button {
|
||||
display: inline-block;
|
||||
width: 77px;
|
||||
padding: .5em;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.fb-share-button {
|
||||
background-color: #2995cd;
|
||||
}
|
||||
|
||||
.twitter-share-button {
|
||||
margin-right: .5em;
|
||||
background-color: #3bcad7;
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
width: 16px;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.social-icon.facebook svg {
|
||||
width: 7.5px;
|
||||
margin-bottom: .2em;
|
||||
}
|
||||
|
||||
.social-icon.twitter {
|
||||
margin-bottom: .2em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// @TODO:
|
||||
let BASE_URL = 'https://habitica.com';
|
||||
import twitter from 'assets/svg/twitter.svg';
|
||||
import facebook from 'assets/svg/facebook.svg';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
let tweet = this.$t('achievementShare');
|
||||
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
twitter,
|
||||
facebook,
|
||||
}),
|
||||
tweet,
|
||||
achievementLink: `${BASE_URL}`,
|
||||
twitterLink: `https://twitter.com/intent/tweet?text=${tweet}&via=habitica&url=${BASE_URL}&count=none`,
|
||||
facebookLink: `https://www.facebook.com/sharer/sharer.php?text=${tweet}&u=${BASE_URL}`,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,28 @@
|
||||
<template lang="pug">
|
||||
b-modal#armoire-empty(:title="$t('armoireText')", size='lg', :hide-footer="true")
|
||||
.modal-body
|
||||
.row
|
||||
.col-6.offset-3
|
||||
.shop_armoire
|
||||
p {{$t('armoireLastItem')}}
|
||||
p {{$t('armoireNotesEmpty')}}
|
||||
.modal-footer
|
||||
.col-12.text-center
|
||||
button.btn.btn-primary(@click='close()') {{$t('close')}}
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.shop_armoire {
|
||||
margin: 1em auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'armoire-empty');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,203 @@
|
||||
<template lang="pug">
|
||||
b-modal#choose-class(
|
||||
size='lg',
|
||||
:hide-header='true',
|
||||
:hide-footer='true',
|
||||
:no-close-on-esc='true',
|
||||
:no-close-on-backdrop='true',
|
||||
)
|
||||
.modal-body.select-class
|
||||
h1.header-purple.text-center {{ $t('chooseClass') }}
|
||||
.container-fluid
|
||||
br
|
||||
.row
|
||||
.col-md-3(v-for='heroClass in classes')
|
||||
div(@click='selectedClass = heroClass')
|
||||
avatar(
|
||||
:member='user',
|
||||
:avatarOnly='true',
|
||||
:withBackground='false',
|
||||
:overrideAvatarGear='classGear(heroClass)',
|
||||
:hideClassBadge='true',
|
||||
:spritesMargin='"1.8em 1.5em"',
|
||||
:overrideTopPadding='"0px"',
|
||||
:showVisualBuffs='false',
|
||||
:class='selectionBox(selectedClass, heroClass)',
|
||||
)
|
||||
br
|
||||
.d-flex.justify-content-center(v-for='heroClass in classes')
|
||||
.d-inline-flex(v-if='selectedClass === heroClass')
|
||||
.class-badge.d-flex.justify-content-center
|
||||
.svg-icon.align-self-center(v-html='icons[heroClass]')
|
||||
.class-name(:class='`${heroClass}-color`') {{ $t(heroClass) }}
|
||||
div(v-for='heroClass in classes')
|
||||
.class-explanation.text-center(v-if='selectedClass === heroClass') {{ $t(`${heroClass}Text`) }}
|
||||
.text-center(v-markdown='$t("chooseClassLearnMarkdown")')
|
||||
.modal-actions.text-center
|
||||
button.btn.btn-primary.d-inline-block(v-if='!selectedClass', :disabled='true') {{ $t('select') }}
|
||||
button.btn.btn-primary.d-inline-block(v-else, @click='clickSelectClass(selectedClass); close();') {{ $t('selectClass', {heroClass: $t(selectedClass)}) }}
|
||||
.opt-out-wrapper
|
||||
span#classOptOutBtn.danger(@click='clickDisableClasses(); close();') {{ $t('optOutOfClasses') }}
|
||||
span.opt-out-description {{ $t('optOutOfClassesText') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.class-badge {
|
||||
$badge-size: 32px;
|
||||
|
||||
width: $badge-size;
|
||||
height: $badge-size;
|
||||
background: $white;
|
||||
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
|
||||
border-radius: 100px;
|
||||
|
||||
.svg-icon {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
.class-explanation {
|
||||
font-size: 16px;
|
||||
margin: 1.5em auto;
|
||||
}
|
||||
|
||||
#classOptOutBtn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.class-name {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: auto 0.33333em;
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: $red-50;
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
|
||||
.header-purple {
|
||||
color: $purple-200;
|
||||
margin-top: 1.33333em;
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
margin: 2em auto;
|
||||
}
|
||||
|
||||
.opt-out-wrapper {
|
||||
margin: 1em 0 0.5em 0;
|
||||
}
|
||||
|
||||
.selection-box {
|
||||
width: 140px;
|
||||
height: 148px;
|
||||
border-radius: 16px;
|
||||
border: solid 4px $purple-300;
|
||||
}
|
||||
|
||||
.healer-color {
|
||||
color: $yellow-10;
|
||||
}
|
||||
|
||||
.rogue-color {
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
.warrior-color {
|
||||
color: $red-50;
|
||||
}
|
||||
|
||||
.wizard-color {
|
||||
color: $blue-10;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Avatar from '../avatar';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import markdownDirective from 'client/directives/markdown';
|
||||
import warriorIcon from 'assets/svg/warrior.svg';
|
||||
import rogueIcon from 'assets/svg/rogue.svg';
|
||||
import healerIcon from 'assets/svg/healer.svg';
|
||||
import wizardIcon from 'assets/svg/wizard.svg';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Avatar,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
classes: 'content.classes',
|
||||
}),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
warrior: warriorIcon,
|
||||
rogue: rogueIcon,
|
||||
healer: healerIcon,
|
||||
wizard: wizardIcon,
|
||||
}),
|
||||
selectedClass: 'warrior',
|
||||
};
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'choose-class');
|
||||
},
|
||||
clickSelectClass (heroClass) {
|
||||
if (this.user.flags.classSelected && !confirm(this.$t('changeClassConfirmCost'))) return;
|
||||
this.$store.dispatch('user:changeClass', {query: {class: heroClass}});
|
||||
},
|
||||
clickDisableClasses () {
|
||||
this.$store.dispatch('user:disableClasses');
|
||||
},
|
||||
classGear (heroClass) {
|
||||
if (heroClass === 'rogue') {
|
||||
return {
|
||||
armor: 'armor_special_fall2019Rogue',
|
||||
eyewear: 'eyewear_special_fall2019Rogue',
|
||||
head: 'head_special_fall2019Rogue',
|
||||
shield: 'shield_special_fall2019Rogue',
|
||||
weapon: 'weapon_special_fall2019Rogue',
|
||||
};
|
||||
} else if (heroClass === 'wizard') {
|
||||
return {
|
||||
armor: 'armor_special_fall2019Mage',
|
||||
head: 'head_special_fall2019Mage',
|
||||
weapon: 'weapon_special_fall2019Mage',
|
||||
};
|
||||
} else if (heroClass === 'healer') {
|
||||
return {
|
||||
armor: 'armor_special_fall2019Healer',
|
||||
eyewear: 'eyewear_special_fall2019Healer',
|
||||
head: 'head_special_fall2019Healer',
|
||||
shield: 'shield_special_fall2019Healer',
|
||||
weapon: 'weapon_special_fall2019Healer',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
armor: 'armor_special_fall2019Warrior',
|
||||
head: 'head_special_fall2019Warrior',
|
||||
shield: 'shield_special_fall2019Warrior',
|
||||
weapon: 'weapon_special_fall2019Warrior',
|
||||
};
|
||||
}
|
||||
},
|
||||
selectionBox (selectedClass, heroClass) {
|
||||
if (selectedClass === heroClass) {
|
||||
return 'selection-box';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template lang="pug">
|
||||
b-modal#contributor(:title="$t('modalContribAchievement')", size='md', :hide-footer="true")
|
||||
.modal-body
|
||||
.col-12
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
| {{ $t('contribModal', {name: user.profile.name, level: user.contributor.level}) }}
|
||||
br
|
||||
a(:href="$t('conRewardsURL')", target='_blank') {{ $t('contribLink') }}
|
||||
br
|
||||
button.btn.btn-primary(style='margin-top:1em' @click='close()') {{ $t('huzzah') }}
|
||||
br
|
||||
achievement-footer
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'contributor');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,75 @@
|
||||
<template lang="pug">
|
||||
b-modal#death(
|
||||
:title="$t('lostAllHealth')",
|
||||
size='md',
|
||||
:hide-footer="true",
|
||||
no-close-on-esc,
|
||||
no-close-on-backdrop,
|
||||
)
|
||||
.row
|
||||
.col-12
|
||||
.hero-stats
|
||||
.meter-label(:tooltip="$t('health')")
|
||||
span.glyphicon.glyphicon-heart
|
||||
.meter.health(:tooltip='Math.round(user.stats.hp * 100) / 100')
|
||||
.bar(:style='barStyle')
|
||||
// span.meter-text.value
|
||||
| {{Math.ceil(user.stats.hp)}} / {{maxHealth}}
|
||||
avatar(:member='user', :sleep='true', :avatarOnly='true', :withBackground='true')
|
||||
// @TOOD: Sleep +generatedAvatar({sleep:true})
|
||||
span(class='knockout')
|
||||
.col-6.offset-3
|
||||
h4.dont-despair {{ $t('dontDespair') }}
|
||||
p.death-penalty {{ $t('deathPenaltyDetails') }}
|
||||
.modal-footer
|
||||
.col-12.text-center
|
||||
button.btn.btn-danger(@click='revive()') {{ $t('refillHealthTryAgain') }}
|
||||
h4.text-center(v-html="$t('dyingOftenTips')")
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import Avatar from '../avatar';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import percent from '../../../common/script/libs/percent';
|
||||
import {maxHealth} from '../../../common/script/index';
|
||||
import revive from '../../../common/script/ops/revive';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Avatar,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
maxHealth,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
barStyle () {
|
||||
return {
|
||||
width: `${percent(this.user.stats.hp, maxHealth)}%`,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'death');
|
||||
},
|
||||
async revive () {
|
||||
await axios.post('/api/v4/user/revive');
|
||||
revive(this.user);
|
||||
this.close();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,47 @@
|
||||
<template lang="pug">
|
||||
b-modal#drops-enabled(:title="$t('dropsEnabled')", size='lg', :hide-footer="true")
|
||||
.modal-body
|
||||
.col-6.offset-3.text-center
|
||||
p
|
||||
.item-drop-icon(class='Pet_Egg_Wolf')
|
||||
span(v-html='firstDropText')
|
||||
p
|
||||
.item-drop-icon(class='Pet_Currency_Gem')
|
||||
span(v-html="$t('useGems')")
|
||||
.modal-footer
|
||||
.col-12.text-center
|
||||
button.btn.btn-primary(@click='close()') {{ $t('close') }}
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item-drop-icon {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import eggs from '../../../common/script/content/eggs';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
eggs,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
firstDropText () {
|
||||
return this.$t('firstDrop', {
|
||||
eggText: this.eggs.all.Wolf.text(),
|
||||
eggNotes: this.eggs.all.Wolf.notes(),
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'drops-enabled');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,42 @@
|
||||
<template lang="pug">
|
||||
b-modal#generic-achievement(:title='data.message', size='md', :hide-footer='true')
|
||||
.modal-body
|
||||
.col-12
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
p(v-html='data.modalText')
|
||||
button.btn.btn-primary(@click='close()') {{ $t('huzzah') }}
|
||||
achievement-footer
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
|
||||
import {mapState} from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
props: ['data'],
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'generic-achievement');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,38 @@
|
||||
<template lang="pug">
|
||||
b-modal#invited-friend(:title="$t('modalAchievement')", size='md', :hide-footer="true")
|
||||
.modal-body
|
||||
.col-12
|
||||
// @TODO: +achievementAvatar('friends',0)
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
p {{ $t('invitedFriendText') }}
|
||||
br
|
||||
button.btn.btn-primary(@click='close()') {{ $t('huzzah') }}
|
||||
achievement-footer
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'invited-friend');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,38 @@
|
||||
<template lang="pug">
|
||||
b-modal#joined-challenge(:title="$t('modalAchievement')", size='md', :hide-footer="true")
|
||||
.modal-body
|
||||
.col-12
|
||||
// @TODO: +achievementAvatar('challenge',0)
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
p {{ $t('joinedChallengeText') }}
|
||||
br
|
||||
button.btn.btn-primary(@click='close()') {{ $t('huzzah') }}
|
||||
achievement-footer
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'joined-challenge');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,38 @@
|
||||
<template lang="pug">
|
||||
b-modal#joined-guild(:title="$t('modalAchievement')", size='md', :hide-footer="true")
|
||||
.modal-body
|
||||
.col-12
|
||||
// @TODO: +achievementAvatar('guild',0)
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
p {{ $t('joinedGuildText') }}
|
||||
br
|
||||
button.btn.btn-primary(@click='close()') {{ $t('huzzah') }}
|
||||
achievement-footer
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'joined-guild');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template lang="pug">
|
||||
b-modal#just-add-water(:title='title', size='md', :hide-footer='true')
|
||||
.modal-body
|
||||
.col-12
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
p {{ $t('achievementJustAddWaterModalText') }}
|
||||
button.btn.btn-primary(@click='close()') {{ $t('huzzah') }}
|
||||
achievement-footer
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
|
||||
import {mapState} from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
title: `${this.$t('modalAchievement')} ${this.$t('achievementJustAddWater')}`,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'just-add-water');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,167 @@
|
||||
<template lang="pug">
|
||||
b-modal#level-up(:title="$t('levelUpShare')", size='sm', :hide-footer="true", :hide-header="true")
|
||||
.modal-body.text-center
|
||||
h2 {{ $t('reachedLevel', {level: user.stats.lvl}) }}
|
||||
|
||||
avatar.avatar(:member='user')
|
||||
|
||||
p.text {{ $t('levelup') }}
|
||||
|
||||
button.btn.btn-primary(@click='close()') {{ $t('onwards') }}
|
||||
br
|
||||
// @TODO: Keep this? .checkbox
|
||||
input(type='checkbox', v-model='user.preferences.suppressModals.levelUp', @change='changeLevelupSuppress()')
|
||||
label(style='display:inline-block') {{ $t('dontShowAgain') }}
|
||||
|
||||
.container-fluid.share-buttons
|
||||
.row
|
||||
.col-12.text-center
|
||||
a.twitter-share-button.share-button(:href='twitterLink', target='_blank')
|
||||
.social-icon.twitter.svg-icon(v-html='icons.twitter')
|
||||
| {{ $t('tweet') }}
|
||||
a.fb-share-button.share-button(:href='facebookLink', target='_blank')
|
||||
.social-icon.facebook.svg-icon(v-html='icons.facebook')
|
||||
| {{ $t('share') }}
|
||||
// @TODO: Still want this? .col-4
|
||||
a.tumblr-share-button(:data-href='socialLevelLink', data-notes='none')
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
#level-up {
|
||||
h2 {
|
||||
color: #4f2a93;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
min-width: 28em;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding-top: 1em;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.herobox {
|
||||
margin: auto 8.3em;
|
||||
width: 6em;
|
||||
height: 9em;
|
||||
padding-top: 0;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.character-sprites {
|
||||
margin: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
color: #686274;
|
||||
margin-top: 1em;
|
||||
min-height: 0px;
|
||||
}
|
||||
|
||||
.share-buttons {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.share-button {
|
||||
display: inline-block;
|
||||
width: 77px;
|
||||
padding: .5em;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.fb-share-button {
|
||||
background-color: #2995cd;
|
||||
}
|
||||
|
||||
.twitter-share-button {
|
||||
margin-right: .5em;
|
||||
background-color: #3bcad7;
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
width: 16px;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.social-icon.facebook svg {
|
||||
width: 7.5px;
|
||||
margin-bottom: .2em;
|
||||
}
|
||||
|
||||
.social-icon.twitter {
|
||||
margin-bottom: .2em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
margin-left: 6.8em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Avatar from '../avatar';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import {maxHealth} from '../../../common/script/index';
|
||||
import styleHelper from 'client/mixins/styleHelper';
|
||||
import twitter from 'assets/svg/twitter.svg';
|
||||
import facebook from 'assets/svg/facebook.svg';
|
||||
|
||||
let BASE_URL = 'https://habitica.com';
|
||||
|
||||
export default {
|
||||
mixins: [styleHelper],
|
||||
components: {
|
||||
Avatar,
|
||||
},
|
||||
data () {
|
||||
let tweet = this.$t('levelUpShare');
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
twitter,
|
||||
facebook,
|
||||
}),
|
||||
statsAllocationBoxIsOpen: true,
|
||||
maxHealth,
|
||||
tweet,
|
||||
socialLevelLink: `${BASE_URL}/social/level-up`,
|
||||
twitterLink: `https://twitter.com/intent/tweet?text=${tweet}&via=habitica&url=${BASE_URL}/social/level-up&count=none`,
|
||||
facebookLink: `https://www.facebook.com/sharer/sharer.php?text=${tweet}&u=${BASE_URL}/social/level-up`,
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.loadWidgets();
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
showAllocation () {
|
||||
return this.$store.getters['members:hasClass'](this.user) && !this.user.preferences.automaticAllocation;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'level-up');
|
||||
},
|
||||
loadWidgets () {
|
||||
// @TODO:
|
||||
},
|
||||
changeLevelupSuppress () {
|
||||
// @TODO: dispatch set({"preferences.suppressModals.levelUp": user.preferences.suppressModals.levelUp?true: false})
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,85 @@
|
||||
<template lang="pug">
|
||||
b-modal#login-incentives(:title="data.message", size='md', :hide-footer="true")
|
||||
.modal-body
|
||||
.row
|
||||
h3.col-12.text-center(v-if='data.rewardText') {{ $t('unlockedReward', {reward: data.rewardText}) }}
|
||||
.row.reward-row
|
||||
.col-12
|
||||
avatar.avatar(:member='user', :avatarOnly='true', :withBackground='true')
|
||||
.text-center.col-12(v-if='nextReward')
|
||||
.reward-wrap(v-if="!data.rewardText")
|
||||
div(v-if="nextReward.rewardKey.length === 1", :class="nextReward.rewardKey[0]")
|
||||
.reward(v-for="reward in nextReward.rewardKey", v-if="nextReward.rewardKey.length > 1", :class='reward')
|
||||
.reward-wrap(v-if="data.rewardText")
|
||||
div(v-if="data.rewardKey.length === 1", :class="data.rewardKey[0]")
|
||||
.reward(v-for="reward in data.rewardKey", v-if="data.rewardKey.length > 1", :class='reward')
|
||||
.col-12.text-center(v-if="data && data.nextRewardAt")
|
||||
h4 {{ $t('countLeft', {count: data.nextRewardAt - user.loginIncentives}) }}
|
||||
.row
|
||||
.col-12.text-center(v-if='data.rewardText')
|
||||
p {{ $t('earnedRewardForDevotion', {reward: data.rewardText}) }}
|
||||
.col-12.text-center
|
||||
p {{ $t('incentivesDescription') }}
|
||||
.col-12.text-center(v-if="data && data.nextRewardAt")
|
||||
h3 {{ $t('nextRewardUnlocksIn', {numberOfCheckinsLeft: data.nextRewardAt - user.loginIncentives}) }}
|
||||
.modal-footer
|
||||
.col-12.text-center
|
||||
button.btn.btn-primary(@click='close()') {{ $t('awesome') }}
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.reward-wrap {
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.avatar, .reward-wrap {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.reward-row {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.reward {
|
||||
float: left;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import Avatar from '../avatar';
|
||||
import {loginIncentives} from '../../../common/script/content/index';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Avatar,
|
||||
},
|
||||
props: ['data'],
|
||||
data () {
|
||||
return {
|
||||
loginIncentives,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
}),
|
||||
nextReward () {
|
||||
if (!this.loginIncentives[this.user.loginIncentives]) return;
|
||||
const nextRewardKey = this.loginIncentives[this.user.loginIncentives].nextRewardAt;
|
||||
const nextReward = this.loginIncentives[nextRewardKey];
|
||||
return nextReward;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'login-incentives');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template lang="pug">
|
||||
b-modal#lost-masterclasser(:title='title', size='md', :hide-footer='true')
|
||||
.modal-body
|
||||
.col-12
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
p {{ $t('achievementLostMasterclasserModalText') }}
|
||||
button.btn.btn-primary(@click='close()') {{ $t('huzzah') }}
|
||||
achievement-footer
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
|
||||
import {mapState} from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
title: `${this.$t('modalAchievement')} ${this.$t('achievementLostMasterclasser')}`,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'lost-masterclasser');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,96 @@
|
||||
<template lang="pug">
|
||||
b-modal#low-health(:title="$t('losingHealthWarning')", size='md', :hide-footer="true")
|
||||
.modal-body
|
||||
.col-12.text-center
|
||||
.meter-label(:tooltip="$t('health')")
|
||||
span.glyphicon.glyphicon-heart
|
||||
.meter.health(:tooltip='Math.round(user.stats.hp * 100) / 100')
|
||||
.bar(:style='barStyle')
|
||||
span.meter-text.value
|
||||
| {{healthLeft}}
|
||||
.col-12
|
||||
avatar(:member='user', :avatarOnly='true', :withBackground='true')
|
||||
.col-12
|
||||
p {{ $t('losingHealthWarning2') }}
|
||||
h4 {{ $t('toRegainHealth') }}
|
||||
ul
|
||||
li.spaced {{ $t('lowHealthTips1') }}
|
||||
li.spaced {{ $t('lowHealthTips2') }}
|
||||
h4 {{ $t('losingHealthQuickly') }}
|
||||
ul
|
||||
li.spaced {{ $t('lowHealthTips3') }}
|
||||
li.spaced {{ $t('lowHealthTips4') }}
|
||||
h4 {{ $t('goodLuck') }}
|
||||
.modal-footer
|
||||
.col-12.text-center
|
||||
button.btn.btn-primary(@click='acknowledgeHealthWarning()') {{ $t('ok') }}
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hero-stats {
|
||||
position: absolute;
|
||||
margin-left: 9em;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.character-sprites {
|
||||
display: inline-flex;
|
||||
margin: 3em auto;
|
||||
}
|
||||
|
||||
.herobox {
|
||||
padding-top: 0em;
|
||||
margin-left: 16em;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
margin-top: 0em;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Avatar from '../avatar';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import percent from '../../../common/script/libs/percent';
|
||||
import {maxHealth} from '../../../common/script/index';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Avatar,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
maxHealth,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
barStyle () {
|
||||
return {
|
||||
width: `${percent(this.user.stats.hp, maxHealth)}%`,
|
||||
};
|
||||
},
|
||||
healthLeft () {
|
||||
return `${Math.ceil(this.user.stats.hp)} / ${this.maxHealth}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'low-health');
|
||||
},
|
||||
acknowledgeHealthWarning () {
|
||||
this.$store.dispatch('user:set', {
|
||||
'flags.warnedLowHealth': true,
|
||||
});
|
||||
this.close();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template lang="pug">
|
||||
b-modal#mind-over-matter(:title='title', size='md', :hide-footer='true')
|
||||
.modal-body
|
||||
.col-12
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
p {{ $t('achievementMindOverMatterModalText') }}
|
||||
button.btn.btn-primary(@click='close()') {{ $t('huzzah') }}
|
||||
achievement-footer
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
|
||||
import {mapState} from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
title: `${this.$t('modalAchievement')} ${this.$t('achievementMindOverMatter')}`,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'mind-over-matter');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,60 @@
|
||||
<template lang="pug">
|
||||
b-modal#new-stuff(
|
||||
size='lg',
|
||||
:hide-header='true',
|
||||
:hide-footer='true',
|
||||
no-close-on-esc,
|
||||
no-close-on-backdrop
|
||||
)
|
||||
.modal-body
|
||||
.static-view(v-html='html')
|
||||
.modal-footer
|
||||
a.btn.btn-info(href='http://habitica.fandom.com/wiki/Whats_New', target='_blank') {{ this.$t('newsArchive') }}
|
||||
button.btn.btn-secondary(@click='tellMeLater()') {{ this.$t('tellMeLater') }}
|
||||
button.btn.btn-warning(@click='dismissAlert();') {{ this.$t('dismissAlert') }}
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
@import '~client/assets/scss/static.scss';
|
||||
#new-stuff {
|
||||
.modal-body .modal-body {
|
||||
padding-top: 0rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
html: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
async mounted () {
|
||||
this.$root.$on('bv::show::modal', async (modalId) => {
|
||||
if (modalId !== 'new-stuff') return;
|
||||
let response = await axios.get('/api/v4/news');
|
||||
this.html = response.data.html;
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
this.$root.$off('bv::show::modal');
|
||||
},
|
||||
methods: {
|
||||
tellMeLater () {
|
||||
this.$store.dispatch('user:newStuffLater');
|
||||
this.$root.$emit('bv::hide::modal', 'new-stuff');
|
||||
},
|
||||
dismissAlert () {
|
||||
this.$store.dispatch('user:set', {'flags.newStuff': false});
|
||||
this.$root.$emit('bv::hide::modal', 'new-stuff');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template lang="pug">
|
||||
b-modal#quest-completed(v-if='user.party.quest.completed', :title="title",
|
||||
size='md', :hide-footer="true", :no-close-on-esc="true", :no-close-on-backdrop="true",
|
||||
@hide='hide')
|
||||
.modal-body.text-center
|
||||
.quest(:class='`quest_${user.party.quest.completed}`')
|
||||
p(v-if='questData.completion && typeof questData.completion === "function"', v-html='questData.completion()')
|
||||
.quest-rewards.text-center
|
||||
h3(v-once) {{ $t('paymentYouReceived') }}
|
||||
questDialogDrops(:item="questData")
|
||||
.modal-footer
|
||||
button.btn.btn-primary(@click='setQuestCompleted()') {{ $t('ok') }}
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.quest {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.quest-rewards .questRewards {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import quests from 'common/script/content/quests';
|
||||
import questDialogDrops from 'client/components/shops/quests/questDialogDrops';
|
||||
|
||||
import { mapState } from 'client/libs/store';
|
||||
import percent from '../../../common/script/libs/percent';
|
||||
import { maxHealth } from '../../../common/script/index';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
questDialogDrops,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
maxHealth,
|
||||
quests,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
questData () {
|
||||
return this.quests.quests[this.user.party.quest.completed];
|
||||
},
|
||||
title () {
|
||||
return `${this.questData.text()} ${this.$t('completed')}`;
|
||||
},
|
||||
barStyle () {
|
||||
return {
|
||||
width: `${percent(this.user.stats.hp, maxHealth)}%`,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'quest-completed');
|
||||
},
|
||||
setQuestCompleted () {
|
||||
this.close();
|
||||
},
|
||||
hide () {
|
||||
this.$store.dispatch('user:set', {'party.quest.completed': ''});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template lang="pug">
|
||||
b-modal#quest-invitation(v-if='user.party.quest.key && quests[user.party.quest.key]', :title="$t('questInvitation')", size='lg', :hide-footer="true")
|
||||
.modal-header
|
||||
h4 {{ $t('questInvitation') }}
|
||||
| {{quests[user.party.quest.key].text()}}
|
||||
.modal-body
|
||||
.pull-right-sm.text-center
|
||||
.col-centered(:class='`quest_${quests[user.party.quest.key].key}`')
|
||||
div(ng-if='quests[user.party.quest.key].boss')
|
||||
h4 {{quests[user.party.quest.key].boss.name()}}
|
||||
p
|
||||
strong {{ $t('bossHP') }} + ': '
|
||||
| {{quests[user.party.quest.key].boss.hp}}
|
||||
p
|
||||
strong {{ $t('bossStrength') }} + ': '
|
||||
| {{quests[user.party.quest.key].boss.str}}
|
||||
div(ng-if='quests[user.party.quest.key].collect')
|
||||
p(ng-repeat='(k,v) in quests[user.party.quest.key].collect')
|
||||
strong {{ $t('collect') }} + ': '
|
||||
| {{quests[user.party.quest.key].collect[k].count}} {{quests[user.party.quest.key].collect[k].text()}}
|
||||
div(ng-bind-html='quests[user.party.quest.key].notes()')
|
||||
.quest-rewards(:key='user.party.quest.key', header-participant="$t('rewardsAllParticipants')", header-quest-owner="$t('rewardsQuestOwner')")
|
||||
hr
|
||||
h5 {{headerParticipant}}
|
||||
table.table.table-striped
|
||||
tr(ng-repeat='drop in _.reject(quest.drop.items, \'onlyOwner\')')
|
||||
td {{drop.text()}}
|
||||
tr(ng-if='quest.drop.exp > 0')
|
||||
td {{quest.drop.exp}}
|
||||
| {{ $t('experience') }}
|
||||
tr(ng-if='quest.drop.gp > 0')
|
||||
td {{quest.drop.gp}}
|
||||
| {{ $t('gold') }}
|
||||
tr(ng-if='quest.drop.unlock()')
|
||||
td {{quest.drop.unlock()}}
|
||||
div(ng-if='getQuestOwnerRewards(quest).length > 0')
|
||||
h5 {{headerQuestOwner}}
|
||||
table.table.table-striped
|
||||
tr(ng-repeat='drop in getQuestOwnerRewards(quest)')
|
||||
td {{drop.text()}}
|
||||
.modal-footer
|
||||
button.btn.btn-secondary(ng-click='questHold = true; $close()') {{ $t('askLater') }}
|
||||
button.btn.btn-secondary(ng-click='questReject(); $close()') {{ $t('reject') }}
|
||||
button.btn.btn-primary(ng-click='questAccept(); $close()') {{ $t('accept') }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import quests from 'common/script/content/quests';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import percent from '../../../common/script/libs/percent';
|
||||
import {maxHealth} from '../../../common/script/index';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
maxHealth,
|
||||
quests,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
barStyle () {
|
||||
return {
|
||||
width: `${percent(this.user.stats.hp, maxHealth)}%`,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'quest-invitation');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template lang="pug">
|
||||
b-modal#rebirth(:title="$t('modalAchievement')", size='md', :hide-footer="true")
|
||||
.modal-body
|
||||
.col-12
|
||||
// @TODO: +achievementAvatar('sun',0)
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
div(v-if='user.achievements.rebirthLevel < 100')
|
||||
| {{ $t('rebirthAchievement', {number: user.achievements.rebirths, level: user.achievements.rebirthLevel}) }}
|
||||
div(v-if='user.achievements.rebirthLevel >= 100')
|
||||
| {{ $t('rebirthAchievement100', {number: user.achievements.rebirths}) }}
|
||||
br
|
||||
button.btn.btn-primary(@click='close()') {{ $t('huzzah') }}
|
||||
achievement-footer
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
|
||||
import {mapState} from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'rebirth');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,32 @@
|
||||
<template lang="pug">
|
||||
b-modal#rebirth-enabled(:title="$t('rebirthNew')", size='md', :hide-footer="true")
|
||||
.modal-body
|
||||
.col-12
|
||||
.rebirth_orb
|
||||
p
|
||||
span {{ $t('rebirthUnlock') }}
|
||||
.modal-footer
|
||||
.col-12.text-center
|
||||
button.btn.btn-primary(@click='close()') {{ $t('close') }}
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rebirth_orb {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'rebirth-enabled');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template lang="pug">
|
||||
b-modal#streak(:title="$t('streakAchievement')", size='md', :hide-footer="true")
|
||||
.modal-body
|
||||
.col-12
|
||||
// @TODO: +achievementAvatar('thermometer',2.5)
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
h3(v-if='user.achievements.streak === 1') {{ $t('firstStreakAchievement') }}
|
||||
h3(v-if='user.achievements.streak > 1') {{ $t('streakAchievementCount', {streaks: user.achievements.streak}) }}
|
||||
p {{ $t('twentyOneDays') }}
|
||||
p {{ $t('dontBreakStreak') }}
|
||||
br
|
||||
button.btn.btn-primary(@click='close()') {{ $t('dontStop') }}
|
||||
.checkbox
|
||||
input(type='checkbox', v-model='user.preferences.suppressModals.streak', @change='suppressModals')
|
||||
label {{ $t('dontShowAgain') }}
|
||||
achievement-footer
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'streak');
|
||||
},
|
||||
suppressModals () {
|
||||
let surpress = this.user.preferences.suppressModals.streak ? true : false;
|
||||
this.$store.dispatch('user:set', {'preferences.suppressModals.streak': surpress});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,35 @@
|
||||
<template lang="pug">
|
||||
b-modal#testing(:title="$t('guildReminderTitle')", size='lg', :hide-footer="true")
|
||||
.modal-body.text-center
|
||||
br
|
||||
.scene_guilds
|
||||
br
|
||||
h4 {{ $t('guildReminderText1') }}
|
||||
.modal-footer
|
||||
.container-fluid
|
||||
.row
|
||||
.col-6.text-center
|
||||
button.btn.btn-secondary(@click='close()') {{ $t('guildReminderDismiss') }}
|
||||
.col-6.text-center(@click='close()')
|
||||
.btn.btn-primary(@click='takeMethere()') {{ $t('guildReminderCTA') }}
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.scene_guilds {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'testing');
|
||||
},
|
||||
takeMethere () {
|
||||
this.$router.push('/groups/discovery');
|
||||
this.close();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,36 @@
|
||||
<template lang="pug">
|
||||
b-modal#testingletiant(:title="$t('guildReminderTitle')", size='lg', :hide-footer="true")
|
||||
.modal-content
|
||||
.modal-body.text-center
|
||||
br
|
||||
.scene_guilds
|
||||
br
|
||||
h4 {{ $t('guildReminderText2') }}
|
||||
.modal-footer
|
||||
.container-fluid
|
||||
.row
|
||||
.col-6.text-center
|
||||
button.btn.btn-secondary(@click='close()') {{ $t('guildReminderDismiss') }}
|
||||
.col-6.text-center
|
||||
.btn.btn-primary(@click='takeMethere()') {{ $t('guildReminderCTA') }}
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.scene_guilds {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'testingletiant');
|
||||
},
|
||||
takeMethere () {
|
||||
this.$router.push('/groups/discovery');
|
||||
this.close();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template lang="pug">
|
||||
b-modal#ultimate-gear(:title="$t('modalAchievement')", size='md', :hide-footer="true")
|
||||
.modal-body
|
||||
.col-12
|
||||
// @TODO: +achievementAvatar('armor',2.5)
|
||||
achievement-avatar.avatar
|
||||
.col-12.text-center
|
||||
p {{ $t('gearAchievement') }}
|
||||
br
|
||||
table.multi-achievement
|
||||
tr
|
||||
td(v-if='user.achievements.ultimateGearSets.healer').multi-achievement
|
||||
.achievement-ultimate-healer2x.multi-achievement
|
||||
| {{ $t('healer') }}
|
||||
td(v-if='user.achievements.ultimateGearSets.wizard').multi-achievement
|
||||
.achievement-ultimate-mage2x.multi-achievement
|
||||
| {{ $t('mage') }}
|
||||
td(v-if='user.achievements.ultimateGearSets.rogue').multi-achievement
|
||||
.achievement-ultimate-rogue2x.multi-achievement
|
||||
| {{ $t('rogue') }}
|
||||
td(v-if='user.achievements.ultimateGearSets.warrior').multi-achievement
|
||||
.achievement-ultimate-warrior2x.multi-achievement
|
||||
| {{ $t('warrior') }}
|
||||
br
|
||||
div(v-if='!(user.achievements.ultimateGearSets.healer && user.achievements.ultimateGearSets.wizard && user.achievements.ultimateGearSets.rogue && user.achievements.ultimateGearSets.warrior)')
|
||||
p(v-html="$t('moreGearAchievements')")
|
||||
br
|
||||
.shop_armoire
|
||||
p(v-html='$t("armoireUnlocked")')
|
||||
br
|
||||
button.btn.btn-primary(@click='close()') {{ $t('huzzah') }}
|
||||
achievement-footer
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.shop_armoire {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'ultimate-gear');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template lang="pug">
|
||||
b-modal#welcome(:title="$t('welcomeToHabit')", size='lg', :hide-footer="true")
|
||||
.modal-body.container-fluid
|
||||
.row
|
||||
.col-4.col-centered
|
||||
span(style='display:flex')
|
||||
h1
|
||||
| ①
|
||||
h3(style='margin:auto auto auto .5em') {{ $t('welcome1') }}
|
||||
.welcome_basic_avatars(style='margin: 1.5em auto 1.5em')
|
||||
h4 {{ $t('welcome1notes') }}
|
||||
.col-4.col-centered
|
||||
span(style='display:flex')
|
||||
h1
|
||||
| ②
|
||||
h3(style='margin:.3em auto auto .5em') {{ $t('welcome2') }}
|
||||
.welcome_sample_tasks(style='margin: 2.5em auto 1.5em')
|
||||
h4 {{ $t('welcome2notes') }}
|
||||
.col-4.col-centered
|
||||
span(style='display:flex')
|
||||
h1
|
||||
| ③
|
||||
h3(style='margin:auto auto auto .5em') {{ $t('welcome3') }}
|
||||
.welcome_promo_party(style='margin: 1em auto 1em')
|
||||
h4 {{ $t('welcome3notes') }}
|
||||
.modal-footer.text-center(style='margin-top:0')
|
||||
.col-3
|
||||
.col-6
|
||||
button.btn.btn-primary.btn-lg.flex-column(@click='ready()') {{ $t('imReady') }}
|
||||
.col-3
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
ready () {
|
||||
// Guide.goto("intro",0)'
|
||||
this.$router.push('/avatar');
|
||||
this.$root.$emit('bv::hide::modal', 'welcome');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,66 @@
|
||||
<template lang="pug">
|
||||
b-modal#won-challenge(:title="$t('wonChallenge')", size='md', :hide-footer="true")
|
||||
.modal-body.text-center
|
||||
h4(v-markdown='user.achievements.challenges[user.achievements.challenges.length - 1]')
|
||||
.row
|
||||
.col-4
|
||||
.achievement-karaoke-2x
|
||||
.col-4
|
||||
// @TODO: +generatedAvatar({sleep: false})
|
||||
avatar.avatar(:member='user', :avatar-only='true')
|
||||
.col-4
|
||||
.achievement-karaoke-2x
|
||||
p {{ $t('congratulations') }}
|
||||
br
|
||||
button.btn.btn-primary(@click='close()') {{ $t('hurray') }}
|
||||
.modal-footer
|
||||
.col-3
|
||||
a.twitter-share-button(href='https://twitter.com/intent/tweet?text=#{tweet}&via=habitica&url=#{env.BASE_URL}/social/won-challenge&count=none') {{ $t('tweet') }}
|
||||
.col-4(style='margin-left:.8em')
|
||||
.fb-share-button(data-href='#{env.BASE_URL}/social/won-challenge', data-layout='button')
|
||||
.col-4(style='margin-left:.8em')
|
||||
a.tumblr-share-button(data-href='#{env.BASE_URL}/social/won-challenge', data-notes='none')
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.achievement-karaoke-2x {
|
||||
margin: 0 auto;
|
||||
margin-top: 6em;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import markdownDirective from 'client/directives/markdown';
|
||||
import Avatar from '../avatar';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Avatar,
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
data () {
|
||||
let tweet = this.$t('wonChallengeShare');
|
||||
return {
|
||||
tweet,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'won-challenge');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,401 @@
|
||||
<template lang="pug">
|
||||
.row.footer-row
|
||||
buy-gems-modal(v-if='user')
|
||||
//modify-inventory(v-if="isUserLoaded")
|
||||
footer.col-12.expanded
|
||||
.row
|
||||
.col-12.col-md-2
|
||||
h3
|
||||
a(href='https://itunes.apple.com/us/app/habitica/id994882113?ls=1&mt=8', target='_blank') {{ $t('mobileIOS') }}
|
||||
h3
|
||||
a(href='https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica', target='_blank') {{ $t('mobileAndroid') }}
|
||||
.col-12.col-md-2
|
||||
h3 {{ $t('footerCompany') }}
|
||||
ul
|
||||
li
|
||||
router-link(to='/static/features') {{ $t('companyAbout') }}
|
||||
li
|
||||
a(href='https://habitica.wordpress.com/', target='_blank') {{ $t('companyBlog') }}
|
||||
li
|
||||
a(href='http://blog.habitrpg.com/', target='_blank') {{ $t('tumblr') }}
|
||||
li
|
||||
router-link(to='/static/faq') {{ $t('FAQ') }}
|
||||
li
|
||||
a(href='http://habitica.fandom.com/wiki/Whats_New', target='_blank') {{ $t('oldNews') }}
|
||||
li
|
||||
router-link(to='/static/merch') {{ $t('merch') }}
|
||||
li
|
||||
router-link(to='/static/press-kit') {{ $t('presskit') }}
|
||||
li
|
||||
router-link(to='/static/contact') {{ $t('contactUs') }}
|
||||
.col-12.col-md-2
|
||||
h3 {{ $t('footerCommunity') }}
|
||||
ul
|
||||
li
|
||||
a(target="_blanck", href="/static/community-guidelines") {{ $t('communityGuidelines') }}
|
||||
li
|
||||
router-link(to='/hall/contributors') {{ $t('hall') }}
|
||||
li
|
||||
router-link(to='/groups/guild/a29da26b-37de-4a71-b0c6-48e72a900dac') {{ $t('reportBug') }}
|
||||
li
|
||||
a(href='https://trello.com/c/odmhIqyW/440-read-first-table-of-contents', target='_blank') {{ $t('requestFeature') }}
|
||||
li(v-html='$t("communityExtensions")')
|
||||
li(v-html='$t("communityForum")')
|
||||
li
|
||||
a(href='https://www.facebook.com/Habitica', target='_blank') {{ $t('communityFacebook') }}
|
||||
li
|
||||
a(href='https://www.instagram.com/habitica', target='_blank') {{ $t('communityInstagram') }}
|
||||
.col-12.col-md-6
|
||||
.row
|
||||
.col-6
|
||||
h3 {{ $t('footerDevs') }}
|
||||
ul
|
||||
li
|
||||
a(href='/apidoc', target='_blank') {{ $t('APIv3') }}
|
||||
li
|
||||
a(:href="getDataDisplayToolUrl", target='_blank') {{ $t('dataDisplayTool') }}
|
||||
li
|
||||
a(href='http://habitica.fandom.com/wiki/Guidance_for_Blacksmiths', target='_blank') {{ $t('guidanceForBlacksmiths') }}
|
||||
.col-6.social
|
||||
h3 {{ $t('footerSocial') }}
|
||||
.icons
|
||||
a.social-circle(href='https://twitter.com/habitica', target='_blank')
|
||||
.social-icon.svg-icon(v-html='icons.twitter')
|
||||
a.social-circle(href='https://www.instagram.com/habitica/', target='_blank')
|
||||
.social-icon.svg-icon.instagram(v-html='icons.instagram')
|
||||
a.social-circle(href='https://www.facebook.com/Habitica', target='_blank')
|
||||
.social-icon.facebook.svg-icon(v-html='icons.facebook')
|
||||
.row
|
||||
.col-12.col-md-8 {{ $t('donateText3') }}
|
||||
.col-12.col-md-4
|
||||
button.btn.btn-contribute.btn-flat(@click="donate()", v-if="user")
|
||||
.svg-icon.heart(v-html="icons.heart")
|
||||
.text {{ $t('companyDonate') }}
|
||||
.btn.btn-contribute.btn-flat(v-else)
|
||||
a(href='http://habitica.fandom.com/wiki/Contributing_to_Habitica', target='_blank')
|
||||
.svg-icon.heart(v-html="icons.heart")
|
||||
.text {{ $t('companyContribute') }}
|
||||
.row
|
||||
.col-12
|
||||
hr
|
||||
.row
|
||||
.col-12.col-md-5
|
||||
| © 2019 Habitica. All rights reserved.
|
||||
.debug.float-left(v-if="!IS_PRODUCTION && isUserLoaded")
|
||||
button.btn.btn-primary(@click="debugMenuShown = !debugMenuShown") Toggle Debug Menu
|
||||
.debug-group(v-if="debugMenuShown")
|
||||
a.btn.btn-secondary(@click="setHealthLow()") Health = 1
|
||||
a.btn.btn-secondary(@click="addMissedDay(1)") +1 Missed Day
|
||||
a.btn.btn-secondary(@click="addMissedDay(2)") +2 Missed Days
|
||||
a.btn.btn-secondary(@click="addMissedDay(8)") +8 Missed Days
|
||||
a.btn.btn-secondary(@click="addMissedDay(32)") +32 Missed Days
|
||||
a.btn.btn-secondary(@click="addTenGems()") +10 Gems
|
||||
a.btn.btn-secondary(@click="addHourglass()") +1 Mystic Hourglass
|
||||
a.btn.btn-secondary(@click="addGold()") +500GP
|
||||
a.btn.btn-secondary(@click="plusTenHealth()") + 10HP
|
||||
a.btn.btn-secondary(@click="addMana()") +MP
|
||||
a.btn.btn-secondary(@click="addLevelsAndGold()") +Exp +GP +MP
|
||||
a.btn.btn-secondary(@click="addExp()") +Exp
|
||||
a.btn.btn-secondary(@click="addOneLevel()") +1 Level
|
||||
a.btn.btn-secondary(@click="addQuestProgress()", tooltip="+1000 to boss quests. 300 items to collection quests") Quest Progress Up
|
||||
a.btn.btn-secondary(@click="makeAdmin()") Make Admin
|
||||
a.btn.btn-secondary(@click="openModifyInventoryModal()") Modify Inventory
|
||||
.col-12.col-md-2.text-center
|
||||
.logo.svg-icon(v-html='icons.gryphon')
|
||||
.col-12.col-md-5.text-right
|
||||
span.ml-4
|
||||
a(target="_blanck", href="/static/privacy") {{ $t('privacy') }}
|
||||
span.ml-4
|
||||
a(target="_blanck", href="/static/terms") {{ $t('terms') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.footer-row {
|
||||
margin: 0;
|
||||
flex: 0 1 auto;
|
||||
z-index: 17;
|
||||
}
|
||||
|
||||
footer {
|
||||
color: #c3c0c7;
|
||||
padding-bottom: 3em;
|
||||
|
||||
a {
|
||||
color: #2995cd;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #878190;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
.social {
|
||||
h3 {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.icons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
// smaller than desktop
|
||||
@media only screen and (max-width: 992px) {
|
||||
.social-circle {
|
||||
height: 32px !important;
|
||||
width: 32px !important;
|
||||
|
||||
margin-left: 0.75em !important;
|
||||
}
|
||||
}
|
||||
|
||||
.social-circle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #c3c0c7;
|
||||
display: flex;
|
||||
margin-left: 1em;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #a5a1ac;
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
color: #e1e0e3;
|
||||
width: 16px;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 0 auto;
|
||||
color: #e1e0e3;
|
||||
}
|
||||
|
||||
.debug-group {
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
top: -300px;
|
||||
border-radius: 2px;
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.btn-contribute {
|
||||
background: #c3c0c7;
|
||||
box-shadow: none;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: #a5a1ac;
|
||||
|
||||
.text {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.heart {
|
||||
max-height: 25px;
|
||||
width: 18px;
|
||||
margin-right: .5em;
|
||||
margin-bottom: .2em;
|
||||
}
|
||||
|
||||
.text, .heart {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.heart svg {
|
||||
margin-top: .1em;
|
||||
}
|
||||
|
||||
.facebook svg {
|
||||
width: 10px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
footer {
|
||||
&.expanded {
|
||||
padding-left: 6em;
|
||||
padding-right: 6em;
|
||||
padding-top: 3em;
|
||||
background: #e1e0e3;
|
||||
color: #878190;
|
||||
min-height: 408px;
|
||||
|
||||
a {
|
||||
color: #878190;
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: #c3c0c7;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import moment from 'moment';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
|
||||
import gryphon from 'assets/svg/gryphon.svg';
|
||||
import twitter from 'assets/svg/twitter.svg';
|
||||
import facebook from 'assets/svg/facebook.svg';
|
||||
import instagram from 'assets/svg/instagram.svg';
|
||||
import heart from 'assets/svg/heart.svg';
|
||||
|
||||
import modifyInventory from './modifyInventory';
|
||||
import buyGemsModal from './payments/buyGemsModal';
|
||||
|
||||
const IS_PRODUCTION = process.env.NODE_ENV === 'production'; // eslint-disable-line no-process-env
|
||||
|
||||
export default {
|
||||
components: {
|
||||
modifyInventory,
|
||||
buyGemsModal,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
gryphon,
|
||||
twitter,
|
||||
facebook,
|
||||
instagram,
|
||||
heart,
|
||||
}),
|
||||
debugMenuShown: false,
|
||||
IS_PRODUCTION,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
...mapState(['isUserLoaded']),
|
||||
getDataDisplayToolUrl () {
|
||||
const base = 'https://oldgods.net/habitrpg/habitrpg_user_data_display.html';
|
||||
if (!this.user) return;
|
||||
|
||||
return `${base}?uuid=${this.user._id}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
plusTenHealth () {
|
||||
this.$store.dispatch('user:set', {
|
||||
'stats.hp': this.user.stats.hp += 10,
|
||||
});
|
||||
},
|
||||
setHealthLow () {
|
||||
this.$store.dispatch('user:set', {
|
||||
'stats.hp': 1,
|
||||
});
|
||||
},
|
||||
async addMissedDay (numberOfDays) {
|
||||
if (!confirm(`Are you sure you want to reset the day by ${numberOfDays} day(s)?`)) return;
|
||||
|
||||
let date = moment(this.user.lastCron).subtract(numberOfDays, 'days').toDate();
|
||||
|
||||
await axios.post('/api/v4/debug/set-cron', {
|
||||
lastCron: date,
|
||||
});
|
||||
|
||||
// @TODO: Notification.text('-' + numberOfDays + ' day(s), remember to refresh');
|
||||
// @TODO: Sync user?
|
||||
},
|
||||
async addTenGems () {
|
||||
await axios.post('/api/v4/debug/add-ten-gems');
|
||||
// @TODO: Notification.text('+10 Gems!');
|
||||
this.user.balance += 2.5;
|
||||
},
|
||||
async addHourglass () {
|
||||
await axios.post('/api/v4/debug/add-hourglass');
|
||||
// @TODO: Sync?
|
||||
},
|
||||
addGold () {
|
||||
this.$store.dispatch('user:set', {
|
||||
'stats.gp': this.user.stats.gp + 500,
|
||||
});
|
||||
},
|
||||
addMana () {
|
||||
this.$store.dispatch('user:set', {
|
||||
'stats.mp': this.user.stats.mp + 500,
|
||||
});
|
||||
},
|
||||
addLevelsAndGold () {
|
||||
this.$store.dispatch('user:set', {
|
||||
'stats.exp': this.user.stats.exp + 10000,
|
||||
'stats.gp': this.user.stats.gp + 10000,
|
||||
'stats.mp': this.user.stats.mp + 10000,
|
||||
});
|
||||
},
|
||||
addExp () {
|
||||
// @TODO: Name these variables better
|
||||
let exp = 0;
|
||||
let five = 10 * this.user.stats.lvl;
|
||||
let four = Math.pow(this.user.stats.lvl, 2) * 0.25;
|
||||
let three = four + five + 139.75;
|
||||
let two = three / 10;
|
||||
let one = Math.round(two) * 10;
|
||||
exp = this.user.stats.exp + one;
|
||||
|
||||
this.$store.dispatch('user:set', {
|
||||
'stats.exp': exp,
|
||||
});
|
||||
},
|
||||
addOneLevel () {
|
||||
this.$store.dispatch('user:set', {
|
||||
'stats.lvl': this.user.stats.lvl + 1,
|
||||
});
|
||||
},
|
||||
async addQuestProgress () {
|
||||
await axios.post('/api/v4/debug/quest-progress');
|
||||
|
||||
// @TODO: Notification.text('Quest progress increased');
|
||||
// @TODO: User.sync();
|
||||
},
|
||||
async makeAdmin () {
|
||||
await axios.post('/api/v4/debug/make-admin');
|
||||
|
||||
// @TODO: Notification.text('You are now an admin! Go to the Hall of Heroes to change your contributor level.');
|
||||
// @TODO: sync()
|
||||
},
|
||||
openModifyInventoryModal () {
|
||||
this.$root.$emit('bv::show::modal', 'modify-inventory');
|
||||
},
|
||||
donate () {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Gems > Donate',
|
||||
});
|
||||
this.$root.$emit('bv::show::modal', 'buy-gems', {alreadyTracked: true});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,221 @@
|
||||
<template lang="pug">
|
||||
.form
|
||||
.form-group.row.text-center
|
||||
.col-12.col-md-6
|
||||
.btn.btn-secondary.social-button(@click='socialAuth("facebook")')
|
||||
.svg-icon.social-icon(v-html="icons.facebookIcon")
|
||||
span {{registering ? $t('signUpWithSocial', {social: 'Facebook'}) : $t('loginWithSocial', {social: 'Facebook'})}}
|
||||
.col-12.col-md-6
|
||||
.btn.btn-secondary.social-button(@click='socialAuth("google")')
|
||||
.svg-icon.social-icon(v-html="icons.googleIcon")
|
||||
span {{registering ? $t('signUpWithSocial', {social: 'Google'}) : $t('loginWithSocial', {social: 'Google'})}}
|
||||
.form-group(v-if='registering')
|
||||
label(for='usernameInput', v-once) {{$t('username')}}
|
||||
input#usernameInput.form-control(type='text', :placeholder='$t("usernamePlaceholder")', v-model='username', :class='{"input-valid": usernameValid, "input-invalid": usernameInvalid}')
|
||||
.form-group(v-if='!registering')
|
||||
label(for='usernameInput', v-once) {{$t('emailOrUsername')}}
|
||||
input#usernameInput.form-control(type='text', :placeholder='$t("emailOrUsername")', v-model='username')
|
||||
.form-group(v-if='registering')
|
||||
label(for='emailInput', v-once) {{$t('email')}}
|
||||
input#emailInput.form-control(type='email', :placeholder='$t("emailPlaceholder")', v-model='email', :class='{"input-invalid": emailInvalid, "input-valid": emailValid}')
|
||||
.form-group
|
||||
label(for='passwordInput', v-once) {{$t('password')}}
|
||||
a.float-right.forgot-password(v-once, v-if='!registering', @click='forgotPassword = true') {{$t('forgotPassword')}}
|
||||
input#passwordInput.form-control(type='password', :placeholder='$t(registering ? "passwordPlaceholder" : "password")', v-model='password')
|
||||
.form-group(v-if='registering')
|
||||
label(for='confirmPasswordInput', v-once) {{$t('confirmPassword')}}
|
||||
input#confirmPasswordInput.form-control(type='password', :placeholder='$t("confirmPasswordPlaceholder")', v-model='passwordConfirm', :class='{"input-invalid": passwordConfirmInvalid, "input-valid": passwordConfirmValid}')
|
||||
small.form-text(v-once, v-html="$t('termsAndAgreement')")
|
||||
.text-center
|
||||
.btn.btn-info(@click='register()', v-if='registering', v-once) {{$t('joinHabitica')}}
|
||||
.btn.btn-info(@click='login()', v-if='!registering', v-once) {{$t('login')}}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.form {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding-top: 2em;
|
||||
padding-bottom: 4em;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.form-group {
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.social-button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
white-space: inherit;
|
||||
text-align: center;
|
||||
|
||||
.text {
|
||||
display: inline-block;
|
||||
min-height: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
margin-right: 1em;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: .2em;
|
||||
}
|
||||
|
||||
small.form-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input-valid {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import hello from 'hellojs';
|
||||
import { setUpAxios } from 'client/libs/auth';
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
|
||||
import facebookSquareIcon from 'assets/svg/facebook-square.svg';
|
||||
import googleIcon from 'assets/svg/google.svg';
|
||||
|
||||
export default {
|
||||
name: 'AuthForm',
|
||||
data () {
|
||||
let data = {
|
||||
registering: true,
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
usernameIssues: [],
|
||||
};
|
||||
|
||||
data.icons = Object.freeze({
|
||||
facebookIcon: facebookSquareIcon,
|
||||
googleIcon,
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
mounted () {
|
||||
hello.init({
|
||||
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line
|
||||
// windows: WINDOWS_CLIENT_ID,
|
||||
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
emailValid () {
|
||||
if (this.email.length <= 3) return false;
|
||||
return isEmail(this.email);
|
||||
},
|
||||
emailInvalid () {
|
||||
return !this.emailValid;
|
||||
},
|
||||
usernameValid () {
|
||||
if (this.username.length < 1) return false;
|
||||
return this.usernameIssues.length === 0;
|
||||
},
|
||||
usernameInvalid () {
|
||||
return !this.usernameValid;
|
||||
},
|
||||
passwordConfirmValid () {
|
||||
if (this.passwordConfirm.length <= 3) return false;
|
||||
return this.passwordConfirm === this.password;
|
||||
},
|
||||
passwordConfirmInvalid () {
|
||||
return !this.passwordConfirmValid;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
username () {
|
||||
this.validateUsername(this.username);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// eslint-disable-next-line func-names
|
||||
validateUsername: debounce(function (username) {
|
||||
if (username.length < 1) {
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username: this.username,
|
||||
}).then(res => {
|
||||
if (res.issues !== undefined) {
|
||||
this.usernameIssues = res.issues;
|
||||
} else {
|
||||
this.usernameIssues = [];
|
||||
}
|
||||
});
|
||||
}, 500),
|
||||
// @TODO: Abstract hello in to action or lib
|
||||
async socialAuth (network) {
|
||||
try {
|
||||
await hello(network).logout();
|
||||
} catch (e) {} // eslint-disable-line
|
||||
|
||||
try {
|
||||
const redirectUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
let auth = await hello(network).login({
|
||||
scope: 'email',
|
||||
redirect_uri: redirectUrl, // eslint-disable-line camelcase
|
||||
});
|
||||
|
||||
await this.$store.dispatch('auth:socialAuth', {
|
||||
auth,
|
||||
});
|
||||
|
||||
await this.finishAuth();
|
||||
} catch (err) {
|
||||
console.error(err); // eslint-disable-line no-console
|
||||
// logout the user
|
||||
await hello(network).logout();
|
||||
this.socialAuth(network); // login again
|
||||
}
|
||||
},
|
||||
async register () {
|
||||
if (!this.email) {
|
||||
alert(this.$t('missingEmail'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.password !== this.passwordConfirm) {
|
||||
alert(this.$t('passwordConfirmationMatch'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('auth:register', {
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
passwordConfirm: this.passwordConfirm,
|
||||
});
|
||||
|
||||
await this.finishAuth();
|
||||
} catch (e) {
|
||||
if (e.response.data.data && e.response.data.data.errors) {
|
||||
const message = e.response.data.data.errors.map(error => `${error.message}\n`);
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
},
|
||||
async finishAuth () {
|
||||
setUpAxios();
|
||||
|
||||
await this.$store.dispatch('user:fetch', {forceLoad: true});
|
||||
|
||||
this.$emit('authenticate');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script>
|
||||
export default {
|
||||
components: {},
|
||||
methods: {
|
||||
async logout () {
|
||||
return await this.$store.dispatch('auth:logout');
|
||||
},
|
||||
},
|
||||
created () {
|
||||
this.logout();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,575 @@
|
||||
<template lang="pug">
|
||||
.form-wrapper
|
||||
#top-background
|
||||
.seamless_stars_varied_opacity_repeat
|
||||
|
||||
form#login-form(
|
||||
@submit.prevent='handleSubmit',
|
||||
@keyup.enter="handleSubmit",
|
||||
v-if="!forgotPassword && !resetPasswordSetNewOne",
|
||||
)
|
||||
.text-center
|
||||
div
|
||||
.svg-icon.gryphon
|
||||
div
|
||||
.svg-icon.habitica-logo(v-html="icons.habiticaIcon")
|
||||
.form-group.row.text-center
|
||||
.col-12.col-md-6
|
||||
.btn.btn-secondary.social-button(@click='socialAuth("facebook")')
|
||||
.svg-icon.social-icon(v-html="icons.facebookIcon")
|
||||
.text {{registering ? $t('signUpWithSocial', {social: 'Facebook'}) : $t('loginWithSocial', {social: 'Facebook'})}}
|
||||
.col-12.col-md-6
|
||||
.btn.btn-secondary.social-button(@click='socialAuth("google")')
|
||||
.svg-icon.social-icon(v-html="icons.googleIcon")
|
||||
.text {{registering ? $t('signUpWithSocial', {social: 'Google'}) : $t('loginWithSocial', {social: 'Google'})}}
|
||||
.form-group(v-if='registering')
|
||||
label(for='usernameInput', v-once) {{$t('username')}}
|
||||
input#usernameInput.form-control(type='text', :placeholder='$t("usernamePlaceholder")', v-model='username', :class='{"input-valid": usernameValid, "input-invalid": usernameInvalid}')
|
||||
.input-error(v-for="issue in usernameIssues") {{ issue }}
|
||||
.form-group(v-if='!registering')
|
||||
label(for='usernameInput', v-once) {{$t('emailOrUsername')}}
|
||||
input#usernameInput.form-control(type='text', :placeholder='$t("emailOrUsername")', v-model='username')
|
||||
.form-group(v-if='registering')
|
||||
label(for='emailInput', v-once) {{$t('email')}}
|
||||
input#emailInput.form-control(type='email', :placeholder='$t("emailPlaceholder")', v-model='email', :class='{"input-invalid": emailInvalid, "input-valid": emailValid}')
|
||||
.form-group
|
||||
label(for='passwordInput', v-once) {{$t('password')}}
|
||||
a.float-right.forgot-password(v-once, v-if='!registering', @click='forgotPassword = true') {{$t('forgotPassword')}}
|
||||
input#passwordInput.form-control(type='password', :placeholder='$t(registering ? "passwordPlaceholder" : "password")', v-model='password')
|
||||
.form-group(v-if='registering')
|
||||
label(for='confirmPasswordInput', v-once) {{$t('confirmPassword')}}
|
||||
input#confirmPasswordInput.form-control(type='password', :placeholder='$t("confirmPasswordPlaceholder")', v-model='passwordConfirm', :class='{"input-invalid": passwordConfirmInvalid, "input-valid": passwordConfirmValid}')
|
||||
small.form-text(v-once, v-html="$t('termsAndAgreement')")
|
||||
.text-center
|
||||
.btn.btn-info(@click='register()', v-if='registering', v-once) {{$t('joinHabitica')}}
|
||||
.btn.btn-info(@click='login()', v-if='!registering', v-once) {{$t('login')}}
|
||||
.toggle-links
|
||||
router-link(:to="{name: 'login'}", v-if='registering', exact)
|
||||
a.toggle-link(v-once, v-html="$t('alreadyHaveAccountLogin')")
|
||||
router-link(:to="{name: 'register'}", v-if='!registering', exact)
|
||||
a.toggle-link(v-once, v-html="$t('dontHaveAccountSignup')")
|
||||
|
||||
form#forgot-form(v-on:submit.prevent='handleSubmit', @keyup.enter="handleSubmit", v-if='forgotPassword')
|
||||
.text-center
|
||||
div
|
||||
.svg-icon.gryphon
|
||||
div
|
||||
.svg-icon.habitica-logo(v-html="icons.habiticaIcon")
|
||||
.header
|
||||
h2(v-once) {{ $t('emailNewPass') }}
|
||||
p(v-once) {{ $t('forgotPasswordSteps') }}
|
||||
.form-group.row.text-center
|
||||
label(for='usernameInput', v-once) {{$t('email')}}
|
||||
input#usernameInput.form-control(type='text', :placeholder='$t("emailPlaceholder")', v-model='username')
|
||||
.text-center
|
||||
.btn.btn-info(@click='forgotPasswordLink()', v-once) {{$t('sendLink')}}
|
||||
|
||||
form#reset-password-set-new-one-form(v-on:submit.prevent='handleSubmit', @keyup.enter="handleSubmit", v-if='resetPasswordSetNewOne')
|
||||
.text-center
|
||||
div
|
||||
.svg-icon.gryphon
|
||||
div
|
||||
.svg-icon.habitica-logo(v-html="icons.habiticaIcon")
|
||||
.header
|
||||
h2 {{ $t('passwordResetPage') }}
|
||||
.form-group
|
||||
label(for='passwordInput', v-once) {{$t('newPass')}}
|
||||
input#passwordInput.form-control(type='password', :placeholder="$t('password')", v-model='password')
|
||||
.form-group
|
||||
label(for='confirmPasswordInput', v-once) {{$t('confirmPass')}}
|
||||
input#confirmPasswordInput.form-control(type='password', :placeholder='$t("confirmPasswordPlaceholder")', v-model='passwordConfirm')
|
||||
.text-center
|
||||
.btn.btn-info(
|
||||
@click='resetPasswordSetNewOneLink()',
|
||||
:enabled="!resetPasswordSetNewOneData.hasError"
|
||||
) {{$t('setNewPass')}}
|
||||
|
||||
#bottom-wrap(:class="`bottom-wrap-${!registering ? 'login' : 'register'}`")
|
||||
#bottom-background
|
||||
.seamless_mountains_demo_repeat
|
||||
.midground_foreground_extended2
|
||||
</template>
|
||||
|
||||
<style>
|
||||
html, body, #app {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
small a, small a:hover {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
@media only screen and (min-height: 1080px) {
|
||||
.bottom-wrap-register {
|
||||
margin-top: 6em;
|
||||
position: fixed !important;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-height: 862px) {
|
||||
.bottom-wrap-login {
|
||||
margin-top: 6em;
|
||||
position: fixed !important;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
#login-form {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.form-wrapper {
|
||||
background-color: $purple-200;
|
||||
background: $purple-200; /* For browsers that do not support gradients */
|
||||
background: linear-gradient(to bottom, #4f2a93, #6133b4); /* Standard syntax */
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder { /* Chrome/Opera/Safari */
|
||||
color: $purple-400;
|
||||
}
|
||||
::-moz-placeholder { /* Firefox 19+ */
|
||||
color: $purple-400;
|
||||
}
|
||||
:-ms-input-placeholder { /* IE 10+ */
|
||||
color: $purple-400;
|
||||
}
|
||||
:-moz-placeholder { /* Firefox 18- */
|
||||
color: $purple-400;
|
||||
}
|
||||
::placeholder { // Standard browsers
|
||||
color: $purple-400;
|
||||
}
|
||||
|
||||
#login-form, #forgot-form, #reset-password-set-new-one-form {
|
||||
margin: 0 auto;
|
||||
width: 40em;
|
||||
padding-top: 5em;
|
||||
padding-bottom: 4em;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.header {
|
||||
h2 {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.gryphon {
|
||||
background-image: url('~assets/images/melior@3x.png');
|
||||
width: 63.2px;
|
||||
height: 69.4px;
|
||||
background-size: cover;
|
||||
color: $white;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.habitica-logo {
|
||||
width: 144px;
|
||||
height: 31px;
|
||||
margin: 2em auto;
|
||||
}
|
||||
|
||||
label {
|
||||
color: $white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input {
|
||||
margin-bottom: 2em;
|
||||
border-radius: 2px;
|
||||
background-color: #432874;
|
||||
border-color: transparent;
|
||||
height: 50px;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
#usernameInput.input-invalid {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
font-size: 14px;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.social-button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
white-space: inherit;
|
||||
text-align: center;
|
||||
|
||||
.text {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
margin-right: 1em;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: .2em;
|
||||
}
|
||||
}
|
||||
|
||||
#top-background {
|
||||
.seamless_stars_varied_opacity_repeat {
|
||||
background-image: url('~assets/images/auth/seamless_stars_varied_opacity.png');
|
||||
background-repeat: repeat-x;
|
||||
position: absolute;
|
||||
height: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#bottom-wrap {
|
||||
margin-top: 6em;
|
||||
position: static;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#bottom-background {
|
||||
position: relative;
|
||||
|
||||
.seamless_mountains_demo_repeat {
|
||||
background-image: url('~assets/images/auth/seamless_mountains_demo.png');
|
||||
background-repeat: repeat-x;
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.midground_foreground_extended2 {
|
||||
background-image: url('~assets/images/auth/midground_foreground_extended2.png');
|
||||
position: relative;
|
||||
width: 1500px;
|
||||
max-width: 100%;
|
||||
height: 150px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-links {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.toggle-link {
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
color: #bda8ff !important;
|
||||
}
|
||||
|
||||
.input-error {
|
||||
color: #fff;
|
||||
font-size: 90%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import hello from 'hellojs';
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
|
||||
import gryphon from 'assets/svg/gryphon.svg';
|
||||
import habiticaIcon from 'assets/svg/habitica-logo.svg';
|
||||
import facebookSquareIcon from 'assets/svg/facebook-square.svg';
|
||||
import googleIcon from 'assets/svg/google.svg';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
let data = {
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
forgotPassword: false,
|
||||
resetPasswordSetNewOneData: {
|
||||
hasError: null,
|
||||
code: null,
|
||||
},
|
||||
usernameIssues: [],
|
||||
};
|
||||
|
||||
data.icons = Object.freeze({
|
||||
gryphon,
|
||||
habiticaIcon,
|
||||
facebookIcon: facebookSquareIcon,
|
||||
googleIcon,
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
computed: {
|
||||
registering () {
|
||||
if (this.$route.path.startsWith('/register')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
resetPasswordSetNewOne () {
|
||||
if (this.$route.path.startsWith('/reset-password')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
emailValid () {
|
||||
if (this.email.length <= 3) return false;
|
||||
return isEmail(this.email);
|
||||
},
|
||||
emailInvalid () {
|
||||
if (this.email.length <= 3) return false;
|
||||
return !this.emailValid;
|
||||
},
|
||||
usernameValid () {
|
||||
if (this.username.length < 1) return false;
|
||||
return this.usernameIssues.length === 0;
|
||||
},
|
||||
usernameInvalid () {
|
||||
if (this.username.length < 1) return false;
|
||||
return !this.usernameValid;
|
||||
},
|
||||
passwordConfirmValid () {
|
||||
if (this.passwordConfirm.length <= 3) return false;
|
||||
return this.passwordConfirm === this.password;
|
||||
},
|
||||
passwordConfirmInvalid () {
|
||||
if (this.passwordConfirm.length <= 3) return false;
|
||||
return !this.passwordConfirmValid;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
hello.init({
|
||||
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line
|
||||
// windows: WINDOWS_CLIENT_ID,
|
||||
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
$route: {
|
||||
handler () {
|
||||
if (this.resetPasswordSetNewOne) {
|
||||
const query = this.$route.query;
|
||||
const code = query.code;
|
||||
const hasError = query.hasError === 'true' ? true : false;
|
||||
if (hasError) {
|
||||
alert(query.message);
|
||||
this.$router.push({name: 'login'});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
alert(this.$t('invalidPasswordResetCode'));
|
||||
this.$router.push({name: 'login'});
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetPasswordSetNewOneData.code = query.code;
|
||||
this.resetPasswordSetNewOneData.hasError = hasError;
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
username () {
|
||||
this.validateUsername(this.username);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// eslint-disable-next-line func-names
|
||||
validateUsername: debounce(function (username) {
|
||||
if (username.length <= 3 || !this.registering) {
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username: this.username,
|
||||
}).then(res => {
|
||||
if (res.issues !== undefined) {
|
||||
this.usernameIssues = res.issues;
|
||||
} else {
|
||||
this.usernameIssues = [];
|
||||
}
|
||||
});
|
||||
}, 500),
|
||||
async register () {
|
||||
// @TODO do not use alert
|
||||
if (!this.email) {
|
||||
alert(this.$t('missingEmail'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.password !== this.passwordConfirm) {
|
||||
alert(this.$t('passwordConfirmationMatch'));
|
||||
return;
|
||||
}
|
||||
|
||||
// @TODO: implement langauge and invite accepting
|
||||
// var url = ApiUrl.get() + "/api/v4/user/auth/local/register";
|
||||
// if (location.search && location.search.indexOf('Invite=') !== -1) { // matches groupInvite and partyInvite
|
||||
// url += location.search;
|
||||
// }
|
||||
//
|
||||
// if($rootScope.selectedLanguage) {
|
||||
// var toAppend = url.indexOf('?') !== -1 ? '&' : '?';
|
||||
// url = url + toAppend + 'lang=' + $rootScope.selectedLanguage.code;
|
||||
// }
|
||||
|
||||
await this.$store.dispatch('auth:register', {
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
passwordConfirm: this.passwordConfirm,
|
||||
});
|
||||
|
||||
let redirectTo;
|
||||
|
||||
if (this.$route.query.redirectTo) {
|
||||
redirectTo = this.$route.query.redirectTo;
|
||||
} else {
|
||||
redirectTo = '/';
|
||||
}
|
||||
|
||||
// @TODO do not reload entire page
|
||||
// problem is that app.vue created hook should be called again
|
||||
// after user is logged in / just signed up
|
||||
// ALSO it's the only way to make sure language data is reloaded and correct for the logged in user
|
||||
window.location.href = redirectTo;
|
||||
},
|
||||
async login () {
|
||||
await this.$store.dispatch('auth:login', {
|
||||
username: this.username,
|
||||
// email: this.email,
|
||||
password: this.password,
|
||||
});
|
||||
|
||||
let redirectTo;
|
||||
|
||||
if (this.$route.query.redirectTo) {
|
||||
redirectTo = this.$route.query.redirectTo;
|
||||
} else {
|
||||
redirectTo = '/';
|
||||
}
|
||||
|
||||
// @TODO do not reload entire page
|
||||
// problem is that app.vue created hook should be called again
|
||||
// after user is logged in / just signed up
|
||||
// ALSO it's the only way to make sure language data is reloaded and correct for the logged in user
|
||||
window.location.href = redirectTo;
|
||||
},
|
||||
// @TODO: Abstract hello in to action or lib
|
||||
async socialAuth (network) {
|
||||
try {
|
||||
await hello(network).logout();
|
||||
} catch (e) {} // eslint-disable-line
|
||||
|
||||
const redirectUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
let auth = await hello(network).login({
|
||||
scope: 'email',
|
||||
// explicitly pass the redirect url or it might redirect to /home
|
||||
redirect_uri: redirectUrl, // eslint-disable-line camelcase
|
||||
});
|
||||
|
||||
await this.$store.dispatch('auth:socialAuth', {
|
||||
auth,
|
||||
});
|
||||
|
||||
let redirectTo;
|
||||
|
||||
if (this.$route.query.redirectTo) {
|
||||
redirectTo = this.$route.query.redirectTo;
|
||||
} else {
|
||||
redirectTo = '/';
|
||||
}
|
||||
|
||||
// @TODO do not reload entire page
|
||||
// problem is that app.vue created hook should be called again
|
||||
// after user is logged in / just signed up
|
||||
// ALSO it's the only way to make sure language data is reloaded and correct for the logged in user
|
||||
window.location.href = redirectTo;
|
||||
},
|
||||
handleSubmit () {
|
||||
if (this.registering) {
|
||||
this.register();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.forgotPassword) {
|
||||
this.forgotPasswordLink();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.resetPasswordSetNewOne) {
|
||||
this.resetPasswordSetNewOneLink();
|
||||
return;
|
||||
}
|
||||
|
||||
this.login();
|
||||
},
|
||||
async forgotPasswordLink () {
|
||||
if (!this.username) {
|
||||
alert(this.$t('missingEmail'));
|
||||
return;
|
||||
}
|
||||
|
||||
await axios.post('/api/v4/user/reset-password', {
|
||||
email: this.username,
|
||||
});
|
||||
|
||||
alert(this.$t('newPassSent'));
|
||||
},
|
||||
async resetPasswordSetNewOneLink () {
|
||||
if (!this.password) {
|
||||
alert(this.$t('missingNewPassword'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.password !== this.passwordConfirm) {
|
||||
// @TODO i18n and don't use alerts
|
||||
alert(this.$t('passwordConfirmationMatch'));
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await axios.post('/api/v4/user/auth/reset-password-set-new-one', {
|
||||
newPassword: this.password,
|
||||
confirmPassword: this.passwordConfirm,
|
||||
code: this.resetPasswordSetNewOneData.code,
|
||||
});
|
||||
|
||||
if (res.data.message) {
|
||||
alert(res.data.message);
|
||||
}
|
||||
|
||||
this.password = '';
|
||||
this.passwordConfirm = '';
|
||||
this.resetPasswordSetNewOneData.code = '';
|
||||
this.resetPasswordSetNewOneData.hasError = false;
|
||||
this.$router.push({name: 'login'});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,228 @@
|
||||
<template lang="pug">
|
||||
.avatar(:style="{width, height, paddingTop}", :class="backgroundClass", @click.prevent='castEnd()')
|
||||
.character-sprites(:style='{margin: spritesMargin}')
|
||||
template(v-if="!avatarOnly")
|
||||
// Mount Body
|
||||
span(v-if="member.items.currentMount", :class="'Mount_Body_' + member.items.currentMount")
|
||||
|
||||
// Buffs that cause visual changes to avatar: Snowman, Ghost, Flower, etc
|
||||
template(v-for="(klass, item) in visualBuffs")
|
||||
span(v-if="member.stats.buffs[item] && showVisualBuffs", :class="klass")
|
||||
|
||||
// Show flower ALL THE TIME!!!
|
||||
// See https://github.com/HabitRPG/habitica/issues/7133
|
||||
span(:class="'hair_flower_' + member.preferences.hair.flower")
|
||||
|
||||
// Show avatar only if not currently affected by visual buff
|
||||
template(v-if="showAvatar()")
|
||||
span(:class="['chair_' + member.preferences.chair, specialMountClass]")
|
||||
span(:class="[getGearClass('back'), specialMountClass]")
|
||||
span(:class="[skinClass, specialMountClass]")
|
||||
span(:class="[member.preferences.size + '_shirt_' + member.preferences.shirt, specialMountClass]")
|
||||
span(:class="['head_0', specialMountClass]")
|
||||
span(:class="[member.preferences.size + '_' + getGearClass('armor'), specialMountClass]")
|
||||
span(:class="[getGearClass('back_collar'), specialMountClass]")
|
||||
template(v-for="type in ['bangs', 'base', 'mustache', 'beard']")
|
||||
span(:class="['hair_' + type + '_' + member.preferences.hair[type] + '_' + member.preferences.hair.color, specialMountClass]")
|
||||
span(:class="[getGearClass('body'), specialMountClass]")
|
||||
span(:class="[getGearClass('eyewear'), specialMountClass]")
|
||||
span(:class="[getGearClass('head'), specialMountClass]")
|
||||
span(:class="[getGearClass('headAccessory'), specialMountClass]")
|
||||
span(:class="['hair_flower_' + member.preferences.hair.flower, specialMountClass]")
|
||||
span(v-if="!hideGear('shield')", :class="[getGearClass('shield'), specialMountClass]")
|
||||
span(v-if="!hideGear('weapon')", :class="[getGearClass('weapon'), specialMountClass]")
|
||||
|
||||
// Resting
|
||||
span.zzz(v-if="member.preferences.sleep")
|
||||
|
||||
template(v-if="!avatarOnly")
|
||||
// Mount Head
|
||||
span(v-if="member.items.currentMount", :class="'Mount_Head_' + member.items.currentMount")
|
||||
// Pet
|
||||
span.current-pet(v-if="member.items.currentPet", :class="'Pet-' + member.items.currentPet")
|
||||
class-badge.under-avatar(v-if="hasClass && !hideClassBadge", :member-class="member.stats.class")
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.avatar {
|
||||
width: 140px;
|
||||
height: 147px;
|
||||
image-rendering: pixelated;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.character-sprites {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
.character-sprites span {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.current-pet {
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.offset-kangaroo {
|
||||
margin-top: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
import ClassBadge from 'client/components/members/classBadge';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ClassBadge,
|
||||
},
|
||||
props: {
|
||||
member: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
avatarOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hideClassBadge: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
withBackground: {
|
||||
type: Boolean,
|
||||
},
|
||||
overrideAvatarGear: {
|
||||
type: Object,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 140,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 147,
|
||||
},
|
||||
spritesMargin: {
|
||||
type: String,
|
||||
default: '0 auto 0 24px',
|
||||
},
|
||||
overrideTopPadding: {
|
||||
type: String,
|
||||
},
|
||||
showVisualBuffs: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
flatGear: 'content.gear.flat',
|
||||
}),
|
||||
hasClass () {
|
||||
return this.$store.getters['members:hasClass'](this.member);
|
||||
},
|
||||
isBuffed () {
|
||||
return this.$store.getters['members:isBuffed'](this.member);
|
||||
},
|
||||
paddingTop () {
|
||||
if (this.overrideTopPadding) {
|
||||
return this.overrideTopPadding;
|
||||
}
|
||||
|
||||
let val = '28px';
|
||||
|
||||
if (!this.avatarOnly) {
|
||||
if (this.member.items.currentPet) val = '24px';
|
||||
if (this.member.items.currentMount) val = '0px';
|
||||
}
|
||||
|
||||
return val;
|
||||
},
|
||||
backgroundClass () {
|
||||
let background = this.member.preferences.background;
|
||||
|
||||
let allowToShowBackground = !this.avatarOnly || this.withBackground;
|
||||
|
||||
if (this.overrideAvatarGear && this.overrideAvatarGear.background) {
|
||||
return `background_${this.overrideAvatarGear.background}`;
|
||||
}
|
||||
|
||||
if (background && allowToShowBackground) {
|
||||
return `background_${this.member.preferences.background}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
visualBuffs () {
|
||||
return {
|
||||
snowball: 'snowman',
|
||||
spookySparkles: 'ghost',
|
||||
shinySeed: `avatar_floral_${this.member.stats.class}`,
|
||||
seafoam: 'seafoam_star',
|
||||
};
|
||||
},
|
||||
skinClass () {
|
||||
let baseClass = `skin_${this.member.preferences.skin}`;
|
||||
|
||||
return `${baseClass}${this.member.preferences.sleep ? '_sleep' : ''}`;
|
||||
},
|
||||
costumeClass () {
|
||||
return this.member.preferences.costume ? 'costume' : 'equipped';
|
||||
},
|
||||
specialMountClass () {
|
||||
if (!this.avatarOnly && this.member.items.currentMount && this.member.items.currentMount.indexOf('Kangaroo') !== -1) {
|
||||
return 'offset-kangaroo';
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getGearClass (gearType) {
|
||||
let result = this.member.items.gear[this.costumeClass][gearType];
|
||||
|
||||
if (this.overrideAvatarGear && this.overrideAvatarGear[gearType]) {
|
||||
result = this.overrideAvatarGear[gearType];
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
hideGear (gearType) {
|
||||
if (gearType === 'weapon') {
|
||||
let equippedWeapon = this.member.items.gear[this.costumeClass][gearType];
|
||||
|
||||
if (!equippedWeapon) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let equippedIsTwoHanded = this.flatGear[equippedWeapon].twoHanded;
|
||||
let hasOverrideShield = this.overrideAvatarGear && this.overrideAvatarGear.shield;
|
||||
|
||||
return equippedIsTwoHanded && hasOverrideShield;
|
||||
} else if (gearType === 'shield') {
|
||||
let overrideWeapon = this.overrideAvatarGear && this.overrideAvatarGear.weapon;
|
||||
let overrideIsTwoHanded = overrideWeapon && this.flatGear[overrideWeapon].twoHanded;
|
||||
|
||||
return overrideIsTwoHanded;
|
||||
}
|
||||
},
|
||||
castEnd (e) {
|
||||
if (!this.$store.state.spellOptions.castingSpell) return;
|
||||
this.$root.$emit('castEnd', this.member, 'user', e);
|
||||
},
|
||||
showAvatar () {
|
||||
if (!this.showVisualBuffs)
|
||||
return true;
|
||||
|
||||
let buffs = this.member.stats.buffs;
|
||||
|
||||
return !buffs.snowball && !buffs.spookySparkles && !buffs.shinySeed && !buffs.seafoam;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template lang="pug">
|
||||
#body.section.customize-section
|
||||
sub-menu.text-center(:items="items", :activeSubPage="activeSubPage", @changeSubPage="changeSubPage($event)")
|
||||
div(v-if='activeSubPage === "size"')
|
||||
customize-options(
|
||||
:items="sizes",
|
||||
:currentValue="user.preferences.size"
|
||||
)
|
||||
div(v-if='activeSubPage === "shirt"')
|
||||
customize-options(
|
||||
:items="freeShirts",
|
||||
:currentValue="user.preferences.shirt"
|
||||
)
|
||||
customize-options(
|
||||
v-if='editing',
|
||||
:items='specialShirts',
|
||||
:currentValue="user.preferences.shirt",
|
||||
:fullSet='!userOwnsSet("shirt", specialShirtKeys)',
|
||||
@unlock='unlock(`shirt.${specialShirtKeys.join(",shirt.")}`)'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import appearance from 'common/script/content/appearance';
|
||||
import {subPageMixin} from '../../mixins/subPage';
|
||||
import {userStateMixin} from '../../mixins/userState';
|
||||
import {avatarEditorUtilies} from '../../mixins/avatarEditUtilities';
|
||||
import subMenu from './sub-menu';
|
||||
import customizeOptions from './customize-options';
|
||||
import gem from 'assets/svg/gem.svg';
|
||||
|
||||
const freeShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price === 0);
|
||||
const specialShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
|
||||
|
||||
|
||||
export default {
|
||||
props: [
|
||||
'editing',
|
||||
],
|
||||
components: {
|
||||
subMenu,
|
||||
customizeOptions,
|
||||
},
|
||||
mixins: [
|
||||
subPageMixin,
|
||||
userStateMixin,
|
||||
avatarEditorUtilies,
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
specialShirtKeys,
|
||||
icons: Object.freeze({
|
||||
gem,
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
id: 'size',
|
||||
label: this.$t('size'),
|
||||
},
|
||||
{
|
||||
id: 'shirt',
|
||||
label: this.$t('shirt'),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sizes () {
|
||||
return ['slim', 'broad'].map(s => this.mapKeysToFreeOption(s, 'size'));
|
||||
},
|
||||
freeShirts () {
|
||||
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt'));
|
||||
},
|
||||
specialShirts () {
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.specialShirtKeys;
|
||||
let options = keys.map(key => this.mapKeysToOption(key, 'shirt'));
|
||||
return options;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.changeSubPage('size');
|
||||
},
|
||||
methods: {
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,304 @@
|
||||
<template lang="pug">
|
||||
.customize-options(:class="{'background-set': fullSet}")
|
||||
.outer-option-background(
|
||||
v-for='option in items',
|
||||
:key='option.key',
|
||||
@click='option.click(option)',
|
||||
:class='{locked: option.gemLocked || option.goldLocked, premium: Boolean(option.gem), active: option.active || currentValue === option.key, none: option.none, hide: option.hide }'
|
||||
)
|
||||
.option
|
||||
.sprite.customize-option(:class='option.class')
|
||||
.redline-outer(v-if="option.none")
|
||||
.redline
|
||||
.gem-lock(v-if='option.gemLocked')
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span {{ option.gem }}
|
||||
.gold-lock(v-if='option.goldLocked')
|
||||
.svg-icon.gold(v-html='icons.gold')
|
||||
span {{ option.gold }}
|
||||
.purchase-set(v-if='fullSet', @click='unlock()')
|
||||
span.label {{ $t('purchaseAll') }}
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span.price 5
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gem from 'assets/svg/gem.svg';
|
||||
import gold from 'assets/svg/gold.svg';
|
||||
import {avatarEditorUtilies} from '../../mixins/avatarEditUtilities';
|
||||
|
||||
export default {
|
||||
props: ['items', 'currentValue', 'fullSet'],
|
||||
mixins: [
|
||||
avatarEditorUtilies,
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
gem,
|
||||
gold,
|
||||
}),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
unlock () {
|
||||
this.$emit('unlock');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.customize-options {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.outer-option-background {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
pointer-events: visible;
|
||||
cursor: pointer;
|
||||
|
||||
&.premium {
|
||||
height: 112px;
|
||||
width: 96px;
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.option {
|
||||
margin: 12px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&.locked {
|
||||
border-radius: 2px;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
background-color: $white;
|
||||
|
||||
.option {
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
padding-left: 6px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 4px 0 rgba($black, 0.16), 0 1px 8px 0 rgba($black, 0.12);
|
||||
border: 1px solid $purple-500;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.locked):not(.active) {
|
||||
.option:hover {
|
||||
background-color: rgba(213, 200, 255, .32);
|
||||
}
|
||||
}
|
||||
|
||||
&.premium:not(.locked):not(.active) {
|
||||
border-radius: 2px;
|
||||
background-color: rgba(59, 202, 215, 0.1);
|
||||
}
|
||||
|
||||
&.none .option {
|
||||
.sprite {
|
||||
opacity: 0.24;
|
||||
}
|
||||
|
||||
.redline-outer {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
margin: 0 auto 0 0;
|
||||
|
||||
.redline {
|
||||
width: 60px;
|
||||
height: 4px;
|
||||
display: block;
|
||||
background: red;
|
||||
transform: rotate(-45deg);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 20px;
|
||||
margin-left: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active .option {
|
||||
background: white;
|
||||
border: solid 4px $purple-300;
|
||||
}
|
||||
|
||||
&.premium:not(.active) .option {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
.option {
|
||||
vertical-align: bottom;
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
|
||||
margin: 12px 8px;
|
||||
border: 4px solid transparent;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.outer-option-background:not(.none) {
|
||||
|
||||
.sprite.customize-option {
|
||||
// margin: 0 auto;
|
||||
//margin-left: -3px;
|
||||
//margin-top: -7px;
|
||||
margin-top: 0;
|
||||
margin-left: 0;
|
||||
|
||||
&.size, &.shirt {
|
||||
margin-top: -8px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
&.color-bangs {
|
||||
margin-top: 3px;
|
||||
}
|
||||
&.skin {
|
||||
margin-top: -4px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
&.chair {
|
||||
margin-left: -1px;
|
||||
margin-top: -1px;
|
||||
|
||||
&.button_chair_black {
|
||||
// different sprite margin?
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
&.handleless {
|
||||
margin-left: -5px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
}
|
||||
&.color, &.bangs {
|
||||
margin-top: 4px;
|
||||
margin-left: -3px;
|
||||
}
|
||||
|
||||
&.hair.base {
|
||||
margin-top: 0px;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
&.headAccessory {
|
||||
margin-top: 0;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
&.headband {
|
||||
margin-top: -6px;
|
||||
margin-left: -27px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text-center {
|
||||
.gem-lock, .gold-lock {
|
||||
display: inline-block;
|
||||
margin: 0 auto 8px;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.gem-lock, .gold-lock {
|
||||
.svg-icon {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.svg-icon, span {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.gem-lock span {
|
||||
color: $green-10
|
||||
}
|
||||
|
||||
.purchase-set {
|
||||
background: #fff;
|
||||
padding: 0.5em;
|
||||
border-radius: 0 0 2px 2px;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
cursor: pointer;
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
span.price {
|
||||
color: #24cc8f;
|
||||
}
|
||||
|
||||
.gem, .coin {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
&.single {
|
||||
width: 141px;
|
||||
}
|
||||
|
||||
width: 100%;
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.gem, .coin {
|
||||
width: 20px;
|
||||
margin: 0 .5em;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.background-set {
|
||||
background-color: #edecee;
|
||||
border-radius: 2px;
|
||||
|
||||
padding-top: 12px;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
|
||||
width: calc(100% - 24px);
|
||||
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
max-width: unset; // disable col12 styling
|
||||
flex: unset;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,272 @@
|
||||
<template lang="pug">
|
||||
#extra.section.container.customize-section
|
||||
sub-menu.text-center(:items="extraSubMenuItems", :activeSubPage="activeSubPage", @changeSubPage="changeSubPage($event)")
|
||||
|
||||
#hair-color(v-if='activeSubPage === "glasses"')
|
||||
customize-options(
|
||||
:items="eyewear"
|
||||
)
|
||||
|
||||
#animal-ears(v-if='activeSubPage === "ears"')
|
||||
customize-options(
|
||||
:items="animalItems('headAccessory')",
|
||||
:fullSet='!animalItemsOwned("headAccessory")',
|
||||
@unlock='unlock(animalItemsUnlockString("headAccessory"))'
|
||||
)
|
||||
|
||||
#animal-tails(v-if='activeSubPage === "tails"')
|
||||
customize-options(
|
||||
:items="animalItems('back')",
|
||||
:fullSet='!animalItemsOwned("back")',
|
||||
@unlock='unlock(animalItemsUnlockString("back"))'
|
||||
)
|
||||
#headband(v-if='activeSubPage === "headband"')
|
||||
customize-options(
|
||||
:items="headbands",
|
||||
)
|
||||
|
||||
#wheelchairs(v-if='activeSubPage === "wheelchair"')
|
||||
customize-options(
|
||||
:items="chairs",
|
||||
)
|
||||
#flowers(v-if='activeSubPage === "flower"')
|
||||
customize-options(
|
||||
:items="flowers",
|
||||
)
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import appearance from 'common/script/content/appearance';
|
||||
import {subPageMixin} from '../../mixins/subPage';
|
||||
import {userStateMixin} from '../../mixins/userState';
|
||||
import {avatarEditorUtilies} from '../../mixins/avatarEditUtilities';
|
||||
import subMenu from './sub-menu';
|
||||
import customizeOptions from './customize-options';
|
||||
import gem from 'assets/svg/gem.svg';
|
||||
|
||||
const freeShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price === 0);
|
||||
const specialShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
|
||||
|
||||
|
||||
export default {
|
||||
props: [
|
||||
'editing',
|
||||
],
|
||||
components: {
|
||||
subMenu,
|
||||
customizeOptions,
|
||||
},
|
||||
mixins: [
|
||||
subPageMixin,
|
||||
userStateMixin,
|
||||
avatarEditorUtilies,
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
animalItemKeys: {
|
||||
back: ['bearTail', 'cactusTail', 'foxTail', 'lionTail', 'pandaTail', 'pigTail', 'tigerTail', 'wolfTail'],
|
||||
headAccessory: ['bearEars', 'cactusEars', 'foxEars', 'lionEars', 'pandaEars', 'pigEars', 'tigerEars', 'wolfEars'],
|
||||
},
|
||||
chairKeys: ['none', 'black', 'blue', 'green', 'pink', 'red', 'yellow', 'handleless_black', 'handleless_blue', 'handleless_green', 'handleless_pink', 'handleless_red', 'handleless_yellow'],
|
||||
specialShirtKeys,
|
||||
icons: Object.freeze({
|
||||
gem,
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
id: 'size',
|
||||
label: this.$t('size'),
|
||||
},
|
||||
{
|
||||
id: 'shirt',
|
||||
label: this.$t('shirt'),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
extraSubMenuItems () {
|
||||
const items = [];
|
||||
|
||||
if (this.editing) {
|
||||
items.push({
|
||||
id: 'glasses',
|
||||
label: this.$t('glasses'),
|
||||
});
|
||||
}
|
||||
|
||||
items.push(
|
||||
{
|
||||
id: 'wheelchair',
|
||||
label: this.$t('wheelchair'),
|
||||
},
|
||||
{
|
||||
id: 'flower',
|
||||
label: this.$t('accent'),
|
||||
},
|
||||
);
|
||||
|
||||
if (this.editing) {
|
||||
items.push({
|
||||
id: 'ears',
|
||||
label: this.$t('animalEars'),
|
||||
});
|
||||
|
||||
items.push({
|
||||
id: 'tails',
|
||||
label: this.$t('animalTails'),
|
||||
});
|
||||
|
||||
items.push({
|
||||
id: 'headband',
|
||||
label: this.$t('headband'),
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
eyewear () {
|
||||
let keys = [
|
||||
'blackTopFrame', 'blueTopFrame', 'greenTopFrame', 'pinkTopFrame', 'redTopFrame', 'whiteTopFrame', 'yellowTopFrame',
|
||||
'blackHalfMoon', 'blueHalfMoon', 'greenHalfMoon', 'pinkHalfMoon', 'redHalfMoon', 'whiteHalfMoon', 'yellowHalfMoon',
|
||||
];
|
||||
let options = keys.map(key => {
|
||||
let newKey = `eyewear_special_${key}`;
|
||||
let option = {};
|
||||
option.key = key;
|
||||
option.active = this.user.preferences.costume ? this.user.items.gear.costume.eyewear === newKey : this.user.items.gear.equipped.eyewear === newKey;
|
||||
option.class = `eyewear_special_${key}`;
|
||||
option.click = () => {
|
||||
let type = this.user.preferences.costume ? 'costume' : 'equipped';
|
||||
|
||||
return this.equip(newKey, type);
|
||||
};
|
||||
return option;
|
||||
});
|
||||
return options;
|
||||
},
|
||||
freeShirts () {
|
||||
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt'));
|
||||
},
|
||||
specialShirts () {
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.specialShirtKeys;
|
||||
let options = keys.map(key => this.mapKeysToOption(key, 'shirt'));
|
||||
return options;
|
||||
},
|
||||
headbands () {
|
||||
let keys = ['blackHeadband', 'blueHeadband', 'greenHeadband', 'pinkHeadband', 'redHeadband', 'whiteHeadband', 'yellowHeadband'];
|
||||
let options = keys.map(key => {
|
||||
let newKey = `headAccessory_special_${key}`;
|
||||
let option = {};
|
||||
option.key = key;
|
||||
option.active = this.user.preferences.costume ? this.user.items.gear.costume.headAccessory === newKey : this.user.items.gear.equipped.headAccessory === newKey;
|
||||
option.class = `headAccessory_special_${option.key} headband`;
|
||||
option.click = () => {
|
||||
let type = this.user.preferences.costume ? 'costume' : 'equipped';
|
||||
return this.equip(newKey, type);
|
||||
};
|
||||
return option;
|
||||
});
|
||||
return options;
|
||||
},
|
||||
chairs () {
|
||||
let options = this.chairKeys.map(key => {
|
||||
let option = {};
|
||||
option.key = key;
|
||||
if (key === 'none') {
|
||||
option.none = true;
|
||||
}
|
||||
option.active = this.user.preferences.chair === key;
|
||||
option.class = `button_chair_${key} chair ${key.includes('handleless_') ? 'handleless' : ''}`;
|
||||
option.click = () => {
|
||||
return this.set({'preferences.chair': key});
|
||||
};
|
||||
return option;
|
||||
});
|
||||
return options;
|
||||
},
|
||||
flowers () {
|
||||
let keys = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
|
||||
let options = keys.map(key => {
|
||||
let option = {};
|
||||
option.key = key;
|
||||
if (key === 0) {
|
||||
option.none = true;
|
||||
}
|
||||
option.active = this.user.preferences.hair.flower === key;
|
||||
option.class = `hair_flower_${key} flower`;
|
||||
option.click = () => {
|
||||
return this.set({'preferences.hair.flower': key});
|
||||
};
|
||||
return option;
|
||||
});
|
||||
return options;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.changeSubPage(this.extraSubMenuItems[0].id);
|
||||
},
|
||||
methods: {
|
||||
animalItems (category) {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.animalItemKeys[category];
|
||||
let options = keys.map(key => {
|
||||
let newKey = `${category}_special_${key}`;
|
||||
let userPurchased = this.user.items.gear.owned[newKey];
|
||||
|
||||
let option = {};
|
||||
option.key = key;
|
||||
option.active = this.user.preferences.costume ? this.user.items.gear.costume[category] === newKey : this.user.items.gear.equipped[category] === newKey;
|
||||
option.class = `headAccessory_special_${option.key} ${category}`;
|
||||
if (category === 'back') {
|
||||
option.class = `icon_back_special_${option.key} back`;
|
||||
}
|
||||
option.gemLocked = userPurchased === undefined;
|
||||
option.goldLocked = userPurchased === false;
|
||||
if (option.goldLocked) {
|
||||
option.gold = 20;
|
||||
}
|
||||
if (option.gemLocked) {
|
||||
option.gem = 2;
|
||||
}
|
||||
option.locked = option.gemLocked || option.goldLocked;
|
||||
option.click = () => {
|
||||
if (option.gemLocked) {
|
||||
return this.unlock(`items.gear.owned.${newKey}`);
|
||||
} else if (option.goldLocked) {
|
||||
return this.buy(newKey);
|
||||
} else {
|
||||
let type = this.user.preferences.costume ? 'costume' : 'equipped';
|
||||
return this.equip(newKey, type);
|
||||
}
|
||||
};
|
||||
return option;
|
||||
});
|
||||
return options;
|
||||
},
|
||||
animalItemsUnlockString (category) {
|
||||
const keys = this.animalItemKeys[category].map(key => {
|
||||
return `items.gear.owned.${category}_special_${key}`;
|
||||
});
|
||||
|
||||
return keys.join(',');
|
||||
},
|
||||
animalItemsOwned (category) {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
|
||||
let own = true;
|
||||
this.animalItemKeys[category].forEach(key => {
|
||||
if (this.user.items.gear.owned[`${category}_special_${key}`] === undefined) own = false;
|
||||
});
|
||||
return own;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,341 @@
|
||||
<template lang="pug">
|
||||
#hair.section.customize-section
|
||||
sub-menu.text-center(:items="hairSubMenuItems", :activeSubPage="activeSubPage", @changeSubPage="changeSubPage($event)")
|
||||
|
||||
#hair-color(v-if='activeSubPage === "color"')
|
||||
customize-options(
|
||||
:items="freeHairColors",
|
||||
:currentValue="user.preferences.hair.color"
|
||||
)
|
||||
|
||||
div(v-if='editing && set.key !== "undefined"', v-for='set in seasonalHairColors')
|
||||
customize-options(
|
||||
:items='set.options',
|
||||
:currentValue="user.preferences.skin",
|
||||
:fullSet='!hideSet(set) && !userOwnsSet("hair", set.keys, "color")',
|
||||
@unlock='unlock(`hair.color.${set.keys.join(",hair.color.")}`)'
|
||||
)
|
||||
|
||||
#style(v-if='activeSubPage === "style"')
|
||||
div(v-for='set in styleSets')
|
||||
customize-options(
|
||||
:items='set.options',
|
||||
:fullSet='set.fullSet',
|
||||
@unlock='set.unlock()'
|
||||
)
|
||||
#bangs(v-if='activeSubPage === "bangs"')
|
||||
customize-options(
|
||||
:items='hairBangs',
|
||||
:currentValue="user.preferences.hair.bangs"
|
||||
)
|
||||
#facialhair(v-if='activeSubPage === "facialhair"')
|
||||
customize-options(
|
||||
v-if='editing',
|
||||
:items='mustacheList'
|
||||
)
|
||||
customize-options(
|
||||
v-if='editing',
|
||||
:items='beardList',
|
||||
:fullSet='isPurchaseAllNeeded("hair", ["baseHair5", "baseHair6"], ["mustache", "beard"])',
|
||||
@unlock='unlock(`hair.mustache.${baseHair5Keys.join(",hair.mustache.")},hair.beard.${baseHair6Keys.join(",hair.beard.")}`)'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import appearance from 'common/script/content/appearance';
|
||||
import {subPageMixin} from '../../mixins/subPage';
|
||||
import {userStateMixin} from '../../mixins/userState';
|
||||
import {avatarEditorUtilies} from '../../mixins/avatarEditUtilities';
|
||||
import subMenu from './sub-menu';
|
||||
import customizeOptions from './customize-options';
|
||||
import gem from 'assets/svg/gem.svg';
|
||||
import appearanceSets from 'common/script/content/appearance/sets';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
|
||||
const hairColorBySet = groupBy(appearance.hair.color, 'set.key');
|
||||
const freeHairColorKeys = hairColorBySet[undefined].map(s => s.key);
|
||||
|
||||
export default {
|
||||
props: [
|
||||
'editing',
|
||||
],
|
||||
components: {
|
||||
subMenu,
|
||||
customizeOptions,
|
||||
},
|
||||
mixins: [
|
||||
subPageMixin,
|
||||
userStateMixin,
|
||||
avatarEditorUtilies,
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
freeHairColorKeys,
|
||||
icons: Object.freeze({
|
||||
gem,
|
||||
}),
|
||||
baseHair1: [1, 3],
|
||||
baseHair2Keys: [2, 4, 5, 6, 7, 8],
|
||||
baseHair3Keys: [9, 10, 11, 12, 13, 14],
|
||||
baseHair4Keys: [15, 16, 17, 18, 19, 20],
|
||||
baseHair5Keys: [1, 2],
|
||||
baseHair6Keys: [1, 2, 3],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hairSubMenuItems () {
|
||||
const items = [
|
||||
{
|
||||
id: 'color',
|
||||
label: this.$t('color'),
|
||||
},
|
||||
{
|
||||
id: 'bangs',
|
||||
label: this.$t('bangs'),
|
||||
},
|
||||
{
|
||||
id: 'style',
|
||||
label: this.$t('style'),
|
||||
},
|
||||
];
|
||||
|
||||
if (this.editing) {
|
||||
items.push({
|
||||
id: 'facialhair',
|
||||
label: this.$t('facialhair'),
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
freeHairColors () {
|
||||
return freeHairColorKeys.map(s => this.mapKeysToFreeOption(s, 'hair', 'color'));
|
||||
},
|
||||
seasonalHairColors () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
|
||||
let seasonalHairColors = [];
|
||||
for (let key in hairColorBySet) {
|
||||
let set = hairColorBySet[key];
|
||||
|
||||
let keys = set.map(item => {
|
||||
return item.key;
|
||||
});
|
||||
|
||||
let options = keys.map(optionKey => {
|
||||
const option = this.mapKeysToOption(optionKey, 'hair', 'color', key);
|
||||
return option;
|
||||
});
|
||||
|
||||
let text = this.$t(key);
|
||||
if (appearanceSets[key] && appearanceSets[key].text) {
|
||||
text = appearanceSets[key].text();
|
||||
}
|
||||
|
||||
let compiledSet = {
|
||||
key,
|
||||
options,
|
||||
keys,
|
||||
text,
|
||||
};
|
||||
seasonalHairColors.push(compiledSet);
|
||||
}
|
||||
|
||||
return seasonalHairColors;
|
||||
},
|
||||
premiumHairColors () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.premiumHairColorKeys;
|
||||
let options = keys.map(key => {
|
||||
return this.mapKeysToOption(key, 'hair', 'color');
|
||||
});
|
||||
return options;
|
||||
},
|
||||
baseHair2 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.baseHair2Keys;
|
||||
let options = keys.map(key => {
|
||||
return this.mapKeysToOption(key, 'hair', 'base');
|
||||
});
|
||||
return options;
|
||||
},
|
||||
baseHair3 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.baseHair3Keys;
|
||||
let options = keys.map(key => {
|
||||
const option = this.mapKeysToOption(key, 'hair', 'base');
|
||||
return option;
|
||||
});
|
||||
return options;
|
||||
},
|
||||
baseHair4 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.baseHair4Keys;
|
||||
let options = keys.map(key => {
|
||||
return this.mapKeysToOption(key, 'hair', 'base');
|
||||
});
|
||||
return options;
|
||||
},
|
||||
baseHair5 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.baseHair5Keys;
|
||||
let options = keys.map(key => {
|
||||
return this.mapKeysToOption(key, 'hair', 'mustache');
|
||||
});
|
||||
return options;
|
||||
},
|
||||
baseHair6 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.baseHair6Keys;
|
||||
let options = keys.map(key => {
|
||||
return this.mapKeysToOption(key, 'hair', 'beard');
|
||||
});
|
||||
return options;
|
||||
},
|
||||
hairBangs () {
|
||||
const none = this.mapKeysToFreeOption(0, 'hair', 'bangs');
|
||||
none.none = true;
|
||||
|
||||
const options = [1, 2, 3, 4].map(s => this.mapKeysToFreeOption(s, 'hair', 'bangs'));
|
||||
|
||||
return [none, ...options];
|
||||
},
|
||||
mustacheList () {
|
||||
const noneOption = this.mapKeysToFreeOption(0, 'hair', 'mustache');
|
||||
noneOption.none = true;
|
||||
|
||||
return [noneOption, ...this.baseHair5];
|
||||
},
|
||||
beardList () {
|
||||
const noneOption = this.mapKeysToFreeOption(0, 'hair', 'beard');
|
||||
noneOption.none = true;
|
||||
|
||||
return [noneOption, ...this.baseHair6];
|
||||
},
|
||||
styleSets () {
|
||||
const sets = [];
|
||||
|
||||
const emptyHairBase = {
|
||||
...this.mapKeysToFreeOption(0, 'hair', 'base'),
|
||||
none: true,
|
||||
};
|
||||
|
||||
if (this.editing) {
|
||||
sets.push({
|
||||
fullSet: !this.userOwnsSet('hair', this.baseHair3Keys, 'base'),
|
||||
unlock: () => this.unlock(`hair.base.${this.baseHair3Keys.join(',hair.base.')}`),
|
||||
options: [
|
||||
emptyHairBase,
|
||||
...this.baseHair3,
|
||||
],
|
||||
});
|
||||
|
||||
sets.push({
|
||||
fullSet: !this.userOwnsSet('hair', this.baseHair4Keys, 'base'),
|
||||
unlock: () => this.unlock(`hair.base.${this.baseHair4Keys.join(',hair.base.')}`),
|
||||
options: [
|
||||
...this.baseHair4,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
sets.push({
|
||||
options: [
|
||||
emptyHairBase,
|
||||
...this.baseHair1.map(key => this.mapKeysToFreeOption(key, 'hair', 'base')),
|
||||
],
|
||||
});
|
||||
|
||||
if (this.editing) {
|
||||
sets.push({
|
||||
fullSet: !this.userOwnsSet('hair', this.baseHair2Keys, 'base'),
|
||||
unlock: () => this.unlock(`hair.base.${this.baseHair2Keys.join(',hair.base.')}`),
|
||||
options: [
|
||||
...this.baseHair2,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return sets;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.changeSubPage('color');
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Allows you to find out whether you need the "Purchase All" button or not. If there are more than 2 unpurchased items, returns true, otherwise returns false.
|
||||
* @param {string} category - The selected category.
|
||||
* @param {string[]} keySets - The items keySets.
|
||||
* @param {string[]} [types] - The items types (subcategories). Optional.
|
||||
* @returns {boolean} - Determines whether the "Purchase All" button is needed (true) or not (false).
|
||||
*/
|
||||
isPurchaseAllNeeded (category, keySets, types) {
|
||||
const purchasedItemsLengths = [];
|
||||
// If item types are specified, count them
|
||||
if (types && types.length > 0) {
|
||||
// Types can be undefined, so we must check them.
|
||||
types.forEach((type) => {
|
||||
if (this.user.purchased[category][type]) {
|
||||
purchasedItemsLengths
|
||||
.push(Object.keys(this.user.purchased[category][type]).length);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let purchasedItemsCounter = 0;
|
||||
|
||||
// If types are not specified, recursively
|
||||
// search for purchased items in the category
|
||||
const findPurchasedItems = (item) => {
|
||||
if (typeof item === 'object') {
|
||||
Object.values(item)
|
||||
.forEach((innerItem) => {
|
||||
if (typeof innerItem === 'boolean' && innerItem === true) {
|
||||
purchasedItemsCounter += 1;
|
||||
}
|
||||
return findPurchasedItems(innerItem);
|
||||
});
|
||||
}
|
||||
return purchasedItemsCounter;
|
||||
};
|
||||
|
||||
findPurchasedItems(this.user.purchased[category]);
|
||||
if (purchasedItemsCounter > 0) {
|
||||
purchasedItemsLengths.push(purchasedItemsCounter);
|
||||
}
|
||||
}
|
||||
|
||||
// We don't need to count the key sets (below)
|
||||
// if there are no purchased items at all.
|
||||
if (purchasedItemsLengths.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const allItemsLengths = [];
|
||||
// Key sets must be specify correctly.
|
||||
keySets.forEach((keySet) => {
|
||||
allItemsLengths.push(Object.keys(this[keySet]).length);
|
||||
});
|
||||
|
||||
// Simply sum all the length values and
|
||||
// write them into variables for the convenience.
|
||||
const allItems = allItemsLengths.reduce((acc, val) => acc + val);
|
||||
const purchasedItems = purchasedItemsLengths.reduce((acc, val) => acc + val);
|
||||
|
||||
const unpurchasedItems = allItems - purchasedItems;
|
||||
return unpurchasedItems > 2;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<template lang="pug">
|
||||
#skin.section.customize-section
|
||||
sub-menu.text-center(:items="skinSubMenuItems", :activeSubPage="activeSubPage", @changeSubPage="changeSubPage($event)")
|
||||
customize-options(
|
||||
:items="freeSkins",
|
||||
:currentValue="user.preferences.skin"
|
||||
)
|
||||
|
||||
div(v-if='editing && set.key !== "undefined"', v-for='set in seasonalSkins')
|
||||
customize-options(
|
||||
:items='set.options',
|
||||
:currentValue="user.preferences.skin",
|
||||
:fullSet='!hideSet(set) && !userOwnsSet("skin", set.keys)',
|
||||
@unlock='unlock(`skin.${set.keys.join(",skin.")}`)'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import appearance from 'common/script/content/appearance';
|
||||
import {subPageMixin} from '../../mixins/subPage';
|
||||
import {userStateMixin} from '../../mixins/userState';
|
||||
import {avatarEditorUtilies} from '../../mixins/avatarEditUtilities';
|
||||
import appearanceSets from 'common/script/content/appearance/sets';
|
||||
import subMenu from './sub-menu';
|
||||
import customizeOptions from './customize-options';
|
||||
import gem from 'assets/svg/gem.svg';
|
||||
|
||||
import groupBy from 'lodash/groupBy';
|
||||
|
||||
const skinsBySet = groupBy(appearance.skin, 'set.key');
|
||||
|
||||
const freeSkinKeys = skinsBySet[undefined].map(s => s.key);
|
||||
|
||||
// const specialSkinKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
|
||||
|
||||
|
||||
export default {
|
||||
props: [
|
||||
'editing',
|
||||
],
|
||||
components: {
|
||||
subMenu,
|
||||
customizeOptions,
|
||||
},
|
||||
mixins: [
|
||||
subPageMixin,
|
||||
userStateMixin,
|
||||
avatarEditorUtilies,
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
freeSkinKeys,
|
||||
icons: Object.freeze({
|
||||
gem,
|
||||
}),
|
||||
skinSubMenuItems: [
|
||||
{
|
||||
id: 'color',
|
||||
label: this.$t('color'),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
freeSkins () {
|
||||
return freeSkinKeys.map(s => this.mapKeysToFreeOption(s, 'skin'));
|
||||
},
|
||||
seasonalSkins () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
|
||||
let seasonalSkins = [];
|
||||
for (let setKey in skinsBySet) {
|
||||
let set = skinsBySet[setKey];
|
||||
|
||||
let keys = set.map(item => {
|
||||
return item.key;
|
||||
});
|
||||
|
||||
let options = keys.map(optionKey => {
|
||||
const option = this.mapKeysToOption(optionKey, 'skin', '', setKey);
|
||||
|
||||
return option;
|
||||
});
|
||||
|
||||
let text = this.$t(setKey);
|
||||
if (appearanceSets[setKey] && appearanceSets[setKey].text) {
|
||||
text = appearanceSets[setKey].text();
|
||||
}
|
||||
|
||||
let compiledSet = {
|
||||
key: setKey,
|
||||
options,
|
||||
keys,
|
||||
text,
|
||||
};
|
||||
seasonalSkins.push(compiledSet);
|
||||
}
|
||||
|
||||
return seasonalSkins;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.changeSubPage('color');
|
||||
},
|
||||
methods: {
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template lang="pug">
|
||||
.sub-menu.text-center
|
||||
.sub-menu-item(
|
||||
v-for="item of items",
|
||||
:key="item.id",
|
||||
@click='$emit("changeSubPage", item.id)',
|
||||
:class='{active: activeSubPage === item.id}'
|
||||
)
|
||||
strong(v-once) {{ item.label }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['items', 'activeSubPage'],
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.sub-menu {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
padding-top: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sub-menu:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sub-menu-item {
|
||||
padding: 6px 16px;
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #f9f9f9;
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-stretch: normal;
|
||||
line-height: 1.67;
|
||||
letter-spacing: normal;
|
||||
text-align: center;
|
||||
color: $gray-100;
|
||||
}
|
||||
|
||||
.sub-menu .sub-menu-item:hover, .sub-menu .sub-menu-item.active {
|
||||
color: $purple-200;
|
||||
border-bottom: 2px solid $purple-200;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
<template lang="pug">
|
||||
b-modal#banned-account(:title="$t('accountSuspendedTitle')", size='md', :hide-footer="true")
|
||||
.modal-body
|
||||
.row
|
||||
.col-12
|
||||
p(v-markdown='bannedMessage')
|
||||
.modal-footer
|
||||
.col-12.text-center
|
||||
button.btn.btn-primary(@click='close()') {{$t('close')}}
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import markdownDirective from 'client/directives/markdown';
|
||||
|
||||
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS.COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
computed: {
|
||||
bannedMessage () {
|
||||
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
|
||||
const parseSettings = JSON.parse(AUTH_SETTINGS);
|
||||
const userId = parseSettings ? parseSettings.auth.apiId : '';
|
||||
|
||||
return this.$t('accountSuspended', {
|
||||
userId,
|
||||
communityManagerEmail: COMMUNITY_MANAGER_EMAIL,
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'banned-account');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,34 @@
|
||||
<template lang="pug">
|
||||
.categories
|
||||
span.category-label.category-label-blue(v-if='owner')
|
||||
| {{ $t('owned') }}
|
||||
span.category-label.category-label-green(v-if='member')
|
||||
| {{ $t('joined') }}
|
||||
span.category-label(
|
||||
v-for='category in categories',
|
||||
:class="{'category-label-purple':isOfficial(category)}"
|
||||
)
|
||||
| {{ $t(category.name) }}
|
||||
slot
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
categories: {
|
||||
required: true,
|
||||
},
|
||||
owner: {
|
||||
default: false,
|
||||
},
|
||||
member: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isOfficial (category) {
|
||||
return category.name === 'habitica_official';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,401 @@
|
||||
<template lang="pug">
|
||||
.row
|
||||
challenge-modal(@updatedChallenge='updatedChallenge')
|
||||
leave-challenge-modal(:challengeId='challenge._id')
|
||||
close-challenge-modal(:members='members', :challengeId='challenge._id', :prize='challenge.prize')
|
||||
challenge-member-progress-modal(:challengeId='challenge._id')
|
||||
.col-12.col-md-8.standard-page
|
||||
.row
|
||||
.col-12.col-md-6
|
||||
h1(v-markdown='challenge.name')
|
||||
div
|
||||
span.mr-1.ml-0.d-block
|
||||
strong(v-once) {{ $t('createdBy') }}:
|
||||
user-link.mx-1(:user="challenge.leader")
|
||||
span.mr-1.ml-0.d-block(v-if="challenge.group && challenge.group.name !== 'Tavern'")
|
||||
strong(v-once) {{ $t(challenge.group.type) }}:
|
||||
group-link.mx-1(:group="challenge.group")
|
||||
// @TODO: make challenge.author a variable inside the createdBy string (helps with RTL languages)
|
||||
// @TODO: Implement in V2 strong.margin-left(v-once)
|
||||
.svg-icon.calendar-icon(v-html="icons.calendarIcon")
|
||||
| {{$t('endDate')}}
|
||||
// "endDate": "End Date: <% endDate %>",
|
||||
// span {{challenge.endDate}}
|
||||
.tags
|
||||
span.tag(v-for='tag in challenge.tags') {{tag}}
|
||||
.col-12.col-md-6.text-right
|
||||
.box(@click="showMemberModal()")
|
||||
.svg-icon.member-icon(v-html="icons.memberIcon")
|
||||
| {{challenge.memberCount}}
|
||||
.details(v-once) {{$t('participantsTitle')}}
|
||||
.box
|
||||
.svg-icon.gem-icon(v-html="icons.gemIcon")
|
||||
| {{challenge.prize}}
|
||||
.details(v-once) {{$t('prize')}}
|
||||
.row.challenge-actions
|
||||
.col-12.col-md-6
|
||||
strong.view-progress {{ $t('viewProgressOf') }}
|
||||
member-search-dropdown(:text="$t('selectParticipant')", :members='members', :challengeId='challengeId', @member-selected='openMemberProgressModal')
|
||||
.col-12.col-md-6.text-right
|
||||
span(v-if='isLeader || isAdmin')
|
||||
b-dropdown.create-dropdown(:text="$t('addTaskToChallenge')", :variant="'success'")
|
||||
b-dropdown-item(v-for="type in columns", :key="type", @click="createTask(type)")
|
||||
| {{$t(type)}}
|
||||
task-modal(
|
||||
:task="workingTask",
|
||||
:purpose="taskFormPurpose",
|
||||
@cancel="cancelTaskModal()",
|
||||
ref="taskModal",
|
||||
:challengeId="challengeId",
|
||||
@taskCreated='taskCreated',
|
||||
@taskEdited='taskEdited',
|
||||
@taskDestroyed='taskDestroyed'
|
||||
)
|
||||
.row
|
||||
task-column.col-12.col-sm-6(
|
||||
v-for="column in columns",
|
||||
:type="column",
|
||||
:key="column",
|
||||
:taskListOverride='tasksByType[column]',
|
||||
:showOptions="showOptions",
|
||||
@editTask="editTask",
|
||||
@taskDestroyed="taskDestroyed",
|
||||
v-if='tasksByType[column].length > 0')
|
||||
.col-12.col-md-4.sidebar.standard-page
|
||||
.button-container(v-if='canJoin')
|
||||
button.btn.btn-success(v-once, @click='joinChallenge()') {{$t('joinChallenge')}}
|
||||
.button-container(v-if='isLeader || isAdmin')
|
||||
button.btn.btn-primary(v-once, @click='edit()') {{$t('editChallenge')}}
|
||||
.button-container(v-if='isLeader || isAdmin')
|
||||
button.btn.btn-primary(v-once, @click='cloneChallenge()') {{$t('clone')}}
|
||||
.button-container(v-if='isLeader || isAdmin')
|
||||
button.btn.btn-primary(v-once, @click='exportChallengeCsv()') {{$t('exportChallengeCsv')}}
|
||||
.button-container(v-if='isLeader || isAdmin')
|
||||
button.btn.btn-danger(v-once, @click='closeChallenge()') {{$t('endChallenge')}}
|
||||
div
|
||||
sidebar-section(:title="$t('challengeSummary')")
|
||||
p(v-markdown='challenge.summary')
|
||||
sidebar-section(:title="$t('challengeDescription')")
|
||||
p(v-markdown='challenge.description')
|
||||
.text-center(v-if='isMember')
|
||||
button.btn.btn-danger(v-once, @click='leaveChallenge()') {{$t('leaveChallenge')}}
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
h1 {
|
||||
color: $purple-200;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.margin-left {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin-bottom: 1em;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-icon {
|
||||
width: 12px;
|
||||
display: inline-block;
|
||||
margin-right: .2em;
|
||||
}
|
||||
|
||||
.tags {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.tag {
|
||||
border-radius: 30px;
|
||||
background-color: $gray-600;
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: $gray-600;
|
||||
}
|
||||
|
||||
.box {
|
||||
display: inline-block;
|
||||
padding: 1em;
|
||||
border-radius: 2px;
|
||||
background-color: $white;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
margin-left: 1em;
|
||||
width: 120px;
|
||||
height: 76px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
vertical-align: bottom;
|
||||
|
||||
.svg-icon {
|
||||
width: 30px;
|
||||
display: inline-block;
|
||||
margin-right: .2em;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.details {
|
||||
font-size: 12px;
|
||||
margin-top: 0.4em;
|
||||
color: $gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
.description-section {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.challenge-actions {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 2em;
|
||||
|
||||
.view-progress {
|
||||
margin-right: .5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const TASK_KEYS_TO_REMOVE = ['_id', 'completed', 'date', 'dateCompleted', 'history', 'id', 'streak', 'createdAt', 'challenge'];
|
||||
|
||||
import Vue from 'vue';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import omit from 'lodash/omit';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { mapState } from 'client/libs/store';
|
||||
import memberSearchDropdown from 'client/components/members/memberSearchDropdown';
|
||||
import closeChallengeModal from './closeChallengeModal';
|
||||
import Column from '../tasks/column';
|
||||
import TaskModal from '../tasks/taskModal';
|
||||
import markdownDirective from 'client/directives/markdown';
|
||||
import challengeModal from './challengeModal';
|
||||
import challengeMemberProgressModal from './challengeMemberProgressModal';
|
||||
import challengeMemberSearchMixin from 'client/mixins/challengeMemberSearch';
|
||||
import leaveChallengeModal from './leaveChallengeModal';
|
||||
import sidebarSection from '../sidebarSection';
|
||||
import userLink from '../userLink';
|
||||
import groupLink from '../groupLink';
|
||||
import taskDefaults from 'common/script/libs/taskDefaults';
|
||||
|
||||
import gemIcon from 'assets/svg/gem.svg';
|
||||
import memberIcon from 'assets/svg/member-icon.svg';
|
||||
import calendarIcon from 'assets/svg/calendar.svg';
|
||||
|
||||
export default {
|
||||
props: ['challengeId'],
|
||||
mixins: [challengeMemberSearchMixin],
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
components: {
|
||||
closeChallengeModal,
|
||||
leaveChallengeModal,
|
||||
challengeModal,
|
||||
challengeMemberProgressModal,
|
||||
memberSearchDropdown,
|
||||
sidebarSection,
|
||||
TaskColumn: Column,
|
||||
TaskModal,
|
||||
userLink,
|
||||
groupLink,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
searchId: '',
|
||||
columns: ['habit', 'daily', 'todo', 'reward'],
|
||||
icons: Object.freeze({
|
||||
gemIcon,
|
||||
memberIcon,
|
||||
calendarIcon,
|
||||
}),
|
||||
challenge: {},
|
||||
members: [],
|
||||
tasksByType: {
|
||||
habit: [],
|
||||
daily: [],
|
||||
todo: [],
|
||||
reward: [],
|
||||
},
|
||||
editingTask: {},
|
||||
creatingTask: {},
|
||||
workingTask: {},
|
||||
taskFormPurpose: 'create',
|
||||
searchTerm: '',
|
||||
memberResults: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
isMember () {
|
||||
return this.user.challenges.indexOf(this.challenge._id) !== -1;
|
||||
},
|
||||
isLeader () {
|
||||
if (!this.challenge.leader) return false;
|
||||
return this.user._id === this.challenge.leader._id;
|
||||
},
|
||||
isAdmin () {
|
||||
return Boolean(this.user.contributor.admin);
|
||||
},
|
||||
canJoin () {
|
||||
return !this.isMember;
|
||||
},
|
||||
showOptions () {
|
||||
return this.isLeader;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
if (!this.searchId) this.searchId = this.challengeId;
|
||||
if (!this.challenge._id) this.loadChallenge();
|
||||
},
|
||||
async beforeRouteUpdate (to, from, next) {
|
||||
this.searchId = to.params.challengeId;
|
||||
await this.loadChallenge();
|
||||
next();
|
||||
},
|
||||
methods: {
|
||||
cleanUpTask (task) {
|
||||
let cleansedTask = omit(task, TASK_KEYS_TO_REMOVE);
|
||||
|
||||
// Copy checklists but reset to uncomplete and assign new id
|
||||
if (!cleansedTask.checklist) cleansedTask.checklist = [];
|
||||
cleansedTask.checklist.forEach((item) => {
|
||||
item.completed = false;
|
||||
item.id = uuid();
|
||||
});
|
||||
|
||||
if (cleansedTask.type !== 'reward') {
|
||||
delete cleansedTask.value;
|
||||
}
|
||||
|
||||
return cleansedTask;
|
||||
},
|
||||
async loadChallenge () {
|
||||
this.challenge = await this.$store.dispatch('challenges:getChallenge', {challengeId: this.searchId});
|
||||
this.members = await this.loadMembers({ challengeId: this.searchId, includeAllPublicFields: true });
|
||||
let tasks = await this.$store.dispatch('tasks:getChallengeTasks', {challengeId: this.searchId});
|
||||
this.tasksByType = {
|
||||
habit: [],
|
||||
daily: [],
|
||||
todo: [],
|
||||
reward: [],
|
||||
};
|
||||
tasks.forEach((task) => {
|
||||
this.tasksByType[task.type].push(task);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Method for loading members of a group, with optional parameters for
|
||||
* modifying requests.
|
||||
*
|
||||
* @param {Object} payload Used for modifying requests for members
|
||||
*/
|
||||
loadMembers (payload = null) {
|
||||
// Remove unnecessary data
|
||||
if (payload && payload.groupId) {
|
||||
delete payload.groupId;
|
||||
}
|
||||
return this.$store.dispatch('members:getChallengeMembers', payload);
|
||||
},
|
||||
|
||||
editTask (task) {
|
||||
this.taskFormPurpose = 'edit';
|
||||
this.editingTask = cloneDeep(task);
|
||||
this.workingTask = this.editingTask;
|
||||
// Necessary otherwise the first time the modal is not rendered
|
||||
Vue.nextTick(() => {
|
||||
this.$root.$emit('bv::show::modal', 'task-modal');
|
||||
});
|
||||
},
|
||||
createTask (type) {
|
||||
this.taskFormPurpose = 'create';
|
||||
this.creatingTask = taskDefaults({type, text: ''}, this.user);
|
||||
this.workingTask = this.creatingTask;
|
||||
// Necessary otherwise the first time the modal is not rendered
|
||||
Vue.nextTick(() => {
|
||||
this.$root.$emit('bv::show::modal', 'task-modal');
|
||||
});
|
||||
},
|
||||
cancelTaskModal () {
|
||||
this.editingTask = null;
|
||||
this.creatingTask = null;
|
||||
},
|
||||
taskCreated (task) {
|
||||
this.tasksByType[task.type].push(task);
|
||||
},
|
||||
taskEdited (task) {
|
||||
let index = findIndex(this.tasksByType[task.type], (taskItem) => {
|
||||
return taskItem._id === task._id;
|
||||
});
|
||||
this.tasksByType[task.type].splice(index, 1, task);
|
||||
},
|
||||
taskDestroyed (task) {
|
||||
let index = findIndex(this.tasksByType[task.type], (taskItem) => {
|
||||
return taskItem._id === task._id;
|
||||
});
|
||||
this.tasksByType[task.type].splice(index, 1);
|
||||
},
|
||||
showMemberModal () {
|
||||
this.$root.$emit('habitica:show-member-modal', {
|
||||
challengeId: this.challenge._id,
|
||||
groupId: 'challenge', // @TODO: change these terrible settings
|
||||
group: this.challenge.group,
|
||||
memberCount: this.challenge.memberCount,
|
||||
viewingMembers: this.members,
|
||||
fetchMoreMembers: this.loadMembers,
|
||||
});
|
||||
},
|
||||
async joinChallenge () {
|
||||
this.user.challenges.push(this.searchId);
|
||||
await this.$store.dispatch('challenges:joinChallenge', {challengeId: this.searchId});
|
||||
await this.$store.dispatch('tasks:fetchUserTasks', {forceLoad: true});
|
||||
},
|
||||
async leaveChallenge () {
|
||||
this.$root.$emit('bv::show::modal', 'leave-challenge-modal');
|
||||
},
|
||||
closeChallenge () {
|
||||
this.$root.$emit('bv::show::modal', 'close-challenge-modal');
|
||||
},
|
||||
edit () {
|
||||
this.$root.$emit('habitica:update-challenge', {
|
||||
challenge: this.challenge,
|
||||
});
|
||||
},
|
||||
// @TODO: view members
|
||||
updatedChallenge (eventData) {
|
||||
Object.assign(this.challenge, eventData.challenge);
|
||||
},
|
||||
openMemberProgressModal (member) {
|
||||
this.$root.$emit('habitica:challenge:member-progress', {
|
||||
progressMemberId: member._id,
|
||||
isLeader: this.isLeader,
|
||||
isAdmin: this.isAdmin,
|
||||
});
|
||||
},
|
||||
async exportChallengeCsv () {
|
||||
// let response = await this.$store.dispatch('challenges:exportChallengeCsv', {
|
||||
// challengeId: this.searchId,
|
||||
// });
|
||||
window.location = `/api/v4/challenges/${this.searchId}/export/csv`;
|
||||
},
|
||||
cloneChallenge () {
|
||||
this.$root.$emit('habitica:clone-challenge', {
|
||||
challenge: this.challenge,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,391 @@
|
||||
<template lang="pug">
|
||||
.challenge
|
||||
.challenge-prize
|
||||
.number
|
||||
span.svg-icon(v-html="icons.gemIcon")
|
||||
span.value {{challenge.prize}}
|
||||
.label {{ $t('prize') }}
|
||||
.challenge-header
|
||||
router-link(
|
||||
:to="{ name: 'challenge', params: { challengeId: challenge._id } }"
|
||||
)
|
||||
h3.challenge-title(v-markdown='challenge.name')
|
||||
.meta-info
|
||||
.member-count
|
||||
.svg-icon.user-icon(v-html="icons.memberIcon")
|
||||
span.count-label {{challenge.memberCount}}
|
||||
.divider
|
||||
.official(v-if="isOfficial")
|
||||
.svg-icon.user-icon(v-html="icons.officialIcon")
|
||||
.owner(v-if="fullLayout")
|
||||
.owner-item
|
||||
strong {{ $t('createdBy') }}:
|
||||
user-link.mx-1(:user="challenge.leader")
|
||||
.owner-item(v-if="challenge.group && !isTavern(challenge.group)")
|
||||
strong {{ $t(challenge.group.type) }}:
|
||||
group-link.mx-1(:group="challenge.group")
|
||||
category-tags.challenge-categories(
|
||||
:categories="challenge.categories",
|
||||
:owner="isOwner",
|
||||
:member="isMember",
|
||||
)
|
||||
.challenge-description(v-markdown='challenge.summary')
|
||||
.well-wrapper(v-if="fullLayout")
|
||||
.well
|
||||
div(v-for="task in tasksData", :class="{'muted': task.value === 0}")
|
||||
.number
|
||||
.svg-icon(v-html="task.icon", :class="task.label + '-icon'")
|
||||
span.value {{ task.value }}
|
||||
.label {{$t(task.label)}}
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
// Have to use this, because v-markdown creates p element in h3. Scoping doesn't work with it.
|
||||
.challenge-title > p {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
max-height: 3em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-stretch: condensed;
|
||||
line-height: 1.4;
|
||||
letter-spacing: normal;
|
||||
color: $gray-10;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.challenge {
|
||||
background-color: $white;
|
||||
box-shadow: 0 2px 2px 0 rgba($black, 0.15), 0 1px 4px 0 rgba($black, 0.1);
|
||||
margin-bottom: 1em;
|
||||
border-radius: 4px;
|
||||
padding-bottom: .5em;
|
||||
|
||||
.number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.svg-icon {
|
||||
margin-top: -.2em;
|
||||
}
|
||||
|
||||
.value {
|
||||
margin-left: .5em;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
.official {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.challenge-prize {
|
||||
text-align: center;
|
||||
color: $gems-color;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
padding: 18px 24px;
|
||||
margin-left: 1em;
|
||||
background: #24cc8f19;
|
||||
border-bottom-left-radius: 4px;
|
||||
width: 107px;
|
||||
height: 80px;
|
||||
|
||||
.svg-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.value {
|
||||
margin-left: 8px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-stretch: normal;
|
||||
line-height: 1.4;
|
||||
letter-spacing: normal;
|
||||
color: #1ca372;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-stretch: normal;
|
||||
line-height: 1.33;
|
||||
letter-spacing: normal;
|
||||
text-align: center;
|
||||
color: #1ca372;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background-color: $gray-600;
|
||||
margin-right: 12px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.challenge-header {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.well-wrapper {
|
||||
padding: 16px 20px 20px;
|
||||
}
|
||||
|
||||
.challenge-header {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
height: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.count-label {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-stretch: normal;
|
||||
line-height: 1.71;
|
||||
letter-spacing: normal;
|
||||
color: $gray-50;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.meta-info, .owner, .member-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.meta-item, .owner-item {
|
||||
display: inline-flex;
|
||||
color: $gray-200;
|
||||
align-items: center;
|
||||
margin-right: 1em;
|
||||
white-space: nowrap;
|
||||
|
||||
.svg-icon {
|
||||
color: $gray-300;
|
||||
width: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.challenge-categories {
|
||||
clear: right;
|
||||
display: flex;
|
||||
padding: 0 20px 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.user-icon {
|
||||
width: 20px !important;
|
||||
}
|
||||
|
||||
.habit-icon {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.todo-icon {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.daily-icon {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.reward-icon {
|
||||
width: 26px;
|
||||
}
|
||||
|
||||
.challenge-description {
|
||||
margin: 0 20px 0;
|
||||
word-break: break-word;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-stretch: normal;
|
||||
line-height: 1.71;
|
||||
letter-spacing: normal;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.well {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
background-color: $gray-700;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
border-radius: .25em;
|
||||
|
||||
> div {
|
||||
.value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-stretch: normal;
|
||||
line-height: 1.4;
|
||||
letter-spacing: normal;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-stretch: normal;
|
||||
line-height: 1.33;
|
||||
letter-spacing: normal;
|
||||
text-align: center;
|
||||
color: $gray-100;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
object-fit: contain;
|
||||
color: $gray-100;
|
||||
}
|
||||
}
|
||||
|
||||
> div.muted {
|
||||
.value {
|
||||
opacity: 0.5;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-stretch: normal;
|
||||
line-height: 1.4;
|
||||
letter-spacing: normal;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.label {
|
||||
opacity: 0.5;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-stretch: normal;
|
||||
line-height: 1.33;
|
||||
letter-spacing: normal;
|
||||
text-align: center;
|
||||
color: $gray-100;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
object-fit: contain;
|
||||
opacity: 0.5;
|
||||
color: $gray-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { TAVERN_ID } from '../../../common/script/constants';
|
||||
import userLink from '../userLink';
|
||||
import groupLink from '../groupLink';
|
||||
import categoryTags from '../categories/categoryTags';
|
||||
import markdownDirective from 'client/directives/markdown';
|
||||
import {mapState} from 'client/libs/store';
|
||||
|
||||
import gemIcon from 'assets/svg/gem.svg';
|
||||
import memberIcon from 'assets/svg/member-icon.svg';
|
||||
import calendarIcon from 'assets/svg/calendar.svg';
|
||||
import habitIcon from 'assets/svg/habit.svg';
|
||||
import todoIcon from 'assets/svg/todo.svg';
|
||||
import dailyIcon from 'assets/svg/daily.svg';
|
||||
import rewardIcon from 'assets/svg/reward.svg';
|
||||
import officialIcon from 'assets/svg/official.svg';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
challenge: {
|
||||
required: true,
|
||||
},
|
||||
fullLayout: {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
userLink,
|
||||
groupLink,
|
||||
categoryTags,
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
gemIcon,
|
||||
memberIcon,
|
||||
calendarIcon,
|
||||
officialIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
isOwner () {
|
||||
return this.challenge.leader && this.challenge.leader._id === this.user._id;
|
||||
},
|
||||
isMember () {
|
||||
return this.user.challenges.indexOf(this.challenge._id) !== -1;
|
||||
},
|
||||
isOfficial () {
|
||||
return this.challenge.official || this.challenge.categories.map(category => category.slug).includes('habitica_official');
|
||||
},
|
||||
tasksData () {
|
||||
return [
|
||||
{
|
||||
icon: habitIcon,
|
||||
label: 'habit',
|
||||
value: this.challenge.tasksOrder.habits.length,
|
||||
},
|
||||
{
|
||||
icon: dailyIcon,
|
||||
label: 'daily',
|
||||
value: this.challenge.tasksOrder.dailys.length,
|
||||
},
|
||||
{
|
||||
icon: todoIcon,
|
||||
label: 'todo',
|
||||
value: this.challenge.tasksOrder.todos.length,
|
||||
},
|
||||
{
|
||||
icon: rewardIcon,
|
||||
label: 'reward',
|
||||
value: this.challenge.tasksOrder.rewards.length,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isTavern (group) {
|
||||
return group._id === TAVERN_ID;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template lang="pug">
|
||||
b-modal#challenge-member-modal(title="User Progress", size='lg')
|
||||
.row.award-row(v-if='isLeader || isAdmin')
|
||||
.col-12.text-center
|
||||
button.btn.btn-primary(v-once, @click='closeChallenge()') {{ $t('awardWinners') }}
|
||||
.row
|
||||
task-column.col-6(
|
||||
v-for="column in columns",
|
||||
:type="column",
|
||||
:key="column",
|
||||
:taskListOverride='tasksByType[column]')
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.award-row {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import Column from '../tasks/column';
|
||||
|
||||
export default {
|
||||
props: ['challengeId'],
|
||||
components: {
|
||||
TaskColumn: Column,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
columns: ['habit', 'daily', 'todo', 'reward'],
|
||||
tasksByType: {
|
||||
habit: [],
|
||||
daily: [],
|
||||
todo: [],
|
||||
reward: [],
|
||||
},
|
||||
memberId: '',
|
||||
isLeader: false,
|
||||
isAdmin: false,
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('habitica:challenge:member-progress', (data) => {
|
||||
if (!data.progressMemberId) return;
|
||||
this.memberId = data.progressMemberId;
|
||||
this.isLeader = data.isLeader;
|
||||
this.isAdmin = data.isAdmin;
|
||||
this.$root.$emit('bv::show::modal', 'challenge-member-modal');
|
||||
});
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('habitica:challenge:member-progress');
|
||||
},
|
||||
watch: {
|
||||
async memberId (id) {
|
||||
if (!id) return;
|
||||
this.tasksByType = {
|
||||
habit: [],
|
||||
daily: [],
|
||||
todo: [],
|
||||
reward: [],
|
||||
};
|
||||
|
||||
let response = await axios.get(`/api/v4/challenges/${this.challengeId}/members/${this.memberId}`);
|
||||
let tasks = response.data.data.tasks;
|
||||
tasks.forEach((task) => {
|
||||
this.tasksByType[task.type].push(task);
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async closeChallenge () {
|
||||
this.challenge = await this.$store.dispatch('challenges:selectChallengeWinner', {
|
||||
challengeId: this.challengeId,
|
||||
winnerId: this.memberId,
|
||||
});
|
||||
this.$router.push('/challenges/myChallenges');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,478 @@
|
||||
<template lang="pug">
|
||||
b-modal#challenge-modal(:title="title", size='lg', @shown="shown")
|
||||
.form
|
||||
.form-group
|
||||
label
|
||||
strong(v-once) {{$t('name')}} *
|
||||
b-form-input(type="text", :placeholder="$t('challengeNamePlaceholder')", v-model="workingChallenge.name")
|
||||
.form-group
|
||||
label
|
||||
strong(v-once) {{$t('shortName')}} *
|
||||
b-form-input(type="text", :placeholder="$t('shortNamePlaceholder')", v-model="workingChallenge.shortName")
|
||||
.form-group
|
||||
label
|
||||
strong(v-once) {{$t('challengeSummary')}} *
|
||||
div.summary-count {{ $t('charactersRemaining', {characters: charactersRemaining}) }}
|
||||
textarea.summary-textarea.form-control(:placeholder="$t('challengeSummaryPlaceholder')", v-model="workingChallenge.summary")
|
||||
.form-group
|
||||
label
|
||||
strong(v-once) {{$t('challengeDescription')}} *
|
||||
a.float-right(v-markdown='$t("markdownFormattingHelp")')
|
||||
textarea.description-textarea.form-control(:placeholder="$t('challengeDescriptionPlaceholder')", v-model="workingChallenge.description")
|
||||
.form-group(v-if='creating')
|
||||
label
|
||||
strong(v-once) {{$t('challengeGuild')}} *
|
||||
select.form-control(v-model='workingChallenge.group')
|
||||
option(v-for='group in groups', :value='group._id') {{group.name}}
|
||||
.form-group(v-if='workingChallenge.categories')
|
||||
label
|
||||
strong(v-once) {{$t('categories')}} *
|
||||
div.category-wrap(@click.prevent="toggleCategorySelect")
|
||||
span.category-select(v-if='workingChallenge.categories.length === 0') {{$t('none')}}
|
||||
.category-label(v-for='category in workingChallenge.categories') {{$t(categoriesHashByKey[category])}}
|
||||
.category-box(v-if="showCategorySelect && creating")
|
||||
.form-check(
|
||||
v-for="group in categoryOptions",
|
||||
:key="group.key",
|
||||
v-if='group.key !== "habitica_official" || user.contributor.admin'
|
||||
)
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input(type="checkbox",
|
||||
:value="group.key",
|
||||
:id="`challenge-modal-cat-${group.key}`",
|
||||
v-model="workingChallenge.categories")
|
||||
label.custom-control-label(v-once, :for="`challenge-modal-cat-${group.key}`") {{ $t(group.label) }}
|
||||
button.btn.btn-primary(@click.prevent="toggleCategorySelect") {{$t('close')}}
|
||||
// @TODO: Implement in V2 .form-group
|
||||
label
|
||||
strong(v-once) {{$t('endDate')}}
|
||||
b-form-input.end-date-input
|
||||
.form-group(v-if='creating')
|
||||
label
|
||||
strong(v-once) {{$t('prize')}}
|
||||
input(type='number', :min='minPrize', :max='maxPrize', v-model="workingChallenge.prize")
|
||||
.row.footer-wrap
|
||||
.col-12.text-center.submit-button-wrapper
|
||||
.alert.alert-warning(v-if='insufficientGemsForTavernChallenge') You do not have enough gems to create a Tavern challenge
|
||||
// @TODO if buy gems button is added, add analytics tracking to it
|
||||
// see https://github.com/HabitRPG/habitica/blob/develop/website/views/options/social/challenges.jade#L134
|
||||
button.btn.btn-primary(v-if='creating && !cloning', @click='createChallenge()', :disabled='loading') {{$t('createChallengeAddTasks')}}
|
||||
button.btn.btn-primary(v-once, v-if='cloning', @click='createChallenge()', :disabled='loading') {{$t('createChallengeCloneTasks')}}
|
||||
button.btn.btn-primary(v-once, v-if='!creating && !cloning', @click='updateChallenge()') {{$t('updateChallenge')}}
|
||||
.col-12.text-center
|
||||
p(v-once) {{$t('challengeMinimum')}}
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
#challenge-modal {
|
||||
h5 {
|
||||
color: $purple-200;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.summary-count {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
margin-top: 1em;
|
||||
color: $gray-200;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.summary-textarea {
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
.description-textarea {
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.end-date-input {
|
||||
width: 150px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.submit-button-wrapper {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
.footer-wrap {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.category-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.category-box {
|
||||
top: 20em !important;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import clone from 'lodash/clone';
|
||||
|
||||
import markdownDirective from 'client/directives/markdown';
|
||||
|
||||
import { TAVERN_ID, MIN_SHORTNAME_SIZE_FOR_CHALLENGES, MAX_SUMMARY_SIZE_FOR_CHALLENGES } from '../../../common/script/constants';
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
props: ['groupId'],
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
data () {
|
||||
let categoryOptions = [
|
||||
{
|
||||
label: 'habitica_official',
|
||||
key: 'habitica_official',
|
||||
},
|
||||
{
|
||||
label: 'academics',
|
||||
key: 'academics',
|
||||
},
|
||||
{
|
||||
label: 'advocacy_causes',
|
||||
key: 'advocacy_causes',
|
||||
},
|
||||
{
|
||||
label: 'creativity',
|
||||
key: 'creativity',
|
||||
},
|
||||
{
|
||||
label: 'entertainment',
|
||||
key: 'entertainment',
|
||||
},
|
||||
{
|
||||
label: 'finance',
|
||||
key: 'finance',
|
||||
},
|
||||
{
|
||||
label: 'health_fitness',
|
||||
key: 'health_fitness',
|
||||
},
|
||||
{
|
||||
label: 'hobbies_occupations',
|
||||
key: 'hobbies_occupations',
|
||||
},
|
||||
{
|
||||
label: 'location_based',
|
||||
key: 'location_based',
|
||||
},
|
||||
{
|
||||
label: 'mental_health',
|
||||
key: 'mental_health',
|
||||
},
|
||||
{
|
||||
label: 'getting_organized',
|
||||
key: 'getting_organized',
|
||||
},
|
||||
{
|
||||
label: 'self_improvement',
|
||||
key: 'self_improvement',
|
||||
},
|
||||
{
|
||||
label: 'spirituality',
|
||||
key: 'spirituality',
|
||||
},
|
||||
{
|
||||
label: 'time_management',
|
||||
key: 'time_management',
|
||||
},
|
||||
];
|
||||
let hashedCategories = {};
|
||||
categoryOptions.forEach((category) => {
|
||||
hashedCategories[category.key] = category.label;
|
||||
});
|
||||
let categoriesHashByKey = hashedCategories;
|
||||
|
||||
return {
|
||||
workingChallenge: {
|
||||
name: '',
|
||||
summary: '',
|
||||
description: '',
|
||||
categories: [],
|
||||
group: '',
|
||||
dailys: [],
|
||||
habits: [],
|
||||
leader: '',
|
||||
members: [],
|
||||
official: false,
|
||||
prize: 1,
|
||||
rewards: [],
|
||||
shortName: '',
|
||||
todos: [],
|
||||
},
|
||||
cloning: false,
|
||||
cloningChallengeId: '',
|
||||
showCategorySelect: false,
|
||||
categoryOptions,
|
||||
categoriesHashByKey,
|
||||
loading: false,
|
||||
groups: [],
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('habitica:clone-challenge', (data) => {
|
||||
if (!data.challenge) return;
|
||||
this.cloning = true;
|
||||
this.cloningChallengeId = data.challenge._id;
|
||||
this.$store.state.challengeOptions.workingChallenge = Object.assign({}, this.$store.state.challengeOptions.workingChallenge, data.challenge);
|
||||
this.$root.$emit('bv::show::modal', 'challenge-modal');
|
||||
});
|
||||
this.$root.$on('habitica:update-challenge', (data) => {
|
||||
if (!data.challenge) return;
|
||||
this.cloning = false;
|
||||
this.$store.state.challengeOptions.workingChallenge = Object.assign({}, this.$store.state.challengeOptions.workingChallenge, data.challenge);
|
||||
this.$root.$emit('bv::show::modal', 'challenge-modal');
|
||||
});
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('habitica:clone-challenge');
|
||||
},
|
||||
watch: {
|
||||
user () {
|
||||
if (!this.challenge) this.workingChallenge.leader = this.user._id;
|
||||
},
|
||||
challenge () {
|
||||
this.setUpWorkingChallenge();
|
||||
},
|
||||
cloning () {
|
||||
this.setUpWorkingChallenge();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
creating () {
|
||||
return !this.workingChallenge.id;
|
||||
},
|
||||
title () {
|
||||
if (this.creating) {
|
||||
return this.$t('createChallenge');
|
||||
}
|
||||
return this.$t('editingChallenge');
|
||||
},
|
||||
charactersRemaining () {
|
||||
let currentLength = this.workingChallenge.summary ? this.workingChallenge.summary.length : 0;
|
||||
return MAX_SUMMARY_SIZE_FOR_CHALLENGES - currentLength;
|
||||
},
|
||||
maxPrize () {
|
||||
let userBalance = this.user.balance || 0;
|
||||
userBalance = userBalance * 4;
|
||||
|
||||
let groupBalance = 0;
|
||||
let group;
|
||||
this.groups.forEach(item => {
|
||||
if (item._id === this.workingChallenge.group) {
|
||||
group = item;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (group && group.balance && group.leader === this.user._id) {
|
||||
groupBalance = group.balance * 4;
|
||||
}
|
||||
|
||||
return userBalance + groupBalance;
|
||||
},
|
||||
minPrize () {
|
||||
if (this.workingChallenge.group === TAVERN_ID) return 1;
|
||||
return 0;
|
||||
},
|
||||
insufficientGemsForTavernChallenge () {
|
||||
let balance = this.user.balance || 0;
|
||||
let isForTavern = this.workingChallenge.group === TAVERN_ID;
|
||||
|
||||
if (isForTavern) {
|
||||
return balance <= 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
challenge () {
|
||||
return this.$store.state.challengeOptions.workingChallenge;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async shown () {
|
||||
this.groups = await this.$store.dispatch('guilds:getMyGuilds');
|
||||
|
||||
if (this.user.party && this.user.party._id) {
|
||||
await this.$store.dispatch('party:getParty');
|
||||
const party = this.$store.state.party.data;
|
||||
if (party._id) {
|
||||
this.groups.push({
|
||||
name: party.name,
|
||||
_id: party._id,
|
||||
privacy: 'private',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.groups.push({
|
||||
name: this.$t('publicChallengesTitle'),
|
||||
_id: TAVERN_ID,
|
||||
});
|
||||
|
||||
this.setUpWorkingChallenge();
|
||||
},
|
||||
setUpWorkingChallenge () {
|
||||
this.resetWorkingChallenge();
|
||||
|
||||
if (!this.challenge) return;
|
||||
|
||||
this.workingChallenge = Object.assign({}, this.workingChallenge, this.challenge);
|
||||
// @TODO: Should we use a separate field? I think the API expects `group` but it is confusing
|
||||
this.workingChallenge.group = this.workingChallenge.group._id;
|
||||
this.workingChallenge.categories = [];
|
||||
|
||||
if (this.challenge.categories) {
|
||||
this.challenge.categories.forEach(category => {
|
||||
this.workingChallenge.categories.push(category.slug);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.cloning) {
|
||||
this.$delete(this.workingChallenge, '_id');
|
||||
this.$delete(this.workingChallenge, 'id');
|
||||
}
|
||||
},
|
||||
resetWorkingChallenge () {
|
||||
this.workingChallenge = {
|
||||
name: '',
|
||||
summary: '',
|
||||
description: '',
|
||||
categories: [],
|
||||
group: '',
|
||||
dailys: [],
|
||||
habits: [],
|
||||
leader: '',
|
||||
members: [],
|
||||
official: false,
|
||||
prize: 1,
|
||||
rewards: [],
|
||||
shortName: '',
|
||||
todos: [],
|
||||
};
|
||||
|
||||
this.$store.state.workingChallenge = {};
|
||||
},
|
||||
async createChallenge () {
|
||||
this.loading = true;
|
||||
// @TODO: improve error handling, add it to updateChallenge, make errors translatable. Suggestion: `<% fieldName %> is required` where possible, where `fieldName` is inserted as the translatable string that's used for the field header.
|
||||
let errors = [];
|
||||
|
||||
if (!this.workingChallenge.name) errors.push(this.$t('nameRequired'));
|
||||
if (this.workingChallenge.shortName.length < MIN_SHORTNAME_SIZE_FOR_CHALLENGES) errors.push(this.$t('tagTooShort'));
|
||||
if (!this.workingChallenge.summary) errors.push(this.$t('summaryRequired'));
|
||||
if (this.workingChallenge.summary.length > MAX_SUMMARY_SIZE_FOR_CHALLENGES) errors.push(this.$t('summaryTooLong'));
|
||||
if (!this.workingChallenge.description) errors.push(this.$t('descriptionRequired'));
|
||||
if (!this.workingChallenge.group) errors.push(this.$t('locationRequired'));
|
||||
if (!this.workingChallenge.categories || this.workingChallenge.categories.length === 0) errors.push(this.$t('categoiresRequired'));
|
||||
|
||||
if (errors.length > 0) {
|
||||
alert(errors.join('\n'));
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.workingChallenge.timestamp = new Date().getTime();
|
||||
let categoryKeys = this.workingChallenge.categories;
|
||||
let serverCategories = [];
|
||||
categoryKeys.forEach(key => {
|
||||
let catName = this.categoriesHashByKey[key];
|
||||
serverCategories.push({
|
||||
slug: key,
|
||||
name: catName,
|
||||
});
|
||||
});
|
||||
|
||||
let challengeDetails = clone(this.workingChallenge);
|
||||
challengeDetails.categories = serverCategories;
|
||||
|
||||
let challenge;
|
||||
if (this.cloning) {
|
||||
challenge = await this.$store.dispatch('challenges:cloneChallenge', {
|
||||
challenge: challengeDetails,
|
||||
cloningChallengeId: this.cloningChallengeId,
|
||||
});
|
||||
this.cloningChallengeId = '';
|
||||
} else {
|
||||
challenge = await this.$store.dispatch('challenges:createChallenge', {challenge: challengeDetails});
|
||||
}
|
||||
|
||||
// Update Group Prize
|
||||
let challengeGroup = this.groups.find(group => {
|
||||
return group._id === this.workingChallenge.group;
|
||||
});
|
||||
|
||||
// @TODO: Share with server
|
||||
const prizeCost = this.workingChallenge.prize / 4;
|
||||
const challengeGroupLeader = challengeGroup.leader && challengeGroup.leader._id ? challengeGroup.leader._id : challengeGroup.leader;
|
||||
const userIsLeader = challengeGroupLeader === this.user._id;
|
||||
if (challengeGroup && userIsLeader && challengeGroup.balance > 0 && challengeGroup.balance >= prizeCost) {
|
||||
// Group pays for all of prize
|
||||
} else if (challengeGroup && userIsLeader && challengeGroup.balance > 0) {
|
||||
// User pays remainder of prize cost after group
|
||||
let remainder = prizeCost - challengeGroup.balance;
|
||||
this.user.balance -= remainder;
|
||||
} else {
|
||||
// User pays for all of prize
|
||||
this.user.balance -= prizeCost;
|
||||
}
|
||||
|
||||
this.$emit('createChallenge', challenge);
|
||||
this.resetWorkingChallenge();
|
||||
|
||||
this.$root.$emit('bv::hide::modal', 'challenge-modal');
|
||||
this.$router.push(`/challenges/${challenge._id}`);
|
||||
},
|
||||
updateChallenge () {
|
||||
let categoryKeys = this.workingChallenge.categories;
|
||||
let serverCategories = [];
|
||||
categoryKeys.forEach(key => {
|
||||
let newKey = key.trim();
|
||||
let catName = this.categoriesHashByKey[newKey];
|
||||
serverCategories.push({
|
||||
slug: newKey,
|
||||
name: catName,
|
||||
});
|
||||
});
|
||||
|
||||
let challengeDetails = clone(this.workingChallenge);
|
||||
challengeDetails.categories = serverCategories;
|
||||
|
||||
this.$emit('updatedChallenge', {
|
||||
challenge: challengeDetails,
|
||||
});
|
||||
this.$store.dispatch('challenges:updateChallenge', {challenge: challengeDetails});
|
||||
this.resetWorkingChallenge();
|
||||
this.$root.$emit('bv::hide::modal', 'challenge-modal');
|
||||
},
|
||||
toggleCategorySelect () {
|
||||
this.showCategorySelect = !this.showCategorySelect;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,113 @@
|
||||
<template lang="pug">
|
||||
div
|
||||
b-modal#close-challenge-modal(:title="$t('createGuild')", size='md')
|
||||
.header-wrap(slot="modal-header")
|
||||
h2.text-center(v-once) {{$t('endChallenge')}}
|
||||
.row.text-center
|
||||
.col-12
|
||||
.support-habitica
|
||||
// @TODO: Add challenge achievement badge here
|
||||
.col-12
|
||||
strong(v-once) {{$t('selectChallengeWinnersDescription')}}
|
||||
.col-12
|
||||
member-search-dropdown(:text='winnerText', :members='members', :challengeId='challengeId', @member-selected='selectMember')
|
||||
.col-12
|
||||
button.btn.btn-primary(v-once, @click='closeChallenge') {{$t('awardWinners')}}
|
||||
.col-12
|
||||
hr
|
||||
.or {{$t('or')}}
|
||||
.col-12
|
||||
strong(v-once) {{$t('doYouWantedToDeleteChallenge')}}
|
||||
.col-12
|
||||
button.btn.btn-danger(v-once, @click='deleteChallenge()') {{$t('deleteChallenge')}}
|
||||
.footer-wrap(slot="modal-footer")
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
#close-challenge-modal {
|
||||
h2 {
|
||||
color: $purple-200
|
||||
}
|
||||
|
||||
#close-challenge-modal_modal_body {
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
|
||||
.header-wrap {
|
||||
width: 100%;
|
||||
padding-top: 2em;
|
||||
}
|
||||
|
||||
.support-habitica {
|
||||
background-image: url('~client/assets/svg/for-css/support-habitica-gems.svg');
|
||||
width: 325px;
|
||||
height: 89px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.modal-footer, .modal-header {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.footer-wrap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.col-12 {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.or {
|
||||
margin-top: -2em;
|
||||
background: $white;
|
||||
width: 40px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import memberSearchDropdown from 'client/components/members/memberSearchDropdown';
|
||||
|
||||
export default {
|
||||
props: ['challengeId', 'members', 'prize'],
|
||||
components: {
|
||||
memberSearchDropdown,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
winner: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
winnerText () {
|
||||
if (!this.winner.profile) return this.$t('selectMember');
|
||||
return this.winner.profile.name;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectMember (member) {
|
||||
this.winner = member;
|
||||
},
|
||||
async closeChallenge () {
|
||||
this.challenge = await this.$store.dispatch('challenges:selectChallengeWinner', {
|
||||
challengeId: this.challengeId,
|
||||
winnerId: this.winner._id,
|
||||
});
|
||||
this.$router.push('/challenges/myChallenges');
|
||||
},
|
||||
async deleteChallenge () {
|
||||
if (!confirm('Are you sure you want to delete this challenge?')) return;
|
||||
this.challenge = await this.$store.dispatch('challenges:deleteChallenge', {
|
||||
challengeId: this.challengeId,
|
||||
prize: this.prize,
|
||||
});
|
||||
this.$router.push('/challenges/myChallenges');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,199 @@
|
||||
<template lang="pug">
|
||||
.row
|
||||
challenge-modal(v-on:createChallenge='challengeCreated')
|
||||
sidebar(v-on:search="updateSearch", v-on:filter="updateFilters")
|
||||
.col-12.col-md-10.standard-page
|
||||
.row.header-row
|
||||
.col-md-8.text-left
|
||||
h1(v-once) {{$t('findChallenges')}}
|
||||
.col-md-4
|
||||
// @TODO: implement sorting span.dropdown-label {{ $t('sortBy') }}
|
||||
b-dropdown(:text="$t('sort')", right=true)
|
||||
b-dropdown-item(v-for='sortOption in sortOptions', :key="sortOption.value", @click='sort(sortOption.value)') {{sortOption.text}}
|
||||
button.btn.btn-secondary.create-challenge-button.float-right(@click='createChallenge()')
|
||||
.svg-icon.positive-icon(v-html="icons.positiveIcon")
|
||||
span(v-once) {{$t('createChallenge')}}
|
||||
|
||||
.row
|
||||
.no-challenges.text-center.col-md-6.offset-3(v-if='!loading && filteredChallenges.length === 0')
|
||||
h2(v-once) {{$t('noChallengeMatchFilters')}}
|
||||
|
||||
.row
|
||||
.col-12.col-md-6(v-for='challenge in filteredChallenges')
|
||||
challenge-item(:challenge='challenge')
|
||||
mugen-scroll(
|
||||
:handler="infiniteScrollTrigger",
|
||||
:should-handle="!loading && canLoadMore",
|
||||
:threshold="1",
|
||||
v-show="loading",
|
||||
)
|
||||
h2.col-12.loading(v-once) {{ $t('loading') }}
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.header-row {
|
||||
h1 {
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
.create-challenge-button {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.positive-icon {
|
||||
color: $green-10;
|
||||
width: 10px;
|
||||
display: inline-block;
|
||||
margin-right: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.no-challenges {
|
||||
color: $gray-200;
|
||||
margin-top: 10em;
|
||||
|
||||
h2 {
|
||||
color: $gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: $purple-300;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
import Sidebar from './sidebar';
|
||||
import ChallengeItem from './challengeItem';
|
||||
import challengeModal from './challengeModal';
|
||||
import challengeUtilities from 'client/mixins/challengeUtilities';
|
||||
|
||||
import positiveIcon from 'assets/svg/positive.svg';
|
||||
|
||||
import MugenScroll from 'vue-mugen-scroll';
|
||||
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
export default {
|
||||
mixins: [challengeUtilities],
|
||||
components: {
|
||||
Sidebar,
|
||||
ChallengeItem,
|
||||
challengeModal,
|
||||
MugenScroll,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: true,
|
||||
canLoadMore: true,
|
||||
icons: Object.freeze({
|
||||
positiveIcon,
|
||||
}),
|
||||
challenges: [],
|
||||
sort: 'none',
|
||||
sortOptions: [
|
||||
{
|
||||
text: this.$t('none'),
|
||||
value: 'none',
|
||||
},
|
||||
{
|
||||
text: this.$t('participants'),
|
||||
value: 'participants',
|
||||
},
|
||||
{
|
||||
text: this.$t('name'),
|
||||
value: 'name',
|
||||
},
|
||||
{
|
||||
text: this.$t('end_date'),
|
||||
value: 'end_date',
|
||||
},
|
||||
{
|
||||
text: this.$t('start_date'),
|
||||
value: 'start_date',
|
||||
},
|
||||
],
|
||||
search: '',
|
||||
filters: {},
|
||||
page: 0,
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.loadChallenges();
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
filteredChallenges () {
|
||||
return this.challenges;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateSearch (eventData) {
|
||||
this.search = eventData.searchTerm;
|
||||
this.page = 0;
|
||||
this.loadChallenges();
|
||||
},
|
||||
updateFilters (eventData) {
|
||||
this.filters = eventData;
|
||||
this.page = 0;
|
||||
this.loadChallenges();
|
||||
},
|
||||
createChallenge () {
|
||||
this.$root.$emit('bv::show::modal', 'challenge-modal');
|
||||
},
|
||||
async loadChallenges () {
|
||||
this.loading = true;
|
||||
|
||||
let categories = '';
|
||||
if (this.filters.categories) {
|
||||
categories = this.filters.categories.join(',');
|
||||
}
|
||||
|
||||
let owned = '';
|
||||
// @TODO: we skip ownership === 2 because it is the same as === 0 right now
|
||||
if (this.filters.ownership && this.filters.ownership.length === 1) {
|
||||
owned = this.filters.ownership[0];
|
||||
}
|
||||
|
||||
const challenges = await this.$store.dispatch('challenges:getUserChallenges', {
|
||||
page: this.page,
|
||||
search: this.search,
|
||||
categories,
|
||||
owned,
|
||||
});
|
||||
|
||||
if (this.page === 0) {
|
||||
this.challenges = challenges;
|
||||
} else {
|
||||
this.challenges = this.challenges.concat(challenges);
|
||||
}
|
||||
|
||||
// only show the load more Button if the max count was returned
|
||||
this.canLoadMore = challenges.length === 10;
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
challengeCreated (challenge) {
|
||||
this.challenges.push(challenge);
|
||||
},
|
||||
infiniteScrollTrigger () {
|
||||
// show loading and wait until the loadMore debounced
|
||||
// or else it would trigger on every scrolling-pixel (while not loading)
|
||||
if (this.canLoadMore) {
|
||||
this.loading = true;
|
||||
}
|
||||
|
||||
this.loadMore();
|
||||
},
|
||||
loadMore: debounce(function loadMoreDebounce () {
|
||||
this.page += 1;
|
||||
this.loadChallenges();
|
||||
}, 1000),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template lang="pug">
|
||||
div
|
||||
challenge-modal(:groupId='groupId', v-on:createChallenge='challengeCreated')
|
||||
.row.no-quest-section(v-if='challenges.length === 0')
|
||||
.col-12.text-center
|
||||
.svg-icon.challenge-icon(v-html="icons.challengeIcon")
|
||||
h4(v-once) {{ $t('haveNoChallenges') }}
|
||||
p(v-once) {{ $t('challengeDetails') }}
|
||||
button.btn.btn-secondary(@click='createChallenge()') {{ $t('createChallenge') }}
|
||||
template(v-else)
|
||||
challenge-item(v-for='challenge in challenges',:challenge='challenge',:key='challenge._id',:fullLayout='false')
|
||||
.col-12.text-center
|
||||
button.btn.btn-secondary(@click='createChallenge()') {{ $t('createChallenge') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.no-quest-section {
|
||||
padding: 2em;
|
||||
color: $gray-300;
|
||||
|
||||
h4 {
|
||||
color: $gray-300;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import challengeModal from './challengeModal';
|
||||
import {mapState} from 'client/libs/store';
|
||||
import markdownDirective from 'client/directives/markdown';
|
||||
|
||||
import challengeItem from './challengeItem';
|
||||
import challengeIcon from 'assets/svg/challenge.svg';
|
||||
|
||||
export default {
|
||||
props: ['groupId'],
|
||||
components: {
|
||||
challengeModal,
|
||||
challengeItem,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
challenges: [],
|
||||
icons: Object.freeze({
|
||||
challengeIcon,
|
||||
}),
|
||||
groupIdForChallenges: '',
|
||||
};
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
mounted () {
|
||||
this.loadChallenges();
|
||||
},
|
||||
watch: {
|
||||
async groupId () {
|
||||
this.loadChallenges();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async loadChallenges () {
|
||||
this.groupIdForChallenges = this.groupId;
|
||||
if (this.groupId === 'party' && this.user.party._id) this.groupIdForChallenges = this.user.party._id;
|
||||
this.challenges = await this.$store.dispatch('challenges:getGroupChallenges', {groupId: this.groupIdForChallenges});
|
||||
},
|
||||
createChallenge () {
|
||||
this.$root.$emit('bv::show::modal', 'challenge-modal');
|
||||
},
|
||||
challengeCreated (challenge) {
|
||||
if (challenge.group._id !== this.groupIdForChallenges) return;
|
||||
this.challenges.push(challenge);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,19 @@
|
||||
<template lang="pug">
|
||||
.row
|
||||
secondary-menu.col-12
|
||||
router-link.nav-link(:to="{name: 'myChallenges'}", :class="{'active': $route.name === 'myChallenges'}") {{ $t('myChallenges') }}
|
||||
router-link.nav-link(:to="{name: 'findChallenges'}", :class="{'active': $route.name === 'findChallenges'}") {{ $t('findChallenges') }}
|
||||
|
||||
.col-12
|
||||
router-view
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SecondaryMenu from 'client/components/secondaryMenu';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SecondaryMenu,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template lang="pug">
|
||||
b-modal#leave-challenge-modal(:title="$t('leaveChallenge')", size='sm', :hide-footer="true")
|
||||
.modal-body
|
||||
h2 {{ $t('confirmKeepChallengeTasks') }}
|
||||
div
|
||||
button.btn.btn-primary(@click='leaveChallenge("keep")') {{ $t('keepThem') }}
|
||||
button.btn.btn-danger(@click='leaveChallenge("remove-all")') {{ $t('removeThem') }}
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-body {
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import notifications from 'client/mixins/notifications';
|
||||
|
||||
export default {
|
||||
props: ['challengeId'],
|
||||
mixins: [notifications],
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
async leaveChallenge (keep) {
|
||||
let index = findIndex(this.user.challenges, (id) => {
|
||||
return id === this.challengeId;
|
||||
});
|
||||
this.user.challenges.splice(index, 1);
|
||||
await this.$store.dispatch('challenges:leaveChallenge', {
|
||||
challengeId: this.challengeId,
|
||||
keep,
|
||||
});
|
||||
await this.$store.dispatch('tasks:fetchUserTasks', {forceLoad: true});
|
||||
this.close();
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'leave-challenge-modal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,231 @@
|
||||
<template lang="pug">
|
||||
.row
|
||||
challenge-modal(v-on:createChallenge='challengeCreated')
|
||||
sidebar(v-on:search="updateSearch", v-on:filter="updateFilters")
|
||||
|
||||
.col-10.standard-page
|
||||
.row.header-row
|
||||
.col-md-8.text-left
|
||||
h1(v-once) {{$t('myChallenges')}}
|
||||
.col-md-4
|
||||
// @TODO: implement sorting span.dropdown-label {{ $t('sortBy') }}
|
||||
b-dropdown(:text="$t('sort')", right=true)
|
||||
b-dropdown-item(v-for='sortOption in sortOptions', :key="sortOption.value", @click='sort(sortOption.value)') {{sortOption.text}}
|
||||
button.btn.btn-secondary.create-challenge-button.float-right(@click='createChallenge()')
|
||||
.svg-icon.positive-icon(v-html="icons.positiveIcon")
|
||||
span(v-once) {{$t('createChallenge')}}
|
||||
|
||||
.row
|
||||
.no-challenges.text-center.col-md-6.offset-3(v-if='!loading && challenges.length === 0')
|
||||
.svg-icon(v-html="icons.challengeIcon")
|
||||
h2(v-once) {{$t('noChallengeTitle')}}
|
||||
p(v-once) {{$t('challengeDescription1')}}
|
||||
p(v-once) {{$t('challengeDescription2')}}
|
||||
|
||||
.row
|
||||
.no-challenges.text-center.col-md-6.offset-3(v-if='!loading && challenges.length > 0 && filteredChallenges.length === 0')
|
||||
h2(v-once) {{$t('noChallengeMatchFilters')}}
|
||||
|
||||
.row
|
||||
.col-12.col-md-6(v-for='challenge in filteredChallenges')
|
||||
challenge-item(:challenge='challenge')
|
||||
mugen-scroll(
|
||||
:handler="infiniteScrollTrigger",
|
||||
:should-handle="!loading && canLoadMore",
|
||||
:threshold="1",
|
||||
v-show="loading",
|
||||
)
|
||||
h2.col-12.loading(v-once) {{ $t('loading') }}
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.header-row {
|
||||
h1 {
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
.create-challenge-button {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.positive-icon {
|
||||
color: $green-10;
|
||||
width: 10px;
|
||||
display: inline-block;
|
||||
margin-right: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.no-challenges {
|
||||
color: $gray-200;
|
||||
margin-top: 10em;
|
||||
|
||||
h2 {
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
color: #C3C0C7;
|
||||
width: 88.7px;
|
||||
margin: 1em auto;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: $purple-300;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import Sidebar from './sidebar';
|
||||
import ChallengeItem from './challengeItem';
|
||||
import challengeModal from './challengeModal';
|
||||
import challengeUtilities from 'client/mixins/challengeUtilities';
|
||||
|
||||
import challengeIcon from 'assets/svg/challenge.svg';
|
||||
import positiveIcon from 'assets/svg/positive.svg';
|
||||
import MugenScroll from 'vue-mugen-scroll';
|
||||
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
export default {
|
||||
mixins: [challengeUtilities],
|
||||
components: {
|
||||
Sidebar,
|
||||
ChallengeItem,
|
||||
challengeModal,
|
||||
MugenScroll,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
challengeIcon,
|
||||
positiveIcon,
|
||||
}),
|
||||
loading: false,
|
||||
canLoadMore: true,
|
||||
challenges: [],
|
||||
sort: 'none',
|
||||
sortOptions: [
|
||||
{
|
||||
text: this.$t('none'),
|
||||
value: 'none',
|
||||
},
|
||||
{
|
||||
text: this.$t('participants'),
|
||||
value: 'participants',
|
||||
},
|
||||
{
|
||||
text: this.$t('name'),
|
||||
value: 'name',
|
||||
},
|
||||
{
|
||||
text: this.$t('end_date'),
|
||||
value: 'end_date',
|
||||
},
|
||||
{
|
||||
text: this.$t('start_date'),
|
||||
value: 'start_date',
|
||||
},
|
||||
],
|
||||
search: '',
|
||||
filters: {},
|
||||
page: 0,
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.loadChallenges();
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
filteredChallenges () {
|
||||
const filters = this.filters;
|
||||
const user = this.user;
|
||||
|
||||
return this.challenges.filter(challenge => {
|
||||
let isMember = true;
|
||||
|
||||
let filteringRole = filters.roles && filters.roles.length > 0;
|
||||
if (filteringRole && filters.roles.indexOf('participating') !== -1) {
|
||||
isMember = this.isMemberOfChallenge(user, challenge);
|
||||
}
|
||||
|
||||
if (filteringRole && filters.roles.indexOf('not_participating') !== -1) {
|
||||
isMember = !this.isMemberOfChallenge(user, challenge);
|
||||
}
|
||||
|
||||
return isMember;
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateSearch (eventData) {
|
||||
this.search = eventData.searchTerm;
|
||||
this.page = 0;
|
||||
this.loadChallenges();
|
||||
},
|
||||
updateFilters (eventData) {
|
||||
this.filters = eventData;
|
||||
this.page = 0;
|
||||
this.loadChallenges();
|
||||
},
|
||||
createChallenge () {
|
||||
this.$store.state.challengeOptions.workingChallenge = {};
|
||||
this.$root.$emit('bv::show::modal', 'challenge-modal');
|
||||
},
|
||||
async loadChallenges () {
|
||||
this.loading = true;
|
||||
|
||||
let categories = '';
|
||||
if (this.filters.categories) {
|
||||
categories = this.filters.categories.join(',');
|
||||
}
|
||||
|
||||
let owned = '';
|
||||
// @TODO: we skip ownership === 2 because it is the same as === 0 right now
|
||||
if (this.filters.ownership && this.filters.ownership.length === 1) {
|
||||
owned = this.filters.ownership[0];
|
||||
}
|
||||
|
||||
const challenges = await this.$store.dispatch('challenges:getUserChallenges', {
|
||||
page: this.page,
|
||||
search: this.search,
|
||||
categories,
|
||||
owned,
|
||||
member: true,
|
||||
});
|
||||
|
||||
if (this.page === 0) {
|
||||
this.challenges = challenges;
|
||||
} else {
|
||||
this.challenges = this.challenges.concat(challenges);
|
||||
}
|
||||
|
||||
// only show the load more Button if the max count was returned
|
||||
this.canLoadMore = challenges.length === 10;
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
challengeCreated (challenge) {
|
||||
this.challenges.push(challenge);
|
||||
},
|
||||
infiniteScrollTrigger () {
|
||||
// show loading and wait until the loadMore debounced
|
||||
// or else it would trigger on every scrolling-pixel (while not loading)
|
||||
if (this.canLoadMore) {
|
||||
this.loading = true;
|
||||
}
|
||||
|
||||
this.loadMore();
|
||||
},
|
||||
loadMore: debounce(function loadMoreDebounce () {
|
||||
this.page += 1;
|
||||
this.loadChallenges();
|
||||
}, 1000),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,160 @@
|
||||
<template lang="pug">
|
||||
.standard-sidebar.d-none.d-sm-block
|
||||
.form-group
|
||||
input.form-control.search(type="text", :placeholder="$t('search')", v-model='searchTerm')
|
||||
form
|
||||
h2(v-once) {{ $t('filter') }}
|
||||
.form-group
|
||||
h3 {{ $t('category') }}
|
||||
.form-check(
|
||||
v-for="group in categoryOptions",
|
||||
:key="group.key",
|
||||
)
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input(type="checkbox", :value='group.key' v-model="categoryFilters", :id="group.key")
|
||||
label.custom-control-label(v-once, :for="group.key") {{ $t(group.label) }}
|
||||
.form-group(v-if='$route.name !== "findChallenges"')
|
||||
h3 {{ $t('membership') }}
|
||||
.form-check(
|
||||
v-for="group in roleOptions",
|
||||
:key="group.key",
|
||||
)
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input(type="checkbox", :value='group.key' v-model="roleFilters", :id="group.key")
|
||||
label.custom-control-label(v-once, :for="group.key") {{ $t(group.label) }}
|
||||
.form-group
|
||||
h3 {{ $t('ownership') }}
|
||||
.form-check(
|
||||
v-for="group in ownershipOptions",
|
||||
:key="group.key",
|
||||
)
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input(type="checkbox", :value='group.key' v-model="ownershipFilters", :id="group.key")
|
||||
label.custom-control-label(v-once, :for="group.key") {{ $t(group.label) }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import throttle from 'lodash/throttle';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
categoryFilters: [],
|
||||
categoryOptions: [
|
||||
{
|
||||
label: 'habitica_official',
|
||||
key: 'habitica_official',
|
||||
},
|
||||
{
|
||||
label: 'academics',
|
||||
key: 'academics',
|
||||
},
|
||||
{
|
||||
label: 'advocacy_causes',
|
||||
key: 'advocacy_causes',
|
||||
},
|
||||
{
|
||||
label: 'creativity',
|
||||
key: 'creativity',
|
||||
},
|
||||
{
|
||||
label: 'entertainment',
|
||||
key: 'entertainment',
|
||||
},
|
||||
{
|
||||
label: 'finance',
|
||||
key: 'finance',
|
||||
},
|
||||
{
|
||||
label: 'health_fitness',
|
||||
key: 'health_fitness',
|
||||
},
|
||||
{
|
||||
label: 'hobbies_occupations',
|
||||
key: 'hobbies_occupations',
|
||||
},
|
||||
{
|
||||
label: 'location_based',
|
||||
key: 'location_based',
|
||||
},
|
||||
{
|
||||
label: 'mental_health',
|
||||
key: 'mental_health',
|
||||
},
|
||||
{
|
||||
label: 'getting_organized',
|
||||
key: 'getting_organized',
|
||||
},
|
||||
{
|
||||
label: 'self_improvement',
|
||||
key: 'self_improvement',
|
||||
},
|
||||
{
|
||||
label: 'spirituality',
|
||||
key: 'spirituality',
|
||||
},
|
||||
{
|
||||
label: 'time_management',
|
||||
key: 'time_management',
|
||||
},
|
||||
],
|
||||
roleFilters: [],
|
||||
roleOptions: [
|
||||
{
|
||||
label: 'participating',
|
||||
key: 'participating',
|
||||
},
|
||||
{
|
||||
label: 'not_participating',
|
||||
key: 'not_participating',
|
||||
},
|
||||
// {
|
||||
// label: 'either',
|
||||
// key: 'either',
|
||||
// },
|
||||
],
|
||||
ownershipFilters: [],
|
||||
ownershipOptions: [
|
||||
{
|
||||
label: 'owned',
|
||||
key: 'owned',
|
||||
},
|
||||
{
|
||||
label: 'not_owned',
|
||||
key: 'not_owned',
|
||||
},
|
||||
// {
|
||||
// label: 'either',
|
||||
// key: 'either',
|
||||
// },
|
||||
],
|
||||
searchTerm: '',
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
categoryFilters: function categoryFilters () {
|
||||
this.emitFilters();
|
||||
},
|
||||
roleFilters: function roleFilters () {
|
||||
this.emitFilters();
|
||||
},
|
||||
ownershipFilters: function ownershipFilters () {
|
||||
this.emitFilters();
|
||||
},
|
||||
searchTerm: throttle(function searchTerm (newSearch) {
|
||||
this.$emit('search', {
|
||||
searchTerm: newSearch,
|
||||
});
|
||||
}, 500),
|
||||
},
|
||||
methods: {
|
||||
emitFilters () {
|
||||
this.$emit('filter', {
|
||||
categories: this.categoryFilters,
|
||||
roles: this.roleFilters,
|
||||
ownership: this.ownershipFilters,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,236 @@
|
||||
<template lang="pug">
|
||||
.autocomplete-selection(v-if='searchResults.length > 0', :style='autocompleteStyle')
|
||||
.autocomplete-results.d-flex.align-items-center(
|
||||
v-for='result in searchResults',
|
||||
@click='select(result)',
|
||||
@mouseenter='setHover(result)',
|
||||
@mouseleave='resetSelection()',
|
||||
:class='{"hover-background": result.hover}',
|
||||
)
|
||||
span
|
||||
h3.profile-name(:class='userLevelStyle(result.msg)') {{ result.displayName }}
|
||||
.svg-icon(v-html="tierIcon(result.msg)", v-if='showTierStyle(result.msg)')
|
||||
span.username.ml-2(v-if='result.username', :class='{"hover-foreground": result.hover}') @{{ result.username }}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/tiers.scss';
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.autocomplete-results {
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
.autocomplete-selection {
|
||||
box-shadow: 1px 1px 1px #efefef;
|
||||
}
|
||||
|
||||
.hover-background {
|
||||
background-color: rgba(213, 200, 255, 0.32);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hover-foreground {
|
||||
color: $purple-300 !important;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
margin-bottom: 0rem;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 10px;
|
||||
display: inline-block;
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.username {
|
||||
color: $gray-200;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import styleHelper from 'client/mixins/styleHelper';
|
||||
import tier1 from 'assets/svg/tier-1.svg';
|
||||
import tier2 from 'assets/svg/tier-2.svg';
|
||||
import tier3 from 'assets/svg/tier-3.svg';
|
||||
import tier4 from 'assets/svg/tier-4.svg';
|
||||
import tier5 from 'assets/svg/tier-5.svg';
|
||||
import tier6 from 'assets/svg/tier-6.svg';
|
||||
import tier7 from 'assets/svg/tier-7.svg';
|
||||
import tier8 from 'assets/svg/tier-mod.svg';
|
||||
import tier9 from 'assets/svg/tier-staff.svg';
|
||||
import tierNPC from 'assets/svg/tier-npc.svg';
|
||||
|
||||
export default {
|
||||
props: ['selections', 'text', 'caretPosition', 'coords', 'chat', 'textbox'],
|
||||
data () {
|
||||
return {
|
||||
atRegex: /(?!\b)@[\w-]*$/,
|
||||
currentSearch: '',
|
||||
searchActive: false,
|
||||
searchEscaped: false,
|
||||
currentSearchPosition: 0,
|
||||
tmpSelections: [],
|
||||
icons: Object.freeze({
|
||||
tier1,
|
||||
tier2,
|
||||
tier3,
|
||||
tier4,
|
||||
tier5,
|
||||
tier6,
|
||||
tier7,
|
||||
tier8,
|
||||
tier9,
|
||||
tierNPC,
|
||||
}),
|
||||
selected: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
autocompleteStyle () {
|
||||
function heightToUse (textBox, topCoords) {
|
||||
let textBoxHeight = textBox['user-entry'].clientHeight;
|
||||
return topCoords < textBoxHeight ? topCoords + 30 : textBoxHeight + 10;
|
||||
}
|
||||
return {
|
||||
top: `${heightToUse(this.textbox, this.coords.TOP)}px`,
|
||||
left: `${this.coords.LEFT + 30}px`,
|
||||
marginLeft: '-28px',
|
||||
marginTop: '28px',
|
||||
position: 'absolute',
|
||||
minWidth: '100px',
|
||||
zIndex: 100,
|
||||
backgroundColor: 'white',
|
||||
};
|
||||
},
|
||||
searchResults () {
|
||||
if (!this.searchActive) return [];
|
||||
if (!this.atRegex.exec(this.text)) return [];
|
||||
this.currentSearch = this.atRegex.exec(this.text)[0];
|
||||
this.currentSearch = this.currentSearch.substring(1, this.currentSearch.length);
|
||||
|
||||
return this.tmpSelections.filter((option) => {
|
||||
return option.displayName.toLowerCase().indexOf(this.currentSearch.toLowerCase()) !== -1 || option.username && option.username.toLowerCase().indexOf(this.currentSearch.toLowerCase()) !== -1;
|
||||
}).slice(0, 4);
|
||||
},
|
||||
|
||||
},
|
||||
mounted () {
|
||||
this.grabUserNames();
|
||||
},
|
||||
watch: {
|
||||
text (newText) {
|
||||
if (!newText[newText.length - 1] || newText[newText.length - 1] === ' ') {
|
||||
this.searchActive = false;
|
||||
this.searchEscaped = false;
|
||||
return;
|
||||
}
|
||||
if (newText[newText.length - 1] === '@') {
|
||||
this.searchEscaped = false;
|
||||
}
|
||||
if (this.searchEscaped) return;
|
||||
|
||||
if (!this.atRegex.test(newText)) return;
|
||||
|
||||
this.searchActive = true;
|
||||
},
|
||||
chat () {
|
||||
this.resetDefaults();
|
||||
this.grabUserNames();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resetDefaults () {
|
||||
// Mounted is not called when switching between group pages because they have the
|
||||
// the same parent component. So, reset the data
|
||||
this.searchActive = false;
|
||||
this.searchEscaped = false;
|
||||
this.tmpSelections = [];
|
||||
this.resetSelection();
|
||||
},
|
||||
grabUserNames () {
|
||||
let usersThatMessage = groupBy(this.chat, 'user');
|
||||
for (let userKey in usersThatMessage) {
|
||||
let systemMessage = userKey === 'undefined';
|
||||
if (!systemMessage && this.tmpSelections.indexOf(userKey) === -1) {
|
||||
this.tmpSelections.push({
|
||||
displayName: userKey,
|
||||
username: usersThatMessage[userKey][0].username,
|
||||
msg: {
|
||||
backer: usersThatMessage[userKey][0].backer,
|
||||
contributor: usersThatMessage[userKey][0].contributor,
|
||||
},
|
||||
hover: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
showTierStyle (message) {
|
||||
const isContributor = Boolean(message.contributor && message.contributor.level);
|
||||
const isNPC = Boolean(message.backer && message.backer.npc);
|
||||
return isContributor || isNPC;
|
||||
},
|
||||
tierIcon (message) {
|
||||
const isNPC = Boolean(message.backer && message.backer.npc);
|
||||
if (isNPC) {
|
||||
return this.icons.tierNPC;
|
||||
}
|
||||
return this.icons[`tier${message.contributor.level}`];
|
||||
},
|
||||
select (result) {
|
||||
let newText = this.text;
|
||||
const targetName = `${result.username || result.displayName} `;
|
||||
newText = newText.replace(new RegExp(`${this.currentSearch}$`), targetName);
|
||||
this.$emit('select', newText);
|
||||
this.resetSelection();
|
||||
},
|
||||
setHover (result) {
|
||||
this.resetSelection();
|
||||
result.hover = true;
|
||||
},
|
||||
clearHover () {
|
||||
for (const selection of this.searchResults) {
|
||||
selection.hover = false;
|
||||
}
|
||||
},
|
||||
resetSelection () {
|
||||
this.clearHover();
|
||||
this.selected = null;
|
||||
},
|
||||
selectNext () {
|
||||
if (this.searchResults.length > 0) {
|
||||
this.clearHover();
|
||||
this.selected = this.selected === null ?
|
||||
0 :
|
||||
(this.selected + 1) % this.searchResults.length;
|
||||
this.searchResults[this.selected].hover = true;
|
||||
}
|
||||
},
|
||||
selectPrevious () {
|
||||
if (this.searchResults.length > 0) {
|
||||
this.clearHover();
|
||||
this.selected = this.selected === null ?
|
||||
this.searchResults.length - 1 :
|
||||
(this.selected - 1 + this.searchResults.length) % this.searchResults.length;
|
||||
this.searchResults[this.selected].hover = true;
|
||||
}
|
||||
},
|
||||
makeSelection () {
|
||||
if (this.searchResults.length > 0 && this.selected !== null) {
|
||||
const result = this.searchResults[this.selected];
|
||||
this.select(result);
|
||||
}
|
||||
},
|
||||
cancel () {
|
||||
this.searchActive = false;
|
||||
this.searchEscaped = true;
|
||||
this.resetSelection();
|
||||
},
|
||||
},
|
||||
mixins: [styleHelper],
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,288 @@
|
||||
<template lang="pug">
|
||||
div
|
||||
.mentioned-icon(v-if='isUserMentioned')
|
||||
.message-hidden(v-if='!inbox && user.contributor.admin && msg.flagCount') {{flagCountDescription}}
|
||||
.card-body
|
||||
user-link(:userId="msg.uuid", :name="msg.user", :backer="msg.backer", :contributor="msg.contributor")
|
||||
p.time
|
||||
span.mr-1(v-if="msg.username") @{{ msg.username }}
|
||||
span.mr-1(v-if="msg.username") •
|
||||
span(v-b-tooltip="", :title="msg.timestamp | date") {{ msg.timestamp | timeAgo }}
|
||||
span(v-if="msg.client && user.contributor.level >= 4") ({{ msg.client }})
|
||||
.text(v-html='atHighlight(parseMarkdown(msg.text))')
|
||||
.reported(v-if="isMessageReported && (inbox === true)")
|
||||
span(v-once) {{ $t('reportedMessage')}}
|
||||
br
|
||||
span(v-once) {{ $t('canDeleteNow') }}
|
||||
hr
|
||||
.d-flex(v-if='msg.id')
|
||||
.action.d-flex.align-items-center(v-if='!inbox', @click='copyAsTodo(msg)')
|
||||
.svg-icon(v-html="icons.copy")
|
||||
div {{$t('copyAsTodo')}}
|
||||
.action.d-flex.align-items-center(v-if='(inbox || (user.flags.communityGuidelinesAccepted && msg.uuid !== "system")) && (!isMessageReported || user.contributor.admin)', @click='report(msg)')
|
||||
.svg-icon(v-html="icons.report", v-once)
|
||||
div(v-once) {{$t('report')}}
|
||||
.action.d-flex.align-items-center(v-if='msg.uuid === user._id || inbox || user.contributor.admin', @click='remove()')
|
||||
.svg-icon(v-html="icons.delete", v-once)
|
||||
div(v-once) {{$t('delete')}}
|
||||
.ml-auto.d-flex(v-b-tooltip="{title: likeTooltip(msg.likes[user._id])}", v-if='!inbox')
|
||||
.action.d-flex.align-items-center.mr-0(@click='like()', v-if='likeCount > 0', :class='{active: msg.likes[user._id]}')
|
||||
.svg-icon(v-html="icons.liked", :title='$t("liked")')
|
||||
| +{{ likeCount }}
|
||||
.action.d-flex.align-items-center.mr-0(@click='like()', v-if='likeCount === 0', :class='{active: msg.likes[user._id]}')
|
||||
.svg-icon(v-html="icons.like", :title='$t("like")')
|
||||
span(v-if='!msg.likes[user._id] && !inbox') {{ $t('like') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.at-highlight {
|
||||
background-color: rgba(213, 200, 255, 0.32);
|
||||
padding: 0.1rem;
|
||||
}
|
||||
|
||||
.at-text {
|
||||
color: #6133b4;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
@import '~client/assets/scss/tiers.scss';
|
||||
|
||||
.mentioned-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: #bda8ff;
|
||||
box-shadow: 0 1px 1px 0 rgba(26, 24, 29, 0.12);
|
||||
position: absolute;
|
||||
right: -.5em;
|
||||
top: -.5em;
|
||||
}
|
||||
|
||||
.message-hidden {
|
||||
margin-left: 1.5em;
|
||||
margin-top: 1em;
|
||||
color: red;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.75rem 1.25rem 0.75rem 1.25rem;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: #878190;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 14px;
|
||||
color: #4e4a57;
|
||||
text-align: left !important;
|
||||
min-height: 0rem;
|
||||
margin-bottom: -0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
display: inline-block;
|
||||
color: #878190;
|
||||
margin-right: 1em;
|
||||
font-size: 12px;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
color: #A5A1AC;
|
||||
margin-right: .2em;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
color: $purple-300;
|
||||
|
||||
.svg-icon {
|
||||
color: $purple-400;
|
||||
}
|
||||
}
|
||||
|
||||
.reported {
|
||||
margin-top: 18px;
|
||||
color: $red-50;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import moment from 'moment';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
import max from 'lodash/max';
|
||||
|
||||
import habiticaMarkdown from 'habitica-markdown';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import userLink from '../userLink';
|
||||
|
||||
import deleteIcon from 'assets/svg/delete.svg';
|
||||
import copyIcon from 'assets/svg/copy.svg';
|
||||
import likeIcon from 'assets/svg/like.svg';
|
||||
import likedIcon from 'assets/svg/liked.svg';
|
||||
import reportIcon from 'assets/svg/report.svg';
|
||||
import { highlightUsers } from '../../libs/highlightUsers';
|
||||
import { CHAT_FLAG_LIMIT_FOR_HIDING, CHAT_FLAG_FROM_SHADOW_MUTE } from '../../../common/script/constants';
|
||||
|
||||
export default {
|
||||
components: {userLink},
|
||||
props: {
|
||||
msg: {},
|
||||
inbox: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
groupId: {},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
like: likeIcon,
|
||||
copy: copyIcon,
|
||||
report: reportIcon,
|
||||
delete: deleteIcon,
|
||||
liked: likedIcon,
|
||||
}),
|
||||
reported: false,
|
||||
};
|
||||
},
|
||||
filters: {
|
||||
timeAgo (value) {
|
||||
return moment(value).fromNow();
|
||||
},
|
||||
date (value) {
|
||||
// @TODO: Vue doesn't support this so we cant user preference
|
||||
return moment(value).toDate().toString();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
isUserMentioned () {
|
||||
const message = this.msg;
|
||||
const user = this.user;
|
||||
|
||||
if (message.hasOwnProperty('highlight')) return message.highlight;
|
||||
|
||||
message.highlight = false;
|
||||
const messageText = message.text.toLowerCase();
|
||||
const displayName = user.profile.name;
|
||||
const username = user.auth.local && user.auth.local.username;
|
||||
const mentioned = max([messageText.indexOf(username.toLowerCase()), messageText.indexOf(displayName.toLowerCase())]);
|
||||
if (mentioned === -1) return message.highlight;
|
||||
|
||||
const escapedDisplayName = escapeRegExp(displayName);
|
||||
const escapedUsername = escapeRegExp(username);
|
||||
const pattern = `@(${escapedUsername}|${escapedDisplayName})(\\b)`;
|
||||
const precedingChar = messageText.substring(mentioned - 1, mentioned);
|
||||
if (mentioned === 0 || precedingChar.trim() === '' || precedingChar === '@') {
|
||||
let regex = new RegExp(pattern, 'i');
|
||||
message.highlight = regex.test(messageText);
|
||||
}
|
||||
|
||||
return message.highlight;
|
||||
},
|
||||
likeCount () {
|
||||
const message = this.msg;
|
||||
if (!message.likes) return 0;
|
||||
|
||||
let likeCount = 0;
|
||||
for (let key in message.likes) {
|
||||
let like = message.likes[key];
|
||||
if (like) likeCount += 1;
|
||||
}
|
||||
return likeCount;
|
||||
},
|
||||
isMessageReported () {
|
||||
return this.msg.flags && this.msg.flags[this.user.id] || this.reported;
|
||||
},
|
||||
flagCountDescription () {
|
||||
if (!this.msg.flagCount) return '';
|
||||
if (this.msg.flagCount < CHAT_FLAG_LIMIT_FOR_HIDING) return 'Message flagged once, not hidden';
|
||||
if (this.msg.flagCount < CHAT_FLAG_FROM_SHADOW_MUTE) return 'Message hidden';
|
||||
return 'Message hidden (shadow-muted)';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async like () {
|
||||
let message = cloneDeep(this.msg);
|
||||
|
||||
await this.$store.dispatch('chat:like', {
|
||||
groupId: this.groupId,
|
||||
chatId: message.id,
|
||||
});
|
||||
|
||||
if (!message.likes[this.user._id]) {
|
||||
message.likes[this.user._id] = true;
|
||||
} else {
|
||||
message.likes[this.user._id] = !message.likes[this.user._id];
|
||||
}
|
||||
|
||||
this.$emit('message-liked', message);
|
||||
this.$root.$emit('bv::hide::tooltip');
|
||||
},
|
||||
likeTooltip (likedStatus) {
|
||||
if (!likedStatus) return this.$t('like');
|
||||
},
|
||||
copyAsTodo (message) {
|
||||
this.$root.$emit('habitica::copy-as-todo', message);
|
||||
},
|
||||
report () {
|
||||
this.$root.$on('habitica:report-result', data => {
|
||||
if (data.ok) {
|
||||
this.reported = true;
|
||||
}
|
||||
|
||||
this.$root.$off('habitica:report-result');
|
||||
});
|
||||
|
||||
this.$root.$emit('habitica::report-chat', {
|
||||
message: this.msg,
|
||||
groupId: this.groupId || 'privateMessage',
|
||||
});
|
||||
},
|
||||
async remove () {
|
||||
if (!confirm(this.$t('areYouSureDeleteMessage'))) return;
|
||||
|
||||
const message = this.msg;
|
||||
this.$emit('message-removed', message);
|
||||
|
||||
if (this.inbox) {
|
||||
await axios.delete(`/api/v4/inbox/messages/${message.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.$store.dispatch('chat:deleteChat', {
|
||||
groupId: this.groupId,
|
||||
chatId: message.id,
|
||||
});
|
||||
},
|
||||
atHighlight (text) {
|
||||
return highlightUsers(text, this.user.auth.local.username, this.user.profile.name);
|
||||
},
|
||||
parseMarkdown (text) {
|
||||
if (!text) return;
|
||||
return habiticaMarkdown.render(String(text));
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.CHAT_FLAG_LIMIT_FOR_HIDING = CHAT_FLAG_LIMIT_FOR_HIDING;
|
||||
this.CHAT_FLAG_FROM_SHADOW_MUTE = CHAT_FLAG_FROM_SHADOW_MUTE;
|
||||
this.$emit('chat-card-mounted', this.msg.id);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,340 @@
|
||||
<template lang="pug">
|
||||
.container-fluid(ref="container")
|
||||
.row
|
||||
.col-12
|
||||
copy-as-todo-modal(:group-type='groupType', :group-name='groupName', :group-id='groupId')
|
||||
.row.loadmore
|
||||
div(v-if="canLoadMore")
|
||||
.loadmore-divider
|
||||
button.btn.btn-secondary(@click='triggerLoad()') {{ $t('loadEarlierMessages') }}
|
||||
.loadmore-divider
|
||||
h2.col-12.loading(v-show="isLoading") {{ $t('loading') }}
|
||||
div(v-for="(msg, index) in messages", v-if='chat && canViewFlag(msg)', :class='{row: inbox}')
|
||||
.d-flex(v-if='user._id !== msg.uuid', :class='{"flex-grow-1": inbox}')
|
||||
avatar.avatar-left(
|
||||
v-if='msg.userStyles || (cachedProfileData[msg.uuid] && !cachedProfileData[msg.uuid].rejected)',
|
||||
:member="msg.userStyles || cachedProfileData[msg.uuid]",
|
||||
:avatarOnly="true",
|
||||
:overrideTopPadding='"14px"',
|
||||
:hideClassBadge='true',
|
||||
@click.native="showMemberModal(msg.uuid)",
|
||||
:class='{"inbox-avatar-left": inbox}'
|
||||
)
|
||||
.card(:class='{"col-10": inbox}')
|
||||
chat-card(
|
||||
:msg='msg',
|
||||
:inbox='inbox',
|
||||
:groupId='groupId',
|
||||
@message-liked='messageLiked',
|
||||
@message-removed='messageRemoved',
|
||||
@show-member-modal='showMemberModal',
|
||||
@chat-card-mounted='itemWasMounted')
|
||||
.d-flex(v-if='user._id === msg.uuid', :class='{"flex-grow-1": inbox}')
|
||||
.card(:class='{"col-10": inbox}')
|
||||
chat-card(
|
||||
:msg='msg',
|
||||
:inbox='inbox',
|
||||
:groupId='groupId',
|
||||
@message-liked='messageLiked',
|
||||
@message-removed='messageRemoved',
|
||||
@show-member-modal='showMemberModal',
|
||||
@chat-card-mounted='itemWasMounted')
|
||||
avatar(
|
||||
v-if='msg.userStyles || (cachedProfileData[msg.uuid] && !cachedProfileData[msg.uuid].rejected)',
|
||||
:member="msg.userStyles || cachedProfileData[msg.uuid]",
|
||||
:avatarOnly="true",
|
||||
:hideClassBadge='true',
|
||||
:overrideTopPadding='"14px"',
|
||||
@click.native="showMemberModal(msg.uuid)",
|
||||
:class='{"inbox-avatar-right": inbox}'
|
||||
)
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.avatar {
|
||||
width: 10%;
|
||||
min-width: 7rem;
|
||||
}
|
||||
.loadmore {
|
||||
justify-content: center;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
text-align: center;
|
||||
color: $gray-50;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loadmore-divider {
|
||||
height: 1px;
|
||||
background-color: $gray-500;
|
||||
flex: 1;
|
||||
margin-left: 24px;
|
||||
margin-right: 24px;
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-left {
|
||||
margin-left: -1.5rem;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.inbox-avatar-left {
|
||||
margin-left: -1rem;
|
||||
margin-right: 2.5rem;
|
||||
min-width: 5rem;
|
||||
}
|
||||
|
||||
.inbox-avatar-right {
|
||||
margin-left: -3.5rem;
|
||||
}
|
||||
|
||||
.hr {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
border-bottom: 1px solid $gray-500;
|
||||
text-align: center;
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
.hr-middle {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
font-family: 'Roboto Condensed';
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
color: $gray-200;
|
||||
background-color: $gray-700;
|
||||
padding: .2em;
|
||||
margin-top: .2em;
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 0px;
|
||||
margin-bottom: .5em;
|
||||
padding: 0rem;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.message-scroll .d-flex {
|
||||
min-width: 1px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import axios from 'axios';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import debounce from 'lodash/debounce';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
|
||||
import Avatar from '../avatar';
|
||||
import copyAsTodoModal from './copyAsTodoModal';
|
||||
import chatCard from './chatCard';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
chat: {},
|
||||
inbox: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
groupType: {},
|
||||
groupId: {},
|
||||
groupName: {},
|
||||
|
||||
isLoading: Boolean,
|
||||
canLoadMore: Boolean,
|
||||
},
|
||||
components: {
|
||||
copyAsTodoModal,
|
||||
chatCard,
|
||||
Avatar,
|
||||
},
|
||||
mounted () {
|
||||
this.loadProfileCache();
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
currentDayDividerDisplay: moment().day(),
|
||||
cachedProfileData: {},
|
||||
currentProfileLoadedCount: 0,
|
||||
currentProfileLoadedEnd: 10,
|
||||
loading: false,
|
||||
handleScrollBack: false,
|
||||
lastOffset: -1,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
// @TODO: We need a different lazy load mechnism.
|
||||
// But honestly, adding a paging route to chat would solve this
|
||||
messages () {
|
||||
this.loadProfileCache();
|
||||
return this.chat;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleScroll () {
|
||||
this.loadProfileCache(window.scrollY / 1000);
|
||||
},
|
||||
async triggerLoad () {
|
||||
const container = this.$refs.container;
|
||||
|
||||
// get current offset
|
||||
this.lastOffset = container.scrollTop - (container.scrollHeight - container.clientHeight);
|
||||
// disable scroll
|
||||
container.style.overflowY = 'hidden';
|
||||
|
||||
const canLoadMore = this.inbox && !this.isLoading && this.canLoadMore;
|
||||
if (canLoadMore) {
|
||||
await this.$emit('triggerLoad');
|
||||
this.handleScrollBack = true;
|
||||
}
|
||||
},
|
||||
canViewFlag (message) {
|
||||
if (message.uuid === this.user._id) return true;
|
||||
if (!message.flagCount || message.flagCount < 2) return true;
|
||||
return this.user.contributor.admin;
|
||||
},
|
||||
loadProfileCache: debounce(function loadProfileCache (screenPosition) {
|
||||
this._loadProfileCache(screenPosition);
|
||||
}, 1000),
|
||||
async _loadProfileCache (screenPosition) {
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
|
||||
let promises = [];
|
||||
const noProfilesLoaded = Object.keys(this.cachedProfileData).length === 0;
|
||||
|
||||
// @TODO: write an explination
|
||||
// @TODO: Remove this after enough messages are cached
|
||||
if (!noProfilesLoaded && screenPosition && Math.floor(screenPosition) + 1 > this.currentProfileLoadedEnd / 10) {
|
||||
this.currentProfileLoadedEnd = 10 * (Math.floor(screenPosition) + 1);
|
||||
} else if (!noProfilesLoaded && screenPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
let aboutToCache = {};
|
||||
this.messages.forEach(message => {
|
||||
let uuid = message.uuid;
|
||||
|
||||
if (message.userStyles) {
|
||||
this.$set(this.cachedProfileData, uuid, message.userStyles);
|
||||
}
|
||||
|
||||
if (Boolean(uuid) && !this.cachedProfileData[uuid] && !aboutToCache[uuid]) {
|
||||
if (uuid === 'system' || this.currentProfileLoadedCount === this.currentProfileLoadedEnd) return;
|
||||
aboutToCache[uuid] = {};
|
||||
promises.push(axios.get(`/api/v4/members/${uuid}`));
|
||||
this.currentProfileLoadedCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
let results = await Promise.all(promises);
|
||||
results.forEach(result => {
|
||||
// We could not load the user. Maybe they were deleted. So, let's cache empty so we don't try again
|
||||
if (!result || !result.data || result.status >= 400) {
|
||||
return;
|
||||
}
|
||||
|
||||
let userData = result.data.data;
|
||||
this.$set(this.cachedProfileData, userData._id, userData);
|
||||
});
|
||||
|
||||
// Merge in any attempts that were rejected so we don't attempt again
|
||||
for (let uuid in aboutToCache) {
|
||||
if (!this.cachedProfileData[uuid]) {
|
||||
this.$set(this.cachedProfileData, uuid, {rejected: true});
|
||||
}
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
displayDivider (message) {
|
||||
if (this.currentDayDividerDisplay !== moment(message.timestamp).day()) {
|
||||
this.currentDayDividerDisplay = moment(message.timestamp).day();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
async showMemberModal (memberId) {
|
||||
let profile = this.cachedProfileData[memberId];
|
||||
|
||||
if (!profile._id) {
|
||||
const result = await this.$store.dispatch('members:fetchMember', { memberId });
|
||||
if (result.response && result.response.status === 404) {
|
||||
return this.$store.dispatch('snackbars:add', {
|
||||
title: 'Habitica',
|
||||
text: this.$t('messageDeletedUser'),
|
||||
type: 'error',
|
||||
timeout: false,
|
||||
});
|
||||
} else {
|
||||
this.cachedProfileData[memberId] = result.data.data;
|
||||
profile = result.data.data;
|
||||
}
|
||||
}
|
||||
|
||||
// Open the modal only if the data is available
|
||||
if (profile && !profile.rejected) {
|
||||
this.$router.push({name: 'userProfile', params: {userId: profile._id}});
|
||||
}
|
||||
},
|
||||
itemWasMounted: debounce(function itemWasMounted () {
|
||||
if (this.handleScrollBack) {
|
||||
this.handleScrollBack = false;
|
||||
|
||||
const container = this.$refs.container;
|
||||
const offset = container.scrollHeight - container.clientHeight;
|
||||
|
||||
const newOffset = offset + this.lastOffset;
|
||||
|
||||
container.scrollTo(0, newOffset);
|
||||
// enable scroll again
|
||||
container.style.overflowY = 'scroll';
|
||||
}
|
||||
}, 50),
|
||||
messageLiked (message) {
|
||||
const chatIndex = findIndex(this.chat, chatMessage => {
|
||||
return chatMessage.id === message.id;
|
||||
});
|
||||
this.chat.splice(chatIndex, 1, message);
|
||||
},
|
||||
messageRemoved (message) {
|
||||
if (this.inbox) {
|
||||
this.$emit('message-removed', message);
|
||||
return;
|
||||
}
|
||||
|
||||
const chatIndex = findIndex(this.chat, chatMessage => {
|
||||
return chatMessage.id === message.id;
|
||||
});
|
||||
this.chat.splice(chatIndex, 1);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,78 @@
|
||||
<template lang="pug">
|
||||
b-modal#copyAsTodo(:title="$t('copyMessageAsToDo')", :hide-footer="true", size='md')
|
||||
.form-group
|
||||
input.form-control(type='text', v-model='task.text')
|
||||
.form-group
|
||||
textarea.form-control(rows='5', v-model='task.notes' focus-element='true')
|
||||
hr
|
||||
task(v-if='task._id', :isUser="isUser", :task="task")
|
||||
.modal-footer
|
||||
button.btn.btn-secondary(@click='close()') {{ $t('close') }}
|
||||
button.btn.btn-primary(@click='saveTodo()') {{ $t('submit') }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'client/libs/store';
|
||||
import markdownDirective from 'client/directives/markdown';
|
||||
import notificationsMixin from 'client/mixins/notifications';
|
||||
import Task from 'client/components/tasks/task';
|
||||
|
||||
import taskDefaults from 'common/script/libs/taskDefaults';
|
||||
import { TAVERN_ID } from '../../../common/script/constants';
|
||||
|
||||
const baseUrl = 'https://habitica.com';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
components: {
|
||||
Task,
|
||||
},
|
||||
mixins: [notificationsMixin],
|
||||
props: ['copyingMessage', 'groupType', 'groupName', 'groupId'],
|
||||
data () {
|
||||
return {
|
||||
isUser: true,
|
||||
task: {},
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('habitica::copy-as-todo', message => {
|
||||
const notes = `${message.user || 'system message'}${message.user ? ' wrote' : ''} in [${this.groupName}](${this.groupPath()})`;
|
||||
const newTask = {
|
||||
text: message.text,
|
||||
type: 'todo',
|
||||
notes,
|
||||
};
|
||||
this.task = taskDefaults(newTask, this.$store.state.user.data);
|
||||
this.$root.$emit('bv::show::modal', 'copyAsTodo');
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
this.$root.$off('habitica::copy-as-todo');
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
createTask: 'tasks:create',
|
||||
}),
|
||||
groupPath () {
|
||||
if (this.groupId === TAVERN_ID) {
|
||||
return `${baseUrl}/groups/tavern`;
|
||||
} else if (this.groupType === 'party') {
|
||||
return `${baseUrl}/party`;
|
||||
} else {
|
||||
return `${baseUrl}/groups/guild/${this.groupId}`;
|
||||
}
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'copyAsTodo');
|
||||
},
|
||||
saveTodo () {
|
||||
this.createTask(this.task);
|
||||
this.text(this.$t('messageAddedAsToDo'));
|
||||
this.$root.$emit('bv::hide::modal', 'copyAsTodo');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,137 @@
|
||||
<template lang="pug">
|
||||
b-modal#report-flag(:title='$t("abuseFlagModalHeading")', size='md', :hide-footer='true')
|
||||
.modal-body
|
||||
strong(v-html="$t('abuseFlagModalHeading', reportData)")
|
||||
blockquote
|
||||
div(v-markdown='abuseObject.text')
|
||||
div
|
||||
strong {{$t('whyReportingPost')}}
|
||||
span.optional {{$t('optional')}}
|
||||
textarea.form-control(v-model='reportComment', :placeholder='$t("whyReportingPostPlaceholder")')
|
||||
small(v-html="$t('abuseFlagModalBody', abuseFlagModalBody)")
|
||||
.footer.text-center
|
||||
button.pull-left.btn.btn-danger(@click='clearFlagCount()', v-if='user.contributor.admin && abuseObject.flagCount > 0')
|
||||
| Reset Flag Count
|
||||
a.cancel-link(@click.prevent='close()') {{ $t('cancel') }}
|
||||
button.btn.btn-danger(@click='reportAbuse()') {{ $t('report') }}
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#report-flag h5 {
|
||||
color: #f23035;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.modal-body {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-radius: 2px;
|
||||
background-color: #f4f4f4;
|
||||
padding: 1em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
textarea {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
border-radius: 2px;
|
||||
border: solid 1px $gray-400;
|
||||
min-height: 106px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 1em;
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
|
||||
a.cancel-link {
|
||||
color: $blue-10;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.optional {
|
||||
color: $gray-200;
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import notifications from 'client/mixins/notifications';
|
||||
import markdownDirective from 'client/directives/markdown';
|
||||
|
||||
export default {
|
||||
mixins: [notifications],
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
reportData () {
|
||||
let reportMessage = this.abuseObject.user;
|
||||
let isSystemMessage = this.abuseObject.uuid === 'system';
|
||||
if (isSystemMessage) reportMessage = this.$t('systemMessage');
|
||||
return {
|
||||
name: `<span class='text-danger'>${reportMessage}</span>`,
|
||||
};
|
||||
},
|
||||
},
|
||||
data () {
|
||||
let abuseFlagModalBody = {
|
||||
firstLinkStart: '<a href="/static/community-guidelines" target="_blank">',
|
||||
secondLinkStart: '<a href="/static/terms" target="_blank">',
|
||||
linkEnd: '</a>',
|
||||
};
|
||||
|
||||
return {
|
||||
abuseFlagModalBody,
|
||||
abuseObject: '',
|
||||
groupId: '',
|
||||
reportComment: '',
|
||||
};
|
||||
},
|
||||
created () {
|
||||
this.$root.$on('habitica::report-chat', this.handleReport);
|
||||
},
|
||||
destroyed () {
|
||||
this.$root.$off('habitica::report-chat', this.handleReport);
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'report-flag');
|
||||
},
|
||||
async reportAbuse () {
|
||||
this.text(this.$t(this.groupId === 'privateMessage' ? 'pmReported' : 'abuseReported'));
|
||||
|
||||
let result = await this.$store.dispatch('chat:flag', {
|
||||
groupId: this.groupId,
|
||||
chatId: this.abuseObject.id,
|
||||
comment: this.reportComment,
|
||||
});
|
||||
|
||||
this.$root.$emit('habitica:report-result', result);
|
||||
|
||||
this.close();
|
||||
},
|
||||
async clearFlagCount () {
|
||||
await this.$store.dispatch('chat:clearFlagCount', {
|
||||
groupId: this.groupId,
|
||||
chatId: this.abuseObject.id,
|
||||
});
|
||||
this.close();
|
||||
},
|
||||
handleReport (data) {
|
||||
if (!data.message || !data.groupId) return;
|
||||
this.abuseObject = data.message;
|
||||
this.groupId = data.groupId;
|
||||
this.reportComment = '';
|
||||
this.$root.$emit('bv::show::modal', 'report-flag');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,963 @@
|
||||
<template lang="pug">
|
||||
b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true', :hide-footer='true', :modal-class="{'page-2':modalPage > 1 && !editing}")
|
||||
.section.row.welcome-section(v-if='modalPage === 1 && !editing')
|
||||
.col-6.offset-3.text-center
|
||||
h3(v-once) {{$t('welcomeTo')}}
|
||||
.svg-icon.logo(v-html='icons.logoPurple')
|
||||
|
||||
.avatar-section.row(v-if='modalPage > 1', :class='{"page-2": modalPage === 2}')
|
||||
.col-6.offset-3
|
||||
.user-creation-bg(v-if='!editing')
|
||||
avatar(:member='user', :avatarOnly='!editing', :class='{"edit-avatar": editing}')
|
||||
|
||||
.section(v-if='modalPage === 2', :class='{"edit-modal": editing}')
|
||||
// @TODO Implement in V2 .section.row
|
||||
.col-12.text-center
|
||||
button.btn.btn-secondary(v-once) {{$t('randomize')}}
|
||||
#options-nav.container.section.text-center.customize-menu
|
||||
.row
|
||||
.menu-container(@click='changeTopPage("body", "size")', :class='{"col-3": !editing, "col-2 offset-1": editing, active: activeTopPage === "body"}')
|
||||
.menu-item
|
||||
.svg-icon(v-html='icons.bodyIcon')
|
||||
strong(v-once) {{$t('bodyBody')}}
|
||||
.indicator
|
||||
.menu-container(@click='changeTopPage("skin", "color")', :class='{"col-3": !editing, "col-2": editing, active: activeTopPage === "skin"}')
|
||||
.menu-item
|
||||
.svg-icon(v-html='icons.skinIcon')
|
||||
strong(v-once) {{$t('skin')}}
|
||||
.indicator
|
||||
.menu-container(@click='changeTopPage("hair", "color")', :class='{"col-3": !editing, "col-2": editing, active: activeTopPage === "hair"}')
|
||||
.menu-item
|
||||
.svg-icon(v-html='icons.hairIcon')
|
||||
strong(v-once) {{$t('hair')}}
|
||||
.indicator
|
||||
.menu-container(@click='changeTopPage("extra", "glasses")', :class='{"col-3": !editing, "col-2": editing, active: activeTopPage === "extra"}')
|
||||
.menu-item
|
||||
.svg-icon(v-html='icons.accessoriesIcon')
|
||||
strong(v-once) {{$t('extra')}}
|
||||
.indicator
|
||||
.menu-container.col-2(@click='changeTopPage("backgrounds", "2019")', v-if='editing', :class='{active: activeTopPage === "backgrounds"}')
|
||||
.menu-item
|
||||
.svg-icon(v-html='icons.backgroundsIcon')
|
||||
strong(v-once) {{$t('backgrounds')}}
|
||||
.indicator
|
||||
|
||||
body-settings(
|
||||
v-if='activeTopPage === "body"',
|
||||
:editing="editing",
|
||||
)
|
||||
|
||||
skin-settings(
|
||||
v-if='activeTopPage === "skin"',
|
||||
:editing="editing",
|
||||
)
|
||||
|
||||
hairSettings(
|
||||
v-if='activeTopPage === "hair"',
|
||||
:editing="editing"
|
||||
)
|
||||
|
||||
extraSettings(
|
||||
v-if='activeTopPage === "extra"',
|
||||
:editing="editing"
|
||||
)
|
||||
|
||||
#backgrounds.section.container.customize-section(v-if='activeTopPage === "backgrounds"')
|
||||
.row.title-row
|
||||
toggle-switch.backgroundFilterToggle(:label="'Hide locked backgrounds'", v-model='filterBackgrounds')
|
||||
.row.text-center.title-row(v-if='!filterBackgrounds')
|
||||
strong {{backgroundShopSets[0].text}}
|
||||
.row.title-row(v-if='!filterBackgrounds')
|
||||
.col-12(v-if='showPlainBackgroundBlurb(backgroundShopSets[0].identifier, backgroundShopSets[0].items)') {{ $t('incentiveBackgroundsUnlockedWithCheckins') }}
|
||||
.col-2(v-for='bg in backgroundShopSets[0].items',
|
||||
@click='unlock("background." + bg.key)',
|
||||
:popover-title='bg.text',
|
||||
:popover='bg.notes',
|
||||
popover-trigger='mouseenter')
|
||||
.incentive-background(:class='[`background_${bg.key}`]')
|
||||
.small-rectangle
|
||||
sub-menu.text-center(:items="bgSubMenuItems", :activeSubPage="activeSubPage", @changeSubPage="changeSubPage($event)")
|
||||
.row.customize-menu(v-if='!filterBackgrounds' v-for='(sets, key) in backgroundShopSetsByYear')
|
||||
.row.background-set(v-for='set in sets', v-if='activeSubPage === key')
|
||||
.col-8.offset-2.text-center.set-title
|
||||
strong {{set.text}}
|
||||
.col-4.text-center.customize-option.background-button(v-for='bg in set.items',
|
||||
@click='!user.purchased.background[bg.key] ? backgroundSelected(bg) : unlock("background." + bg.key)',
|
||||
:popover-title='bg.text',
|
||||
:popover='bg.notes',
|
||||
popover-trigger='mouseenter')
|
||||
.background(:class='[`background_${bg.key}`, backgroundLockedStatus(bg.key)]')
|
||||
i.glyphicon.glyphicon-lock(v-if='!user.purchased.background[bg.key]')
|
||||
.purchase-background.single(v-if='!user.purchased.background[bg.key]')
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span.price 7
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
:class="{'item-selected-badge': isBackgroundPinned(bg), 'hide': !isBackgroundPinned(bg)}",
|
||||
@click.prevent.stop="togglePinned(bg)",
|
||||
v-if='!user.purchased.background[bg.key]'
|
||||
)
|
||||
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
|
||||
.purchase-background.set(v-if='!ownsSet("background", set.items) && set.identifier !== "incentiveBackgrounds"' @click='unlock(setKeys("background", set.items))')
|
||||
span.label {{ $t('purchaseAll') }}
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span.price 15
|
||||
.row.customize-menu(v-if='filterBackgrounds')
|
||||
.col-4.text-center.customize-option.background-button(v-for='(bg) in ownedBackgrounds'
|
||||
@click='unlock("background." + bg.key)',
|
||||
:popover-title='bg.text',
|
||||
:popover='bg.notes',
|
||||
popover-trigger='mouseenter')
|
||||
.background(:class='[`background_${bg.key}`, backgroundLockedStatus(bg.key)]'
|
||||
)
|
||||
|
||||
.container.interests-section(v-if='modalPage === 3 && !editing')
|
||||
.section.row
|
||||
.col-12.text-center
|
||||
h2 {{ $t('wantToWorkOn') }}
|
||||
.section.row
|
||||
.col-6
|
||||
.task-option
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input#work(type="checkbox", value='work', v-model='taskCategories')
|
||||
label.custom-control-label(v-once, for="work") {{ $t('work') }}
|
||||
.task-option
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input#excercise(type="checkbox", value='exercise', v-model='taskCategories')
|
||||
label.custom-control-label(v-once, for="excercise") {{ $t('exercise') }}
|
||||
.task-option
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input#health_wellness(type="checkbox", value='health_wellness', v-model='taskCategories')
|
||||
label.custom-control-label(v-once, for="health_wellness") {{ $t('health_wellness') }}
|
||||
.task-option
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input#school(type="checkbox", value='school', v-model='taskCategories')
|
||||
label.custom-control-label(v-once, for="school") {{ $t('school') }}
|
||||
.col-6
|
||||
.task-option
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input#chores(type="checkbox", value='chores', v-model='taskCategories')
|
||||
label.custom-control-label(v-once, for="chores") {{ $t('chores') }}
|
||||
.task-option
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input#creativity(type="checkbox", value='creativity', v-model='taskCategories')
|
||||
label.custom-control-label(v-once, for="creativity") {{ $t('creativity') }}
|
||||
.task-option
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input#self_care(type="checkbox", value='self_care', v-model='taskCategories')
|
||||
label.custom-control-label(v-once, for="self_care") {{ $t('self_care') }}
|
||||
|
||||
.section.d-flex.justify-content-center.justin-outer-section(:class='{top: modalPage > 1}', v-if='!editing')
|
||||
.justin-section.d-flex.align-items-center
|
||||
.featured-label
|
||||
span.rectangle
|
||||
span.text Justin
|
||||
span.rectangle
|
||||
.justin-message
|
||||
.corner-decoration(:style="{top: '-2px', right: '-2px'}")
|
||||
.corner-decoration(:style="{top: '-2px', left: '-2px'}")
|
||||
.corner-decoration(:style="{bottom: '-2px', right: '-2px'}")
|
||||
.corner-decoration(:style="{bottom: '-2px', left: '-2px'}")
|
||||
div(v-if='modalPage === 1')
|
||||
p(v-once, v-html='$t("justinIntroMessage1")')
|
||||
p(v-once) {{ $t('justinIntroMessageUsername') }}
|
||||
div(v-if='modalPage === 2')
|
||||
p {{ $t('justinIntroMessageAppearance') }}
|
||||
div(v-if='modalPage === 3')
|
||||
p(v-once) {{ $t('justinIntroMessage3') }}
|
||||
.npc-justin-textbox
|
||||
.section.mr-5.ml-5.first-page-footer(v-if='modalPage === 1')
|
||||
username-form(@usernameConfirmed='modalPage += 1', :avatarIntro='true')
|
||||
.small.text-center(v-html="$t('usernameTOSRequirements')")
|
||||
|
||||
.section.container.footer(v-if='!editing && !(modalPage === 1)')
|
||||
.footer-left
|
||||
div.prev-outer(v-if='modalPage > 1', @click='prev()')
|
||||
.prev-arrow.svg-icon(v-html='icons.arrowLeft')
|
||||
.prev(v-once) {{ $t('prev') }}
|
||||
.footer-center.text-center.circles
|
||||
.circle(:class="{active: modalPage === 1}")
|
||||
.circle(:class="{active: modalPage === 2}")
|
||||
.circle(:class="{active: modalPage === 3}")
|
||||
.footer-right
|
||||
div.next-outer(v-if='modalPage < 3', @click='next()')
|
||||
.next(v-once) {{$t('next')}}
|
||||
.next-arrow.svg-icon(v-html='icons.arrowRight')
|
||||
div.next-outer(v-if='modalPage === 3 && !loading', @click='done()', :class="{disabled: taskCategories.length === 0}")
|
||||
.next(v-once) {{$t('finish')}}
|
||||
.next-arrow.svg-icon(v-html='icons.arrowRight')
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
$dialogMarginTop: 56px;
|
||||
$userCreationBgHeight: 105px;
|
||||
|
||||
/* @TODO do not rely on avatar-modal___BV_modal_body_,
|
||||
it already changed once when bootstrap-vue reached version 1 */
|
||||
|
||||
#avatar-modal___BV_modal_body_, #avatar-modal___BV_modal_body_ {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-2 {
|
||||
#avatar-modal___BV_modal_body_ {
|
||||
margin-top: $dialogMarginTop;
|
||||
}
|
||||
|
||||
&#avatar-modal {
|
||||
.modal-dialog.modal-md {
|
||||
margin-top: 186px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#avatar-modal {
|
||||
|
||||
.first-page-footer {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.customize-section {
|
||||
text-align: center;
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
|
||||
#creator-background {
|
||||
background-color: $purple-200;
|
||||
}
|
||||
|
||||
.corner-decoration {
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #ffbe5d;
|
||||
border: inherit;
|
||||
outline: inherit;
|
||||
}
|
||||
|
||||
.small {
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
.purchase-all {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.edit-modal {
|
||||
margin-top: 10em;
|
||||
}
|
||||
|
||||
.row.sub-menu + .row.sub-menu {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
margin-top: 2.5em;
|
||||
margin-bottom: 2.5em;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 190px;
|
||||
margin: 0 auto 1.25em;
|
||||
}
|
||||
|
||||
.user-creation-bg {
|
||||
background-image: url('~client/assets/creator/creator-hills-bg.png');
|
||||
height: $userCreationBgHeight;
|
||||
width: 219px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: absolute !important; // was overwritten in production build
|
||||
top: -22px;
|
||||
left: 4em;
|
||||
}
|
||||
|
||||
.top {
|
||||
position: absolute;
|
||||
top: -($dialogMarginTop + $userCreationBgHeight - 16px);
|
||||
right: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.justin-outer-section:not(.top) {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.avatar-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.edit-avatar {
|
||||
left: 9.2em;
|
||||
}
|
||||
|
||||
.justin-section {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.justin-message {
|
||||
border-color: #ffa623;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
outline-color: #b36213;
|
||||
outline-style: solid;
|
||||
outline-width: 2px;
|
||||
position: relative;
|
||||
padding: 2em;
|
||||
margin: 2px;
|
||||
height: 100%;
|
||||
width: 400px;
|
||||
background: $gray-700;
|
||||
|
||||
p {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
p + p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.npc-justin-textbox {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: -3.1rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background-image: url('~client/assets/images/justin_textbox.png');
|
||||
}
|
||||
|
||||
.featured-label {
|
||||
position: absolute;
|
||||
top: -1rem;
|
||||
left: 1.5rem;
|
||||
border-radius: 2px;
|
||||
margin: auto;
|
||||
|
||||
.text {
|
||||
font-size: 12px;
|
||||
min-height: auto;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.circles {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.circle {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #d8d8d8;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 1em;
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.circle.active {
|
||||
background-color: $purple-300;
|
||||
}
|
||||
|
||||
.customize-menu {
|
||||
.menu-item .svg-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.menu-container {
|
||||
color: #a5a1ac;
|
||||
}
|
||||
|
||||
.menu-container:hover, .menu-container.active {
|
||||
cursor: pointer;
|
||||
color: $purple-300;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu-container.active .indicator{
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
border-left: 12px solid transparent;
|
||||
border-right: 12px solid transparent;
|
||||
border-bottom: 12px solid $gray-700;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.text-center {
|
||||
.gem-lock, .gold-lock {
|
||||
display: inline-block;
|
||||
margin-right: 1em;
|
||||
margin-bottom: 1.6em;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.gem-lock, .gold-lock {
|
||||
.svg-icon {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.svg-icon, span {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.gem-lock span {
|
||||
color: $green-10
|
||||
}
|
||||
|
||||
.gold-lock span {
|
||||
color: $yellow-10
|
||||
}
|
||||
|
||||
.customize-section {
|
||||
background-color: #f9f9f9;
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.interests-section {
|
||||
margin-top: 3em;
|
||||
margin-bottom: 60px;
|
||||
|
||||
.task-option {
|
||||
margin: 0 auto;
|
||||
width: 70%;
|
||||
}
|
||||
}
|
||||
|
||||
#backgrounds {
|
||||
padding-top: 12px;
|
||||
|
||||
.title-row {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.backgroundFilterToggle {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.set-title {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.background {
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
strong {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.incentive-background {
|
||||
background-image: none;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
border-radius: 8px;
|
||||
background-color: #92b6bd;
|
||||
margin: 0 auto;
|
||||
padding-top: .3em;
|
||||
|
||||
.small-rectangle {
|
||||
width: 60px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
margin: 0 auto;
|
||||
opacity: .6;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
.background_violet {
|
||||
background-color: #a993ed;
|
||||
}
|
||||
|
||||
.background_blue {
|
||||
background-color: #92b6bd;
|
||||
}
|
||||
|
||||
.background_green {
|
||||
background-color: #92bd94;
|
||||
}
|
||||
|
||||
.background_purple {
|
||||
background-color: #9397bd;
|
||||
}
|
||||
|
||||
.background_red {
|
||||
background-color: #b77e80;
|
||||
}
|
||||
|
||||
.background_yellow {
|
||||
background-color: #bcbb91;
|
||||
}
|
||||
|
||||
.incentive-background:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.background:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.purchase-background {
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
padding: 0.5em;
|
||||
border-radius: 0 0 2px 2px;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
cursor: pointer;
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
span.price {
|
||||
color: #24cc8f;
|
||||
}
|
||||
|
||||
.gem, .coin {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
&.single {
|
||||
width: 141px;
|
||||
}
|
||||
|
||||
&.set {
|
||||
width: 100%;
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.gem, .coin {
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gem, .coin {
|
||||
margin: 0 .5em;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.background-set {
|
||||
width: 100%;
|
||||
margin: 10px;
|
||||
background-color: #edecee;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
* {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.footer-left, .footer-right {
|
||||
flex: auto;
|
||||
flex-grow: 0;
|
||||
width: 30%
|
||||
}
|
||||
|
||||
.footer-center {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.prev, .next {
|
||||
color: $gray-300;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
.prev {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.next {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.prev-outer {
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
.prev {
|
||||
color: $purple-300;
|
||||
}
|
||||
|
||||
.prev-arrow {
|
||||
path {
|
||||
fill: $purple-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prev-arrow, .next-arrow {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.next-outer {
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
.next {
|
||||
color: $purple-300;
|
||||
}
|
||||
|
||||
.next-arrow {
|
||||
path {
|
||||
fill: $purple-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.badge-svg {
|
||||
left: calc((100% - 18px) / 2);
|
||||
cursor: pointer;
|
||||
color: $gray-400;
|
||||
background: $white;
|
||||
padding: 4.5px 6px;
|
||||
|
||||
&.item-selected-badge {
|
||||
background: $purple-300;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-12 {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
span.badge.badge-pill.badge-item.badge-svg:not(.item-selected-badge) {
|
||||
color: #a5a1ac;
|
||||
}
|
||||
|
||||
span.badge.badge-pill.badge-item.badge-svg.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.background-button {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.background-button:hover {
|
||||
span.badge.badge-pill.badge-item.badge-svg.hide {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import map from 'lodash/map';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import avatar from './avatar';
|
||||
import usernameForm from './settings/usernameForm';
|
||||
import { getBackgroundShopSets } from '../../common/script/libs/shops';
|
||||
import guide from 'client/mixins/guide';
|
||||
import notifications from 'client/mixins/notifications';
|
||||
import toggleSwitch from 'client/components/ui/toggleSwitch';
|
||||
import bodySettings from './avatarModal/body-settings';
|
||||
import skinSettings from './avatarModal/skin-settings';
|
||||
import hairSettings from './avatarModal/hair-settings';
|
||||
import extraSettings from './avatarModal/extra-settings';
|
||||
import subMenu from './avatarModal/sub-menu';
|
||||
|
||||
import logoPurple from 'assets/svg/logo-purple.svg';
|
||||
import bodyIcon from 'assets/svg/body.svg';
|
||||
import accessoriesIcon from 'assets/svg/accessories.svg';
|
||||
import skinIcon from 'assets/svg/skin.svg';
|
||||
import hairIcon from 'assets/svg/hair.svg';
|
||||
import backgroundsIcon from 'assets/svg/backgrounds.svg';
|
||||
import gem from 'assets/svg/gem.svg';
|
||||
import gold from 'assets/svg/gold.svg';
|
||||
import pin from 'assets/svg/pin.svg';
|
||||
import arrowRight from 'assets/svg/arrow_right.svg';
|
||||
import arrowLeft from 'assets/svg/arrow_left.svg';
|
||||
import isPinned from 'common/script/libs/isPinned';
|
||||
import {avatarEditorUtilies} from '../mixins/avatarEditUtilities';
|
||||
|
||||
import content from 'common/script/content/index';
|
||||
|
||||
export default {
|
||||
mixins: [guide, notifications, avatarEditorUtilies],
|
||||
components: {
|
||||
avatar,
|
||||
toggleSwitch,
|
||||
usernameForm,
|
||||
bodySettings,
|
||||
skinSettings,
|
||||
hairSettings,
|
||||
extraSettings,
|
||||
|
||||
subMenu,
|
||||
},
|
||||
mounted () {
|
||||
if (this.editing) this.modalPage = 2;
|
||||
// Buy modal is global, so we listen at root. I'd like to not
|
||||
this.$root.$on('buyModal::boughtItem', this.backgroundPurchased);
|
||||
},
|
||||
data () {
|
||||
let backgroundShopSets = getBackgroundShopSets();
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
backgroundShopSets,
|
||||
backgroundUpdate: new Date(),
|
||||
filterBackgrounds: false,
|
||||
|
||||
icons: Object.freeze({
|
||||
logoPurple,
|
||||
bodyIcon,
|
||||
accessoriesIcon,
|
||||
skinIcon,
|
||||
hairIcon,
|
||||
backgroundsIcon,
|
||||
gem,
|
||||
pin,
|
||||
gold,
|
||||
arrowRight,
|
||||
arrowLeft,
|
||||
}),
|
||||
modalPage: 1,
|
||||
activeTopPage: 'body',
|
||||
activeSubPage: 'size',
|
||||
taskCategories: [],
|
||||
skinSubMenuItems: [
|
||||
{
|
||||
id: 'color',
|
||||
label: this.$t('color'),
|
||||
},
|
||||
],
|
||||
|
||||
bgSubMenuItems: ['2019', '2018', '2017', '2016', '2015', '2014'].map(y =>
|
||||
({
|
||||
id: y,
|
||||
label: y,
|
||||
})
|
||||
),
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
editing () {
|
||||
if (this.editing) this.modalPage = 2;
|
||||
},
|
||||
startingPage () {
|
||||
if (!this.$store.state.avatarEditorOptions.startingPage) return;
|
||||
this.activeTopPage = this.$store.state.avatarEditorOptions.startingPage;
|
||||
this.activeSubPage = this.$store.state.avatarEditorOptions.subpage;
|
||||
this.$store.state.avatarEditorOptions.startingPage = '';
|
||||
this.$store.state.avatarEditorOptions.subpage = '';
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
|
||||
|
||||
editing () {
|
||||
return this.$store.state.avatarEditorOptions.editingUser;
|
||||
},
|
||||
startingPage () {
|
||||
return this.$store.state.avatarEditorOptions.startingPage;
|
||||
},
|
||||
backgroundShopSetsByYear () {
|
||||
// @TODO: add dates to backgrounds
|
||||
let backgroundShopSetsByYear = {
|
||||
2014: [],
|
||||
2015: [],
|
||||
2016: [],
|
||||
2017: [],
|
||||
2018: [],
|
||||
2019: [],
|
||||
};
|
||||
|
||||
// Hack to force update for now until we restructure the data
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
|
||||
this.backgroundShopSets.forEach((set) => {
|
||||
let year = set.identifier.substr(set.identifier.length - 4);
|
||||
if (!backgroundShopSetsByYear[year]) return;
|
||||
|
||||
let setOwnedByUser = false;
|
||||
for (let key in set.items) {
|
||||
if (this.user.purchased.background[key]) setOwnedByUser = true;
|
||||
}
|
||||
set.userOwns = setOwnedByUser;
|
||||
|
||||
backgroundShopSetsByYear[year].push(set);
|
||||
});
|
||||
return backgroundShopSetsByYear;
|
||||
},
|
||||
ownedBackgrounds () {
|
||||
let ownedBackgrounds = [];
|
||||
|
||||
// Hack to force update for now until we restructure the data
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
|
||||
this.backgroundShopSets.forEach((set) => {
|
||||
set.items.forEach((bg) => {
|
||||
if (this.user.purchased.background[bg.key]) {
|
||||
ownedBackgrounds.push(bg);
|
||||
}
|
||||
});
|
||||
});
|
||||
return ownedBackgrounds;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
purchase (type, key) {
|
||||
this.$store.dispatch('shops:purchase', {
|
||||
type,
|
||||
key,
|
||||
});
|
||||
this.backgroundUpdate = new Date();
|
||||
},
|
||||
prev () {
|
||||
this.modalPage -= 1;
|
||||
},
|
||||
next () {
|
||||
this.modalPage += 1;
|
||||
},
|
||||
changeTopPage (page, subpage) {
|
||||
this.activeTopPage = page;
|
||||
if (subpage) this.activeSubPage = subpage;
|
||||
},
|
||||
changeSubPage (page) {
|
||||
this.activeSubPage = page;
|
||||
},
|
||||
async done () {
|
||||
this.loading = true;
|
||||
|
||||
let tasksToCreate = [
|
||||
...content.tasksByCategory.defaults.map(t => ({
|
||||
...t,
|
||||
text: t.text(),
|
||||
notes: t.notes && t.notes(),
|
||||
})),
|
||||
];
|
||||
this.taskCategories.forEach(category => {
|
||||
tasksToCreate = tasksToCreate.concat(content.tasksByCategory[category].map(t => ({
|
||||
...t,
|
||||
text: t.text(),
|
||||
notes: t.notes && t.notes(),
|
||||
})));
|
||||
});
|
||||
|
||||
// @TODO: Move to the action
|
||||
let response = await axios.post('/api/v4/tasks/user', tasksToCreate);
|
||||
let tasks = response.data.data;
|
||||
tasks.forEach(task => {
|
||||
this.$store.state.user.data.tasksOrder[`${task.type}s`].unshift(task._id);
|
||||
this.$store.state.tasks.data[`${task.type}s`].unshift(task);
|
||||
});
|
||||
|
||||
this.$root.$emit('bv::hide::modal', 'avatar-modal');
|
||||
this.$router.push('/');
|
||||
this.$store.dispatch('user:set', {
|
||||
'flags.welcomed': true,
|
||||
});
|
||||
|
||||
// @TODO: This is a timeout to ensure dom is loaded
|
||||
window.setTimeout(() => {
|
||||
this.initTour();
|
||||
this.goto('intro', 0);
|
||||
}, 1000);
|
||||
},
|
||||
showPlainBackgroundBlurb (identifier, set) {
|
||||
return identifier === 'incentiveBackgrounds' && !this.ownsSet('background', set);
|
||||
},
|
||||
ownsSet (type, set) {
|
||||
let setOwnedByUser = false;
|
||||
|
||||
for (let key in set) {
|
||||
let value = set[key];
|
||||
if (type === 'background') key = value.key;
|
||||
if (this.user.purchased[type][key]) setOwnedByUser = true;
|
||||
}
|
||||
|
||||
return setOwnedByUser;
|
||||
},
|
||||
setKeys (type, _set) {
|
||||
return map(_set, (v, k) => {
|
||||
if (type === 'background') k = v.key;
|
||||
return `${type}.${k}`;
|
||||
}).join(',');
|
||||
},
|
||||
backgroundLockedStatus (bgKey) {
|
||||
let backgroundClass = 'background-locked';
|
||||
if (this.user.purchased.background[bgKey]) backgroundClass = 'background-unlocked';
|
||||
return backgroundClass;
|
||||
},
|
||||
isBackgroundPinned (bg) {
|
||||
return isPinned(this.user, bg);
|
||||
},
|
||||
togglePinned (bg) {
|
||||
if (!this.$store.dispatch('user:togglePinnedItem', {type: bg.pinType, path: bg.path})) {
|
||||
this.text(this.$t('unpinnedItem', {item: bg.text}));
|
||||
}
|
||||
},
|
||||
backgroundSelected (bg) {
|
||||
this.$root.$emit('buyModal::showItem', bg);
|
||||
},
|
||||
backgroundPurchased () {
|
||||
this.backgroundUpdate = new Date();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,4 @@
|
||||
<template lang="pug">
|
||||
// Container component used when you only need an empty view to support children routes
|
||||
router-view
|
||||
</template>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template lang="pug">
|
||||
.row.standard-page(v-if='groupIsSubscribed && isLeader')
|
||||
.col-12.col-md-6.offset-md-3
|
||||
table.table.alert.alert-info
|
||||
tr(v-if='group.purchased.plan.dateTerminated')
|
||||
td.alert.alert-warning
|
||||
span.noninteractive-button.btn-danger {{ $t('canceledGroupPlan') }}
|
||||
i.glyphicon.glyphicon-time {{ $t('groupPlanCanceled') }}
|
||||
strong {{dateTerminated}}
|
||||
tr(v-if='!group.purchased.plan.dateTerminated')
|
||||
td
|
||||
h3 {{ $t('paymentDetails') }}
|
||||
p(v-if='group.purchased.plan.planId') {{ $t('groupSubscriptionPrice') }}
|
||||
tr(v-if='group.purchased.plan.extraMonths')
|
||||
td
|
||||
span.glyphicon.glyphicon-credit-card
|
||||
| {{ $t('purchasedGroupPlanPlanExtraMonths', purchasedGroupPlanPlanExtraMonths) }}
|
||||
tr(v-if='group.purchased.plan.consecutive.count || group.purchased.plan.consecutive.offset')
|
||||
td
|
||||
span.glyphicon.glyphicon-forward
|
||||
| {{ $t('consecutiveSubscription') }}
|
||||
ul.list-unstyled
|
||||
li {{ $t('consecutiveMonths') }} {{group.purchased.plan.consecutive.count + group.purchased.plan.consecutive.offset}}
|
||||
li {{ $t('gemCapExtra') }} {{group.purchased.plan.consecutive.gemCapExtra}}
|
||||
li {{ $t('mysticHourglasses') }} {{group.purchased.plan.consecutive.trinkets}}
|
||||
.col-12.col-md-6.offset-md-3
|
||||
button.btn.btn-success(class='btn-success', v-if='group.purchased.plan.dateTerminated', @click='upgradeGroup()')
|
||||
| {{ $t('upgrade') }}
|
||||
.btn.btn-primary(v-if='!group.purchased.plan.dateTerminated && group.purchased.plan.paymentMethod === "Stripe"',
|
||||
@click='showStripeEdit({groupId: group.id})') {{ $t('subUpdateCard') }}
|
||||
.btn.btn-sm.btn-danger(v-if='!group.purchased.plan.dateTerminated',
|
||||
@click='cancelSubscriptionConfirm({group: group})') {{ $t('cancelGroupSub') }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import paymentsMixin from 'client/mixins/payments';
|
||||
import { CONSTANTS, getLocalSetting, removeLocalSetting } from 'client/libs/userlocalManager';
|
||||
|
||||
export default {
|
||||
mixins: [paymentsMixin],
|
||||
props: ['groupId'],
|
||||
data () {
|
||||
return {
|
||||
group: {},
|
||||
};
|
||||
},
|
||||
async mounted () {
|
||||
await this.loadGroup();
|
||||
|
||||
let appState = getLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE);
|
||||
if (appState) {
|
||||
appState = JSON.parse(appState);
|
||||
if (appState.groupPlanCanceled) {
|
||||
removeLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE);
|
||||
this.$root.$emit('habitica:subscription-canceled', {
|
||||
dateTerminated: this.dateTerminated,
|
||||
isGroup: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
isLeader () {
|
||||
return this.user._id === this.group.leader._id;
|
||||
},
|
||||
groupIsSubscribed () {
|
||||
return this.group.purchased && this.group.purchased.plan && this.group.purchased.plan.customerId;
|
||||
},
|
||||
dateTerminated () {
|
||||
if (!this.user.preferences || !this.user.preferences.dateFormat) return moment(this.group.purchased.plan.dateTerminated);
|
||||
return moment(this.group.purchased.plan.dateTerminated).format(this.user.preferences.dateFormat.toUpperCase());
|
||||
},
|
||||
purchasedGroupPlanPlanExtraMonths () {
|
||||
return {
|
||||
months: parseFloat(this.group.purchased.plan.extraMonths).toFixed(2),
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async loadGroup () {
|
||||
let group = await this.$store.dispatch('guilds:getGroup', {groupId: this.groupId});
|
||||
this.group = Object.assign({}, group);
|
||||
},
|
||||
upgradeGroup () {
|
||||
this.$store.state.upgradingGroup = this.group;
|
||||
this.$router.push('/group-plans');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,156 @@
|
||||
<template lang="pug">
|
||||
.create-group-modal-pages
|
||||
.col-12(v-if='activePage === PAGES.CREATE_GROUP')
|
||||
h2 {{ $t('nameYourGroup') }}
|
||||
.form-group
|
||||
label.control-label(for='new-group-name') {{ $t('name') }}
|
||||
input.form-control#new-group-name.input-medium.option-content(required, type='text', :placeholder="$t('exampleGroupName')", v-model='newGroup.name')
|
||||
.form-group
|
||||
label(for='new-group-description') {{ $t('description') }}
|
||||
textarea.form-control#new-group-description.option-content(cols='3', :placeholder="$t('exampleGroupDesc')", v-model='newGroup.description')
|
||||
.form-group.text-left(v-if='newGroup.type === "guild"')
|
||||
.custom-control.custom-radio
|
||||
input.custom-control-input(type='radio', name='new-group-privacy', value='private', v-model='newGroup.privacy')
|
||||
label.custom-control-label {{ $t('thisGroupInviteOnly') }}
|
||||
.form-group.text-left
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input(type='checkbox', v-model='newGroup.leaderOnly.challenges' id='create-group-leaderOnlyChallenges-checkbox')
|
||||
label.custom-control-label(for='create-group-leaderOnlyChallenges-checkbox') {{ $t('leaderOnlyChallenges') }}
|
||||
.form-group(v-if='newGroup.type === "party"')
|
||||
button.btn.btn-secondary.form-control(@click='createGroup()', :value="$t('createGroupPlan')")
|
||||
.form-group
|
||||
button.btn.btn-primary.btn-lg.btn-block(@click="createGroup()", :disabled="!newGroupIsReady") {{ $t('createGroupPlan') }}
|
||||
.col-12(v-if='activePage === PAGES.PAY')
|
||||
h2 {{ $t('choosePaymentMethod') }}
|
||||
.payments-column
|
||||
button.purchase.btn.btn-primary.payment-button.payment-item(@click='pay(PAYMENTS.STRIPE)')
|
||||
.svg-icon.credit-card-icon(v-html="icons.creditCardIcon")
|
||||
| {{ $t('card') }}
|
||||
amazon-button.payment-item(:amazon-data="pay(PAYMENTS.AMAZON)")
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h2 {
|
||||
font-family: 'Varela Round', sans-serif;
|
||||
font-weight: normal;
|
||||
font-size: 29px;
|
||||
color: #34313a;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.box {
|
||||
border-radius: 2px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
padding: 2em;
|
||||
text-align: center;
|
||||
vertical-align: bottom;
|
||||
height: 100px;
|
||||
width: 306px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.box .svg-icon {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.box:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import paymentsMixin from '../../mixins/payments';
|
||||
import amazonButton from 'client/components/payments/amazonButton';
|
||||
|
||||
import creditCardIcon from 'assets/svg/credit-card-icon.svg';
|
||||
|
||||
export default {
|
||||
mixins: [paymentsMixin],
|
||||
components: {
|
||||
amazonButton,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
amazonPayments: {},
|
||||
icons: Object.freeze({
|
||||
creditCardIcon,
|
||||
}),
|
||||
PAGES: {
|
||||
CREATE_GROUP: 'create-group',
|
||||
UPGRADE_GROUP: 'upgrade-group',
|
||||
PAY: 'pay',
|
||||
},
|
||||
PAYMENTS: {
|
||||
AMAZON: 'amazon',
|
||||
STRIPE: 'stripe',
|
||||
},
|
||||
activePage: 'create-group',
|
||||
newGroup: {
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
name: '',
|
||||
leaderOnly: {
|
||||
challenges: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
newGroupIsReady () {
|
||||
return Boolean(this.newGroup.name);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changePage (page) {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'group-plans-static',
|
||||
eventAction: 'view',
|
||||
eventLabel: page,
|
||||
});
|
||||
this.activePage = page;
|
||||
window.scrollTo(0, 0);
|
||||
},
|
||||
createGroup () {
|
||||
this.changePage(this.PAGES.PAY);
|
||||
},
|
||||
pay (paymentMethod) {
|
||||
const subscriptionKey = 'group_monthly';
|
||||
let paymentData = {
|
||||
subscription: subscriptionKey,
|
||||
coupon: null,
|
||||
};
|
||||
|
||||
if (this.upgradingGroup && this.upgradingGroup._id) {
|
||||
paymentData.groupId = this.upgradingGroup._id;
|
||||
paymentData.group = this.upgradingGroup;
|
||||
} else {
|
||||
paymentData.groupToCreate = this.newGroup;
|
||||
}
|
||||
|
||||
this.paymentMethod = paymentMethod;
|
||||
if (this.paymentMethod === this.PAYMENTS.STRIPE) {
|
||||
this.showStripe(paymentData);
|
||||
} else if (this.paymentMethod === this.PAYMENTS.AMAZON) {
|
||||
paymentData.type = 'subscription';
|
||||
return paymentData;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,235 @@
|
||||
<template lang="pug">
|
||||
b-modal#group-plan-overview(title="Empty", size='lg', hide-footer=true, @shown='shown()')
|
||||
.header-wrap.text-center(slot="modal-header")
|
||||
h2(v-once) {{ $t('gettingStarted') }}
|
||||
p(v-once) {{ $t('congratsOnGroupPlan') }}
|
||||
.row
|
||||
.col-12
|
||||
.card(:class='{expanded: expandedQuestions.question1}')
|
||||
.question-head
|
||||
.q Q.
|
||||
.title {{ $t('whatsIncludedGroup') }}
|
||||
.arrow.float-right(@click='toggle("question1")')
|
||||
.svg-icon(v-html="icons.upIcon", v-if='expandedQuestions.question1')
|
||||
.svg-icon(v-html="icons.downIcon", v-else)
|
||||
.question-body(v-if='expandedQuestions.question1')
|
||||
p {{ $t('whatsIncludedGroupDesc') }}
|
||||
.col-12
|
||||
.card(:class='{expanded: expandedQuestions.question2}')
|
||||
.question-head
|
||||
.q Q.
|
||||
.title {{ $t('howDoesBillingWork') }}
|
||||
.arrow.float-right(@click='toggle("question2")')
|
||||
.svg-icon(v-html="icons.upIcon", v-if='expandedQuestions.question2')
|
||||
.svg-icon(v-html="icons.downIcon", v-else)
|
||||
.question-body(v-if='expandedQuestions.question2')
|
||||
p {{ $t('howDoesBillingWorkDesc') }}
|
||||
.col-12
|
||||
.card(:class='{expanded: expandedQuestions.question3}')
|
||||
.question-head
|
||||
.q Q.
|
||||
.title {{ $t('howToAssignTask') }}
|
||||
.arrow.float-right(@click='toggle("question3")')
|
||||
.svg-icon(v-html="icons.upIcon", v-if='expandedQuestions.question3')
|
||||
.svg-icon(v-html="icons.downIcon", v-else)
|
||||
.question-body(v-if='expandedQuestions.question3')
|
||||
p {{ $t('howToAssignTaskDesc') }}
|
||||
.assign-tasks.image-example
|
||||
.col-12
|
||||
.card(:class='{expanded: expandedQuestions.question4}')
|
||||
.question-head
|
||||
.q Q.
|
||||
.title {{ $t('howToRequireApproval') }}
|
||||
.arrow.float-right(@click='toggle("question4")')
|
||||
.svg-icon(v-html="icons.upIcon", v-if='expandedQuestions.question4')
|
||||
.svg-icon(v-html="icons.downIcon", v-else)
|
||||
.question-body(v-if='expandedQuestions.question4')
|
||||
p {{ $t('howToRequireApprovalDesc') }}
|
||||
.requires-approval.image-example
|
||||
p {{ $t('howToRequireApprovalDesc2') }}
|
||||
.approval-requested.image-example
|
||||
.col-12
|
||||
.card(:class='{expanded: expandedQuestions.question5}')
|
||||
.question-head
|
||||
.q Q.
|
||||
.title {{ $t('whatIsGroupManager') }}
|
||||
.arrow.float-right(@click='toggle("question5")')
|
||||
.svg-icon(v-html="icons.upIcon", v-if='expandedQuestions.question5')
|
||||
.svg-icon(v-html="icons.downIcon", v-else)
|
||||
.question-body(v-if='expandedQuestions.question5')
|
||||
p {{ $t('whatIsGroupManagerDesc') }}
|
||||
.promote-leader.image-example
|
||||
.col-12.text-center
|
||||
button.btn.btn-primary.close-button(@click='close()') {{ $t('goToTaskBoard') }}
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#group-plan-overview___BV_modal_header_ {
|
||||
border-bottom: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import url('https://fonts.googleapis.com/css?family=Varela+Round');
|
||||
|
||||
.header-wrap {
|
||||
padding-left: 4em;
|
||||
padding-right: 4em;
|
||||
|
||||
h2 {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #878190;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
margin-bottom: 2em;
|
||||
|
||||
.col-12 {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.card.expanded {
|
||||
padding-bottom: 1em;
|
||||
|
||||
.title {
|
||||
color: #4f2a93;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
min-height: 60px;
|
||||
border-radius: 4px;
|
||||
background-color: #ffffff;
|
||||
border: none;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
|
||||
.question-head {
|
||||
.q {
|
||||
font-family: 'Varela Round', sans-serif;
|
||||
font-size: 20px;
|
||||
color: #a5a1ac;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
margin: 1em;
|
||||
padding-top: .9em;
|
||||
|
||||
.svg-icon {
|
||||
width: 26px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.question-body {
|
||||
padding-left: 4.4em;
|
||||
padding-right: 4em;
|
||||
|
||||
p {
|
||||
color: #4e4a57;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-example {
|
||||
background-repeat: no-repeat;
|
||||
margin: 0 auto;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.assign-tasks {
|
||||
background-image: url('~assets/images/group-plans/assign-task@3x.png');
|
||||
width: 400px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.requires-approval {
|
||||
background-image: url('~assets/images/group-plans/requires-approval@3x.png');
|
||||
width: 402px;
|
||||
height: 20px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.approval-requested {
|
||||
background-image: url('~assets/images/group-plans/approval-requested@3x.png');
|
||||
width: 471px;
|
||||
height: 204px;
|
||||
}
|
||||
|
||||
.promote-leader {
|
||||
background-image: url('~assets/images/group-plans/promote-leader@3x.png');
|
||||
width: 423px;
|
||||
height: 185px;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
margin-top: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
import upIcon from 'assets/svg/up.svg';
|
||||
import downIcon from 'assets/svg/down.svg';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
upIcon,
|
||||
downIcon,
|
||||
}),
|
||||
expandedQuestions: {
|
||||
question1: false,
|
||||
question2: false,
|
||||
question3: false,
|
||||
question4: false,
|
||||
question5: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
shown () {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'viewed-group-plan-overview',
|
||||
});
|
||||
},
|
||||
toggle (question) {
|
||||
this.expandedQuestions[question] = !this.expandedQuestions[question];
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'group-plan-overview');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template lang="pug">
|
||||
.row
|
||||
group-form-modal
|
||||
secondary-menu.col-12
|
||||
router-link.nav-link(:to="{name: 'groupPlanDetailTaskInformation', params: {groupId}}",
|
||||
exact, :class="{'active': $route.name === 'groupPlanDetailTaskInformation'}") {{ $t('groupTaskBoard') }}
|
||||
router-link.nav-link(:to="{name: 'groupPlanDetailInformation', params: {groupId}}",
|
||||
exact, :class="{'active': $route.name === 'groupPlanDetailInformation'}") {{ $t('groupInformation') }}
|
||||
router-link.nav-link(
|
||||
v-if='isLeader',
|
||||
:to="{name: 'groupPlanBilling', params: {groupId}}",
|
||||
exact,
|
||||
:class="{'active': $route.name === 'groupPlanBilling'}") {{ $t('groupBilling') }}
|
||||
|
||||
.col-12
|
||||
router-view
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import groupFormModal from 'client/components/groups/groupFormModal';
|
||||
import SecondaryMenu from 'client/components/secondaryMenu';
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
props: ['groupId'],
|
||||
components: {
|
||||
SecondaryMenu,
|
||||
groupFormModal,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
groupPlans: 'groupPlans',
|
||||
}),
|
||||
currentGroup () {
|
||||
let groupFound = this.groupPlans.find(group => {
|
||||
return group._id === this.groupId;
|
||||
});
|
||||
|
||||
return groupFound;
|
||||
},
|
||||
isLeader () {
|
||||
if (!this.currentGroup) return false;
|
||||
return this.currentGroup.leader === this.user._id;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,312 @@
|
||||
<template lang="pug">
|
||||
.standard-page
|
||||
group-plan-overview-modal
|
||||
task-modal(
|
||||
:task="workingTask",
|
||||
:purpose="taskFormPurpose",
|
||||
@cancel="cancelTaskModal()",
|
||||
ref="taskModal",
|
||||
:groupId="groupId",
|
||||
v-on:taskCreated='taskCreated',
|
||||
v-on:taskEdited='taskEdited',
|
||||
v-on:taskDestroyed='taskDestroyed'
|
||||
)
|
||||
.row.tasks-navigation
|
||||
.col-12.col-md-4
|
||||
h1 {{ $t('groupTasksTitle') }}
|
||||
// @TODO: Abstract to component!
|
||||
.col-12.col-md-4
|
||||
input.form-control.input-search(type="text", :placeholder="$t('search')", v-model="searchText")
|
||||
.create-task-area.d-flex(v-if='canCreateTasks')
|
||||
transition(name="slide-tasks-btns")
|
||||
.d-flex(v-if="openCreateBtn")
|
||||
.create-task-btn.diamond-btn(
|
||||
v-for="type in columns",
|
||||
:key="type",
|
||||
@click="createTask(type)",
|
||||
v-b-tooltip.hover.bottom="$t(type)",
|
||||
)
|
||||
.svg-icon(v-html="icons[type]", :class='`icon-${type}`')
|
||||
|
||||
#create-task-btn.create-btn.diamond-btn.btn.btn-success(
|
||||
@click="openCreateBtn = !openCreateBtn",
|
||||
:class="{open: openCreateBtn}",
|
||||
)
|
||||
.svg-icon(v-html="icons.positive")
|
||||
b-tooltip(target="create-task-btn", placement="bottom", v-if="!openCreateBtn") {{ $t('addTaskToGroupPlan') }}
|
||||
|
||||
.row
|
||||
task-column.col-12.col-md-3(
|
||||
v-for="column in columns",
|
||||
:type="column",
|
||||
:key="column",
|
||||
:taskListOverride='tasksByType[column]',
|
||||
:showOptions="showOptions"
|
||||
v-on:editTask="editTask",
|
||||
v-on:loadGroupCompletedTodos="loadGroupCompletedTodos",
|
||||
v-on:taskDestroyed="taskDestroyed",
|
||||
:group='group',
|
||||
:searchText="searchText")
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
@import '~client/assets/scss/create-task.scss';
|
||||
|
||||
.tasks-navigation {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.positive {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
color: $green-500;
|
||||
margin-right: 8px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import taskDefaults from 'common/script/libs/taskDefaults';
|
||||
import TaskColumn from '../tasks/column';
|
||||
import TaskModal from '../tasks/taskModal';
|
||||
import GroupPlanOverviewModal from './groupPlanOverviewModal';
|
||||
|
||||
import positiveIcon from 'assets/svg/positive.svg';
|
||||
import filterIcon from 'assets/svg/filter.svg';
|
||||
import deleteIcon from 'assets/svg/delete.svg';
|
||||
import habitIcon from 'assets/svg/habit.svg';
|
||||
import dailyIcon from 'assets/svg/daily.svg';
|
||||
import todoIcon from 'assets/svg/todo.svg';
|
||||
import rewardIcon from 'assets/svg/reward.svg';
|
||||
|
||||
import Vue from 'vue';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
props: ['groupId'],
|
||||
components: {
|
||||
TaskColumn,
|
||||
TaskModal,
|
||||
GroupPlanOverviewModal,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
openCreateBtn: false,
|
||||
searchId: '',
|
||||
columns: ['habit', 'daily', 'todo', 'reward'],
|
||||
tasksByType: {
|
||||
habit: [],
|
||||
daily: [],
|
||||
todo: [],
|
||||
reward: [],
|
||||
},
|
||||
editingTask: {},
|
||||
creatingTask: {},
|
||||
workingTask: {},
|
||||
taskFormPurpose: 'create',
|
||||
// @TODO: Separate component?
|
||||
searchText: '',
|
||||
selectedTags: [],
|
||||
temporarilySelectedTags: [],
|
||||
isFilterPanelOpen: false,
|
||||
icons: Object.freeze({
|
||||
positive: positiveIcon,
|
||||
filter: filterIcon,
|
||||
destroy: deleteIcon,
|
||||
habit: habitIcon,
|
||||
daily: dailyIcon,
|
||||
todo: todoIcon,
|
||||
reward: rewardIcon,
|
||||
}),
|
||||
editingTags: false,
|
||||
group: {},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
// call again the method if the route changes (when this route is already active)
|
||||
$route: 'load',
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
this.$set(this, 'searchId', to.params.groupId);
|
||||
next();
|
||||
},
|
||||
mounted () {
|
||||
if (!this.searchId) this.searchId = this.groupId;
|
||||
this.load();
|
||||
|
||||
if (this.$route.query.showGroupOverview) {
|
||||
this.$root.$emit('bv::show::modal', 'group-plan-overview');
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
tagsByType () {
|
||||
const userTags = this.user.tags;
|
||||
const tagsByType = {
|
||||
challenges: {
|
||||
key: 'challenges',
|
||||
tags: [],
|
||||
},
|
||||
groups: {
|
||||
key: 'groups',
|
||||
tags: [],
|
||||
},
|
||||
user: {
|
||||
key: 'tags',
|
||||
tags: [],
|
||||
},
|
||||
};
|
||||
|
||||
userTags.forEach(t => {
|
||||
if (t.group) {
|
||||
tagsByType.groups.tags.push(t);
|
||||
} else if (t.challenge) {
|
||||
tagsByType.challenges.tags.push(t);
|
||||
} else {
|
||||
tagsByType.user.tags.push(t);
|
||||
}
|
||||
});
|
||||
|
||||
return tagsByType;
|
||||
},
|
||||
canCreateTasks () {
|
||||
if (!this.group) return false;
|
||||
return this.group.leader && this.group.leader._id === this.user._id || this.group.managers && Boolean(this.group.managers[this.user._id]);
|
||||
},
|
||||
showOptions () {
|
||||
return this.canCreateTasks;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async load () {
|
||||
this.tasksByType = {
|
||||
habit: [],
|
||||
daily: [],
|
||||
todo: [],
|
||||
reward: [],
|
||||
};
|
||||
|
||||
this.group = await this.$store.dispatch('guilds:getGroup', {
|
||||
groupId: this.searchId,
|
||||
});
|
||||
|
||||
let members = await this.$store.dispatch('members:getGroupMembers', {groupId: this.searchId});
|
||||
this.group.members = members;
|
||||
|
||||
let tasks = await this.$store.dispatch('tasks:getGroupTasks', {
|
||||
groupId: this.searchId,
|
||||
});
|
||||
|
||||
let groupedApprovals = await this.loadApprovals();
|
||||
|
||||
tasks.forEach((task) => {
|
||||
if (groupedApprovals[task._id] && groupedApprovals[task._id].length > 0) task.approvals = groupedApprovals[task._id];
|
||||
this.tasksByType[task.type].push(task);
|
||||
});
|
||||
},
|
||||
async loadApprovals () {
|
||||
let approvalRequests = await this.$store.dispatch('tasks:getGroupApprovals', {
|
||||
groupId: this.searchId,
|
||||
});
|
||||
|
||||
return groupBy(approvalRequests, 'group.taskId');
|
||||
},
|
||||
editTask (task) {
|
||||
this.taskFormPurpose = 'edit';
|
||||
this.editingTask = cloneDeep(task);
|
||||
this.workingTask = this.editingTask;
|
||||
// Necessary otherwise the first time the modal is not rendered
|
||||
Vue.nextTick(() => {
|
||||
this.$root.$emit('bv::show::modal', 'task-modal');
|
||||
});
|
||||
},
|
||||
async loadGroupCompletedTodos () {
|
||||
const completedTodos = await this.$store.dispatch('tasks:getCompletedGroupTasks', {
|
||||
groupId: this.searchId,
|
||||
});
|
||||
|
||||
completedTodos.forEach((task) => {
|
||||
const existingTaskIndex = findIndex(this.tasksByType.todo, (todo) => {
|
||||
return todo._id === task._id;
|
||||
});
|
||||
if (existingTaskIndex === -1) {
|
||||
this.tasksByType.todo.push(task);
|
||||
}
|
||||
});
|
||||
},
|
||||
createTask (type) {
|
||||
this.openCreateBtn = false;
|
||||
this.taskFormPurpose = 'create';
|
||||
this.creatingTask = taskDefaults({type, text: ''}, this.user);
|
||||
this.workingTask = this.creatingTask;
|
||||
// Necessary otherwise the first time the modal is not rendered
|
||||
Vue.nextTick(() => {
|
||||
this.$root.$emit('bv::show::modal', 'task-modal');
|
||||
});
|
||||
},
|
||||
taskCreated (task) {
|
||||
task.group.id = this.group._id;
|
||||
this.tasksByType[task.type].push(task);
|
||||
},
|
||||
taskEdited (task) {
|
||||
let index = findIndex(this.tasksByType[task.type], (taskItem) => {
|
||||
return taskItem._id === task._id;
|
||||
});
|
||||
this.tasksByType[task.type].splice(index, 1, task);
|
||||
},
|
||||
taskDestroyed (task) {
|
||||
let index = findIndex(this.tasksByType[task.type], (taskItem) => {
|
||||
return taskItem._id === task._id;
|
||||
});
|
||||
this.tasksByType[task.type].splice(index, 1);
|
||||
},
|
||||
cancelTaskModal () {
|
||||
this.editingTask = null;
|
||||
this.creatingTask = null;
|
||||
this.workingTask = {};
|
||||
},
|
||||
toggleFilterPanel () {
|
||||
if (this.isFilterPanelOpen === true) {
|
||||
this.closeFilterPanel();
|
||||
} else {
|
||||
this.openFilterPanel();
|
||||
}
|
||||
},
|
||||
openFilterPanel () {
|
||||
this.isFilterPanelOpen = true;
|
||||
this.temporarilySelectedTags = this.selectedTags.slice();
|
||||
},
|
||||
closeFilterPanel () {
|
||||
this.temporarilySelectedTags = [];
|
||||
this.isFilterPanelOpen = false;
|
||||
},
|
||||
resetFilters () {
|
||||
this.selectedTags = [];
|
||||
this.closeFilterPanel();
|
||||
},
|
||||
applyFilters () {
|
||||
const temporarilySelectedTags = this.temporarilySelectedTags;
|
||||
this.selectedTags = temporarilySelectedTags.slice();
|
||||
this.closeFilterPanel();
|
||||
},
|
||||
toggleTag (tag) {
|
||||
const temporarilySelectedTags = this.temporarilySelectedTags;
|
||||
const tagI = temporarilySelectedTags.indexOf(tag.id);
|
||||
if (tagI === -1) {
|
||||
temporarilySelectedTags.push(tag.id);
|
||||
} else {
|
||||
temporarilySelectedTags.splice(tagI, 1);
|
||||
}
|
||||
},
|
||||
isTagSelected (tag) {
|
||||
const tagId = tag.id;
|
||||
if (this.temporarilySelectedTags.indexOf(tagId) !== -1) return true;
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template lang="pug">
|
||||
b-link(
|
||||
v-if='group',
|
||||
@click.prevent='goToGroup'
|
||||
) {{group.name}}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { TAVERN_ID } from '../../common/script/constants';
|
||||
|
||||
export default {
|
||||
props: ['group'],
|
||||
methods: {
|
||||
goToGroup () {
|
||||
if (this.group.type === 'party') {
|
||||
this.$router.push({name: 'party'});
|
||||
} else if (this.group._id === TAVERN_ID) {
|
||||
this.$router.push({name: 'tavern'});
|
||||
} else {
|
||||
this.$router.push({name: 'guild', params: {groupId: this.group._id}});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,313 @@
|
||||
<template lang="pug">
|
||||
.row.chat-row
|
||||
.col-12
|
||||
h3.float-left.label(:class="{accepted: communityGuidelinesAccepted }") {{ label }}
|
||||
div.float-right(v-markdown='$t("markdownFormattingHelp")')
|
||||
|
||||
.row(v-if="communityGuidelinesAccepted")
|
||||
textarea(:placeholder='placeholder',
|
||||
v-model='newMessage',
|
||||
ref='user-entry',
|
||||
:class='{"user-entry": newMessage}',
|
||||
@keydown='updateCarretPosition',
|
||||
@keyup.ctrl.enter='sendMessageShortcut()',
|
||||
@keydown.tab='handleTab($event)',
|
||||
@keydown.up='selectPreviousAutocomplete($event)',
|
||||
@keydown.down='selectNextAutocomplete($event)',
|
||||
@keydown.enter='selectAutocomplete($event)',
|
||||
@keydown.esc='handleEscape($event)',
|
||||
@paste='disableMessageSendShortcut()',
|
||||
maxlength='3000'
|
||||
)
|
||||
span {{ currentLength }} / 3000
|
||||
autocomplete(
|
||||
ref='autocomplete',
|
||||
:text='newMessage',
|
||||
v-on:select="selectedAutocomplete",
|
||||
:textbox='textbox',
|
||||
:coords='coords',
|
||||
:caretPosition = 'caretPosition',
|
||||
:chat='group.chat')
|
||||
|
||||
community-guidelines
|
||||
|
||||
.row.chat-actions
|
||||
.col-6.chat-receive-actions
|
||||
button.btn.btn-secondary.float-left.fetch(v-once, @click='fetchRecentMessages()') {{ $t('fetchRecentMessages') }}
|
||||
button.btn.btn-secondary.float-left(v-once, @click='reverseChat()') {{ $t('reverseChat') }}
|
||||
.col-6.chat-send-actions
|
||||
button.btn.btn-primary.send-chat.float-right(:disabled="!communityGuidelinesAccepted", @click='sendMessage()') {{ $t('send') }}
|
||||
|
||||
slot(
|
||||
name="additionRow",
|
||||
)
|
||||
|
||||
.row
|
||||
.hr.col-12
|
||||
chat-message(:chat.sync='group.chat', :group-type='group.type', :group-id='group._id', :group-name='group.name')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import autocomplete from '../chat/autoComplete';
|
||||
import communityGuidelines from './communityGuidelines';
|
||||
import chatMessage from '../chat/chatMessages';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import markdownDirective from 'client/directives/markdown';
|
||||
|
||||
export default {
|
||||
props: ['label', 'group', 'placeholder'],
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
components: {
|
||||
autocomplete,
|
||||
communityGuidelines,
|
||||
chatMessage,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
newMessage: '',
|
||||
sending: false,
|
||||
caretPosition: 0,
|
||||
chat: {
|
||||
submitDisable: false,
|
||||
submitTimeout: null,
|
||||
},
|
||||
coords: {
|
||||
TOP: 0,
|
||||
LEFT: 0,
|
||||
},
|
||||
textbox: this.$refs,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
currentLength () {
|
||||
return this.newMessage.length;
|
||||
},
|
||||
communityGuidelinesAccepted () {
|
||||
return this.user.flags.communityGuidelinesAccepted;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a
|
||||
getCoord (e, text) {
|
||||
this.caretPosition = text.selectionEnd;
|
||||
let div = document.createElement('div');
|
||||
let span = document.createElement('span');
|
||||
let copyStyle = getComputedStyle(text);
|
||||
|
||||
[].forEach.call(copyStyle, (prop) => {
|
||||
div.style[prop] = copyStyle[prop];
|
||||
});
|
||||
|
||||
div.style.position = 'absolute';
|
||||
document.body.appendChild(div);
|
||||
div.textContent = text.value.substr(0, this.caretPosition);
|
||||
span.textContent = text.value.substr(this.caretPosition) || '.';
|
||||
div.appendChild(span);
|
||||
this.coords = {
|
||||
TOP: span.offsetTop,
|
||||
LEFT: span.offsetLeft,
|
||||
};
|
||||
document.body.removeChild(div);
|
||||
},
|
||||
updateCarretPosition: debounce(function updateCarretPosition (eventUpdate) {
|
||||
this._updateCarretPosition(eventUpdate);
|
||||
}, 250),
|
||||
_updateCarretPosition (eventUpdate) {
|
||||
let text = eventUpdate.target;
|
||||
this.getCoord(eventUpdate, text);
|
||||
},
|
||||
async sendMessageShortcut () {
|
||||
// If the user recently pasted in the text field, don't submit
|
||||
if (!this.chat.submitDisable) {
|
||||
this.sendMessage();
|
||||
}
|
||||
},
|
||||
async sendMessage () {
|
||||
if (this.sending) return;
|
||||
this.sending = true;
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await this.$store.dispatch('chat:postChat', {
|
||||
group: this.group,
|
||||
message: this.newMessage,
|
||||
});
|
||||
} catch (e) {
|
||||
// catch exception to allow function to continue
|
||||
}
|
||||
|
||||
if (response) {
|
||||
this.group.chat.unshift(response.message);
|
||||
this.newMessage = '';
|
||||
}
|
||||
|
||||
this.sending = false;
|
||||
|
||||
// @TODO: I would like to not reload everytime we send. Why are we reloading?
|
||||
// The response has all the necessary data...
|
||||
let chat = await this.$store.dispatch('chat:getChat', {groupId: this.group._id});
|
||||
this.group.chat = chat;
|
||||
},
|
||||
|
||||
disableMessageSendShortcut () {
|
||||
// Some users were experiencing accidental sending of messages after pasting
|
||||
// So, after pasting, disable the shortcut for a second.
|
||||
this.chat.submitDisable = true;
|
||||
|
||||
if (this.chat.submitTimeout) {
|
||||
// If someone pastes during the disabled period, prevent early re-enable
|
||||
clearTimeout(this.chat.submitTimeout);
|
||||
this.chat.submitTimeout = null;
|
||||
}
|
||||
|
||||
this.chat.submitTimeout = window.setTimeout(() => {
|
||||
this.chat.submitTimeout = null;
|
||||
this.chat.submitDisable = false;
|
||||
}, 500);
|
||||
},
|
||||
|
||||
handleTab (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
this.$refs.autocomplete.selectPrevious();
|
||||
} else {
|
||||
this.$refs.autocomplete.selectNext();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleEscape (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.cancel();
|
||||
}
|
||||
},
|
||||
|
||||
selectNextAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.selectNext();
|
||||
}
|
||||
},
|
||||
|
||||
selectPreviousAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.selectPrevious();
|
||||
}
|
||||
},
|
||||
|
||||
selectAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.makeSelection();
|
||||
}
|
||||
},
|
||||
|
||||
selectedAutocomplete (newText) {
|
||||
this.newMessage = newText;
|
||||
},
|
||||
|
||||
fetchRecentMessages () {
|
||||
this.$emit('fetchRecentMessages');
|
||||
},
|
||||
reverseChat () {
|
||||
this.group.chat.reverse();
|
||||
},
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
// Reset chat
|
||||
this.newMessage = '';
|
||||
this.coords = {
|
||||
TOP: 0,
|
||||
LEFT: 0,
|
||||
};
|
||||
|
||||
next();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
@import '~client/assets/scss/variables.scss';
|
||||
|
||||
.chat-actions {
|
||||
margin-top: 1em;
|
||||
|
||||
.chat-receive-actions {
|
||||
padding-left: 0;
|
||||
|
||||
button {
|
||||
margin-bottom: 1em;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-send-actions {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-row {
|
||||
position: relative;
|
||||
|
||||
.label:not(.accepted) {
|
||||
color: #a5a1ac;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 150px;
|
||||
width: 100%;
|
||||
background-color: $white;
|
||||
border: solid 1px $gray-400;
|
||||
font-style: italic;
|
||||
line-height: 1.43;
|
||||
color: $gray-300;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.user-entry {
|
||||
font-style: normal;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.hr {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
border-bottom: 1px solid $gray-500;
|
||||
text-align: center;
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
.hr-middle {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
font-family: 'Roboto Condensed';
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
color: $gray-200;
|
||||
background-color: $gray-700;
|
||||
padding: .2em;
|
||||
margin-top: .2em;
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template lang="pug">
|
||||
.row.community-guidelines(v-if='!communityGuidelinesAccepted')
|
||||
div.col.col-sm-12.col-xl-8(v-once, v-html="$t('communityGuidelinesIntro')")
|
||||
|
||||
div.col-md-auto.col-md-12.col-xl-4
|
||||
button.btn.btn-info.btn-follow-guidelines(@click='acceptCommunityGuidelines()', v-once) {{ $t('acceptCommunityGuidelines') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.community-guidelines {
|
||||
background-color: rgba(135, 129, 144, 0.84);
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
color: $white;
|
||||
width: 100%;
|
||||
// width: calc(100% - 24px);
|
||||
border-radius: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.col {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: $white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 20px 12px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.btn-follow-guidelines {
|
||||
white-space: pre-line;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
communityGuidelinesAccepted () {
|
||||
return this.user.flags.communityGuidelinesAccepted;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
acceptCommunityGuidelines () {
|
||||
this.$store.dispatch('user:set', {'flags.communityGuidelinesAccepted': true});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,194 @@
|
||||
<template lang="pug">
|
||||
b-modal#create-party-modal(size='lg', hide-footer=true)
|
||||
.header-wrap(slot="modal-header")
|
||||
.quest_screen
|
||||
.row.heading
|
||||
.col-12.text-center.pr-5.pl-5
|
||||
h2(v-once) {{ $t('playInPartyTitle') }}
|
||||
p.mb-4(v-once) {{ $t('playInPartyDescription') }}
|
||||
button.btn.btn-primary(v-once, @click='createParty()') {{ $t('createParty') }}
|
||||
.row.grey-row
|
||||
.col-12.text-center
|
||||
.join-party
|
||||
h2(v-once) {{ $t('wantToJoinPartyTitle') }}
|
||||
p(v-html='$t("wantToJoinPartyDescription")')
|
||||
.form-group(@click='copyUsername')
|
||||
.d-flex.align-items-center
|
||||
label.mr-3(v-once) {{ $t('username') }}
|
||||
.flex-grow-1
|
||||
.input-group-prepend.input-group-text @
|
||||
.text {{ user.auth.local.username }}
|
||||
.svg-icon.copy-icon(v-html='icons.copy')
|
||||
.small(v-once) {{ $t('copy') }}
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#create-party-modal .modal-body {
|
||||
padding: 0rem 0.75rem;
|
||||
}
|
||||
|
||||
#create-party-modal .modal-dialog {
|
||||
width: 35.75rem;
|
||||
}
|
||||
|
||||
#create-party-modal .modal-header {
|
||||
padding: 0;
|
||||
border-bottom: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.copy-icon {
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.form-control[readonly] {
|
||||
background-color: $white;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
background-color: $gray-600;
|
||||
border-radius: 2px;
|
||||
border: solid 1px $gray-500;
|
||||
width: 90%;
|
||||
margin: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.grey-row {
|
||||
background-color: $gray-700;
|
||||
color: #4e4a57;
|
||||
padding: 2em;
|
||||
border-radius: 0px 0px 2px 2px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: $gray-100;
|
||||
}
|
||||
|
||||
.header-wrap {
|
||||
padding: 0;
|
||||
color: #4e4a57;
|
||||
width: 100%;
|
||||
|
||||
.quest_screen {
|
||||
background-image: url('~client/assets/images/group@3x.png');
|
||||
background-size: cover;
|
||||
width: 100%;
|
||||
height: 246px;
|
||||
margin-bottom: 1.5rem;
|
||||
border-radius: 2px 2px 0 0;
|
||||
image-rendering: optimizequality;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: $purple-200;
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
input.form-control {
|
||||
border: 0px;
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
|
||||
.input-group-prepend {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background-color: $white;
|
||||
border: 0px;
|
||||
border-radius: 0px;
|
||||
color: $gray-300;
|
||||
padding: 0rem 0.1rem 0rem 0.75rem;
|
||||
}
|
||||
|
||||
.join-party {
|
||||
background-image: url('~client/assets/images/party.png');
|
||||
background-size: cover;
|
||||
width: 203px;
|
||||
height: 66px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
label {
|
||||
color: $gray-100;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0rem;
|
||||
margin-left: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-dialog .text {
|
||||
min-height: 1rem;
|
||||
margin: 0.75rem auto 0.75rem 0.25rem;
|
||||
}
|
||||
|
||||
.small {
|
||||
color: $gray-200;
|
||||
margin: auto 0.5rem auto 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import notifications from 'client/mixins/notifications';
|
||||
|
||||
import copyIcon from 'assets/svg/copy.svg';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
copy: copyIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
async createParty () {
|
||||
let group = {
|
||||
type: 'party',
|
||||
};
|
||||
group.name = this.$t('possessiveParty', {name: this.user.profile.name});
|
||||
let party = await this.$store.dispatch('guilds:create', {group});
|
||||
this.$store.state.party.data = party;
|
||||
this.user.party._id = party._id;
|
||||
|
||||
Analytics.updateUser({
|
||||
partyID: party._id,
|
||||
partySize: 1,
|
||||
});
|
||||
|
||||
this.$root.$emit('bv::hide::modal', 'create-party-modal');
|
||||
this.$router.push('/party');
|
||||
},
|
||||
copyUsername () {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(this.user.auth.local.username);
|
||||
} else {
|
||||
let copyText = document.createElement('textarea');
|
||||
copyText.value = this.user.auth.local.username;
|
||||
document.body.appendChild(copyText);
|
||||
copyText.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(copyText);
|
||||
}
|
||||
this.text(this.$t('usernameCopied'));
|
||||
},
|
||||
},
|
||||
mixins: [notifications],
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,212 @@
|
||||
<template lang="pug">
|
||||
.row
|
||||
sidebar(@search="updateSearch", @filter="updateFilters")
|
||||
|
||||
.standard-page
|
||||
.header.row
|
||||
.col-8
|
||||
h1.page-header.float-left(v-once) {{ $t('publicGuilds') }}
|
||||
.col-4
|
||||
// @TODO: Add when we implement recent activity .float-right
|
||||
span.dropdown-label {{ $t('sortBy') }}
|
||||
b-dropdown(:text="$t('sort')", right=true)
|
||||
b-dropdown-item(v-for='sortOption in sortOptions', :key="sortOption.value", @click='sort(sortOption.value)') {{sortOption.text}}
|
||||
button.btn.btn-secondary.create-group-button.float-right(@click='createGroup()')
|
||||
.svg-icon.positive-icon(v-html="icons.positiveIcon")
|
||||
span(v-once) {{$t('createGuild2')}}
|
||||
|
||||
.row
|
||||
.no-guilds.text-center.col-md-6.offset-md-3(v-if='!loading && filteredGuilds.length === 0')
|
||||
h2(v-once) {{$t('noGuildsMatchFilters')}}
|
||||
|
||||
.row
|
||||
.col-md-12
|
||||
public-guild-item(v-for="guild in filteredGuilds", :key='guild._id', :guild="guild", :display-leave='true')
|
||||
mugen-scroll(
|
||||
:handler="fetchGuilds",
|
||||
:should-handle="!loading && !this.hasLoadedAllGuilds",
|
||||
:handle-on-mount="true",
|
||||
v-show="loading",
|
||||
)
|
||||
span(v-once) {{ $t('loading') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.sort-select {
|
||||
margin: 2em;
|
||||
}
|
||||
|
||||
.positive-icon {
|
||||
color: $green-10;
|
||||
width: 10px;
|
||||
display: inline-block;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.no-guilds {
|
||||
color: $gray-200;
|
||||
margin-top: 10em;
|
||||
|
||||
h2 {
|
||||
color: $gray-200;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import MugenScroll from 'vue-mugen-scroll';
|
||||
import PublicGuildItem from './publicGuildItem';
|
||||
import Sidebar from './sidebar';
|
||||
import groupUtilities from 'client/mixins/groupsUtilities';
|
||||
|
||||
import positiveIcon from 'assets/svg/positive.svg';
|
||||
|
||||
function _mapCategories (guilds) {
|
||||
guilds.forEach((guild) => {
|
||||
if (!guild.categories) return;
|
||||
guild.categorySlugs = guild.categories.map(cat => {
|
||||
if (!cat) return;
|
||||
return cat.slug;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
mixins: [groupUtilities],
|
||||
components: { PublicGuildItem, MugenScroll, Sidebar },
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
positiveIcon,
|
||||
}),
|
||||
loading: false,
|
||||
hasLoadedAllGuilds: false,
|
||||
lastPageLoaded: 0,
|
||||
search: '',
|
||||
filters: {},
|
||||
sort: 'none',
|
||||
sortOptions: [
|
||||
{
|
||||
text: this.$t('none'),
|
||||
value: 'none',
|
||||
},
|
||||
{
|
||||
text: this.$t('memberCount'),
|
||||
value: 'member_count',
|
||||
},
|
||||
{
|
||||
text: this.$t('recentActivity'),
|
||||
value: 'recent_activity',
|
||||
},
|
||||
],
|
||||
guilds: [],
|
||||
queryFilters: {
|
||||
minMemberCount: 0,
|
||||
maxMemberCount: 0,
|
||||
leader: false,
|
||||
member: false,
|
||||
categories: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredGuilds () {
|
||||
let search = this.search;
|
||||
let filters = this.filters;
|
||||
let user = this.$store.state.user.data;
|
||||
let filterGuild = this.filterGuild;
|
||||
// @TODO: Move this to the server
|
||||
return this.guilds.filter((guild) => {
|
||||
return filterGuild(guild, filters, search, user);
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async updateSearch (eventData) {
|
||||
// this.search = eventData.searchTerm; @TODO: Probably don't need this anymore
|
||||
|
||||
// Reset the page when filters are updated but not the other queries
|
||||
this.lastPageLoaded = 0;
|
||||
this.queryFilters.page = this.lastPageLoaded;
|
||||
|
||||
this.queryFilters.search = eventData.searchTerm;
|
||||
|
||||
let guilds = await this.$store.dispatch('guilds:getPublicGuilds', this.queryFilters);
|
||||
_mapCategories(guilds);
|
||||
this.guilds = guilds;
|
||||
},
|
||||
async updateFilters (eventData) {
|
||||
// this.filters = eventData; @TODO: Probably don't need this anymore
|
||||
|
||||
// Reset all filters
|
||||
this.queryFilters = {
|
||||
minMemberCount: 0,
|
||||
maxMemberCount: 0,
|
||||
leader: false,
|
||||
member: false,
|
||||
categories: '',
|
||||
};
|
||||
|
||||
// Reset the page when filters are updated
|
||||
this.lastPageLoaded = 0;
|
||||
this.hasLoadedAllGuilds = false;
|
||||
this.queryFilters.page = this.lastPageLoaded;
|
||||
|
||||
this.queryFilters.categories = eventData.categories.join(',');
|
||||
|
||||
// Role filters
|
||||
let filteringRole = eventData.roles && eventData.roles.length > 0;
|
||||
if (filteringRole && eventData.roles.indexOf('member') !== -1) {
|
||||
this.queryFilters.member = true;
|
||||
}
|
||||
|
||||
if (filteringRole && eventData.roles.indexOf('guild_leader') !== -1) {
|
||||
this.queryFilters.leader = true;
|
||||
}
|
||||
|
||||
// Size filters
|
||||
if (eventData.guildSize && eventData.guildSize.indexOf('gold_tier') !== -1) {
|
||||
this.queryFilters.minMemberCount = 1000;
|
||||
this.queryFilters.maxMemberCount = 0; // No max
|
||||
}
|
||||
|
||||
if (eventData.guildSize && eventData.guildSize.indexOf('silver_tier') !== -1) {
|
||||
this.queryFilters.minMemberCount = 100;
|
||||
this.queryFilters.maxMemberCount = 999;
|
||||
}
|
||||
|
||||
if (eventData.guildSize && eventData.guildSize.indexOf('bronze_tier') !== -1) {
|
||||
this.queryFilters.minMemberCount = 0; // No Min
|
||||
this.queryFilters.maxMemberCount = 99;
|
||||
}
|
||||
|
||||
let guilds = await this.$store.dispatch('guilds:getPublicGuilds', this.queryFilters);
|
||||
_mapCategories(guilds);
|
||||
this.guilds = guilds;
|
||||
},
|
||||
async fetchGuilds () {
|
||||
// We have the data cached
|
||||
if (this.lastPageLoaded === 0 && this.guilds.length > 0) {
|
||||
this.lastPageLoaded += 1;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.queryFilters.page = this.lastPageLoaded;
|
||||
let guilds = await this.$store.dispatch('guilds:getPublicGuilds', this.queryFilters);
|
||||
if (guilds.length === 0) this.hasLoadedAllGuilds = true;
|
||||
|
||||
_mapCategories(guilds);
|
||||
this.guilds.push(...guilds);
|
||||
|
||||
this.lastPageLoaded++;
|
||||
this.loading = false;
|
||||
},
|
||||
createGroup () {
|
||||
this.$store.state.editingGroup = {};
|
||||
this.$root.$emit('bv::show::modal', 'guild-form');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,529 @@
|
||||
<template lang="pug">
|
||||
.row(v-if="group._id")
|
||||
group-form-modal(v-if='isParty')
|
||||
start-quest-modal(:group='this.group')
|
||||
quest-details-modal(:group='this.group')
|
||||
participant-list-modal(:group='this.group')
|
||||
group-gems-modal
|
||||
.col-12.col-sm-8.standard-page
|
||||
.row
|
||||
.col-12.col-md-6.title-details
|
||||
h1 {{group.name}}
|
||||
div
|
||||
span.mr-1.ml-0
|
||||
strong(v-once) {{$t('groupLeader')}}:
|
||||
user-link.mx-1(:user="group.leader")
|
||||
.col-12.col-md-6
|
||||
.row.icon-row
|
||||
.col-4.offset-4(v-bind:class="{ 'offset-8': isParty }")
|
||||
.item-with-icon(@click="showMemberModal()")
|
||||
.svg-icon.shield(v-html="icons.goldGuildBadgeIcon", v-if='group.memberCount > 1000')
|
||||
.svg-icon.shield(v-html="icons.silverGuildBadgeIcon", v-if='group.memberCount > 100 && group.memberCount < 999')
|
||||
.svg-icon.shield(v-html="icons.bronzeGuildBadgeIcon", v-if='group.memberCount < 100')
|
||||
span.number {{ group.memberCount | abbrNum }}
|
||||
div.member-list.label(v-once) {{ $t('memberList') }}
|
||||
.col-4(v-if='!isParty')
|
||||
.item-with-icon(@click='showGroupGems()')
|
||||
.svg-icon.gem(v-html="icons.gem")
|
||||
span.number {{group.balance * 4}}
|
||||
div.label(v-once) {{ $t('guildBank') }}
|
||||
chat(
|
||||
:label="$t('chat')",
|
||||
:group="group",
|
||||
:placeholder="!isParty ? $t('chatPlaceholder') : $t('partyChatPlaceholder')",
|
||||
@fetchRecentMessages="fetchRecentMessages()"
|
||||
)
|
||||
template(slot="additionRow")
|
||||
.row(v-if='showNoNotificationsMessage')
|
||||
.col-12.no-notifications
|
||||
| {{$t('groupNoNotifications')}}
|
||||
.col-12.col-sm-4.sidebar
|
||||
.row(:class='{"guild-background": !isParty}')
|
||||
.col-12.buttons-wrapper
|
||||
.button-container
|
||||
button.btn.btn-success(class='btn-success', v-if='isLeader && !group.purchased.active', @click='upgradeGroup()')
|
||||
| {{ $t('upgrade') }}
|
||||
.button-container
|
||||
button.btn.btn-primary(b-btn, @click="updateGuild", v-once, v-if='isLeader || isAdmin') {{ $t('edit') }}
|
||||
.button-container
|
||||
button.btn.btn-success(class='btn-success', v-if='!isMember', @click='join()') {{ $t('join') }}
|
||||
.button-container
|
||||
button.btn.btn-primary(v-once, @click='showInviteModal()') {{$t('invite')}}
|
||||
// @TODO: hide the invitation button if there's an active group plan and the player is not the leader
|
||||
.button-container
|
||||
// @TODO: V2 button.btn.btn-primary(v-once, v-if='!isLeader') {{$t('messageGuildLeader')}} // Suggest making the button visible to the leader too - useful for them to test how the feature works or to send a note to themself. -- Alys
|
||||
.button-container
|
||||
// @TODO: V2 button.btn.btn-primary(v-once, v-if='isMember && !isParty') {{$t('donateGems')}} // Suggest removing the isMember restriction - it's okay if non-members donate to a public guild. Also probably allow it for parties if parties can buy imagery. -- Alys
|
||||
div
|
||||
quest-sidebar-section(:group='group', v-if='isParty')
|
||||
sidebar-section(:title="$t('guildSummary')", v-if='!isParty')
|
||||
p(v-markdown='group.summary')
|
||||
sidebar-section(:title="$t('groupDescription')")
|
||||
p(v-markdown='group.description')
|
||||
sidebar-section(
|
||||
:title="$t('challenges')",
|
||||
:tooltip="$t('challengeDetails')"
|
||||
)
|
||||
group-challenges(:groupId='searchId')
|
||||
div.text-center
|
||||
button.btn.btn-danger(v-if='isMember', @click='clickLeave()') {{ isParty ? $t('leaveParty') : $t('leaveGroup') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
@media (min-width: 1300px) {
|
||||
.standard-page {
|
||||
max-width: calc(100% - 430px);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
max-width: 430px !important;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin-bottom: 1em;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.item-with-icon {
|
||||
border-radius: 2px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
padding: 1em;
|
||||
text-align: center;
|
||||
min-width: 80px;
|
||||
max-width: 120px;
|
||||
height: 76px;
|
||||
|
||||
.svg-icon.shield, .svg-icon.gem {
|
||||
width: 28px;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.item-with-icon:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: $gray-600;
|
||||
padding-bottom: 2em;
|
||||
|
||||
}
|
||||
|
||||
.buttons-wrapper {
|
||||
padding-top: 2.8em;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 2em 0;
|
||||
padding: 1em;
|
||||
|
||||
h3.leader {
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 16px;
|
||||
line-height: 1.43;
|
||||
color: $gray-50;
|
||||
}
|
||||
}
|
||||
|
||||
.guild-background {
|
||||
background-image: url('~assets/images/groups/grassy-meadow-backdrop.png');
|
||||
height: 246px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
background-color: $white;
|
||||
border: solid 1px $gray-400;
|
||||
font-size: 16px;
|
||||
line-height: 1.43;
|
||||
color: $gray-300;
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
textarea::-webkit-input-placeholder { /* Chrome/Opera/Safari */
|
||||
font-style: italic;
|
||||
}
|
||||
textarea::-moz-placeholder { /* Firefox 19+ */
|
||||
font-style: italic;
|
||||
}
|
||||
textarea:-ms-input-placeholder { /* IE 10+ */
|
||||
font-style: italic;
|
||||
}
|
||||
textarea:-moz-placeholder { /* Firefox 18- */
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.title-details {
|
||||
padding-top: 1em;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.icon-row {
|
||||
margin-top: 1em;
|
||||
|
||||
.number {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-row {
|
||||
margin-top: 2em;
|
||||
|
||||
.new-message-row {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-actions {
|
||||
margin-top: 1em;
|
||||
|
||||
.chat-receive-actions {
|
||||
padding-left: 0;
|
||||
|
||||
button {
|
||||
margin-bottom: 1em;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-send-actions {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span.action {
|
||||
font-size: 14px;
|
||||
line-height: 1.33;
|
||||
color: $gray-200;
|
||||
font-weight: 500;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
span.action .icon {
|
||||
margin-right: .3em;
|
||||
}
|
||||
|
||||
.hr {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
border-bottom: 1px solid $gray-500;
|
||||
text-align: center;
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
.no-notifications {
|
||||
border-radius: 4px;
|
||||
border: solid 1px $orange-10;
|
||||
padding: 1em;
|
||||
margin-top: 3em;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: $orange-50;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// @TODO: Break this down into components
|
||||
|
||||
import extend from 'lodash/extend';
|
||||
import groupUtilities from 'client/mixins/groupsUtilities';
|
||||
import styleHelper from 'client/mixins/styleHelper';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import membersModal from './membersModal';
|
||||
import startQuestModal from './startQuestModal';
|
||||
import questDetailsModal from './questDetailsModal';
|
||||
import participantListModal from './participantListModal';
|
||||
import groupFormModal from './groupFormModal';
|
||||
import groupChallenges from '../challenges/groupChallenges';
|
||||
import groupGemsModal from 'client/components/groups/groupGemsModal';
|
||||
import questSidebarSection from 'client/components/groups/questSidebarSection';
|
||||
import markdownDirective from 'client/directives/markdown';
|
||||
import chat from './chat';
|
||||
import sidebarSection from '../sidebarSection';
|
||||
import userLink from '../userLink';
|
||||
|
||||
import deleteIcon from 'assets/svg/delete.svg';
|
||||
import copyIcon from 'assets/svg/copy.svg';
|
||||
import likeIcon from 'assets/svg/like.svg';
|
||||
import likedIcon from 'assets/svg/liked.svg';
|
||||
import reportIcon from 'assets/svg/report.svg';
|
||||
import gemIcon from 'assets/svg/gem.svg';
|
||||
import questIcon from 'assets/svg/quest.svg';
|
||||
import questBackground from 'assets/svg/quest-background-border.svg';
|
||||
import goldGuildBadgeIcon from 'assets/svg/gold-guild-badge-small.svg';
|
||||
import silverGuildBadgeIcon from 'assets/svg/silver-guild-badge-small.svg';
|
||||
import bronzeGuildBadgeIcon from 'assets/svg/bronze-guild-badge-small.svg';
|
||||
|
||||
export default {
|
||||
mixins: [groupUtilities, styleHelper],
|
||||
props: ['groupId'],
|
||||
components: {
|
||||
membersModal,
|
||||
startQuestModal,
|
||||
groupFormModal,
|
||||
groupChallenges,
|
||||
questDetailsModal,
|
||||
participantListModal,
|
||||
groupGemsModal,
|
||||
questSidebarSection,
|
||||
sidebarSection,
|
||||
userLink,
|
||||
chat,
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
searchId: '',
|
||||
group: {},
|
||||
icons: Object.freeze({
|
||||
like: likeIcon,
|
||||
copy: copyIcon,
|
||||
report: reportIcon,
|
||||
delete: deleteIcon,
|
||||
gem: gemIcon,
|
||||
liked: likedIcon,
|
||||
questIcon,
|
||||
questBackground,
|
||||
goldGuildBadgeIcon,
|
||||
silverGuildBadgeIcon,
|
||||
bronzeGuildBadgeIcon,
|
||||
}),
|
||||
members: [],
|
||||
selectedQuest: {},
|
||||
chat: {
|
||||
submitDisable: false,
|
||||
submitTimeout: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
partyStore () {
|
||||
return this.$store.state.party;
|
||||
},
|
||||
isParty () {
|
||||
return this.$route.path.startsWith('/party');
|
||||
},
|
||||
isLeader () {
|
||||
return this.user._id === this.group.leader._id;
|
||||
},
|
||||
isAdmin () {
|
||||
return Boolean(this.user.contributor.admin);
|
||||
},
|
||||
isMember () {
|
||||
return this.isMemberOfGroup(this.user, this.group);
|
||||
},
|
||||
showNoNotificationsMessage () {
|
||||
return this.group.memberCount > this.$store.state.constants.LARGE_GROUP_COUNT_MESSAGE_CUTOFF;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
if (this.isParty) this.searchId = 'party';
|
||||
if (!this.searchId) this.searchId = this.groupId;
|
||||
this.load();
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
this.$set(this, 'searchId', to.params.groupId);
|
||||
|
||||
next();
|
||||
},
|
||||
watch: {
|
||||
// call again the method if the route changes (when this route is already active)
|
||||
$route: 'fetchGuild',
|
||||
partyStore () {
|
||||
if (this.$store.state.party._id) {
|
||||
this.group = this.$store.state.party;
|
||||
} else {
|
||||
this.group = null;
|
||||
this.$router.push('/');
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
acceptCommunityGuidelines () {
|
||||
this.$store.dispatch('user:set', {'flags.communityGuidelinesAccepted': true});
|
||||
},
|
||||
async load () {
|
||||
if (this.isParty) {
|
||||
this.searchId = 'party';
|
||||
// @TODO: Set up from old client. Decide what we need and what we don't
|
||||
// Check Desktop notifs
|
||||
// Load invites
|
||||
}
|
||||
await this.fetchGuild();
|
||||
// Fetch group members on load
|
||||
this.members = await this.loadMembers({
|
||||
groupId: this.group._id,
|
||||
includeAllPublicFields: true,
|
||||
});
|
||||
this.$root.$on('updatedGroup', group => {
|
||||
let updatedGroup = extend(this.group, group);
|
||||
this.$set(this.group, updatedGroup);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Method for loading members of a group, with optional parameters for
|
||||
* modifying requests.
|
||||
*
|
||||
* @param {Object} payload Used for modifying requests for members
|
||||
*/
|
||||
loadMembers (payload = null) {
|
||||
// Remove unnecessary data
|
||||
if (payload && payload.challengeId) {
|
||||
delete payload.challengeId;
|
||||
}
|
||||
|
||||
return this.$store.dispatch('members:getGroupMembers', payload);
|
||||
},
|
||||
showMemberModal () {
|
||||
this.$root.$emit('habitica:show-member-modal', {
|
||||
groupId: this.group._id,
|
||||
group: this.group,
|
||||
memberCount: this.group.memberCount,
|
||||
viewingMembers: this.members,
|
||||
fetchMoreMembers: this.loadMembers,
|
||||
});
|
||||
},
|
||||
fetchRecentMessages () {
|
||||
this.fetchGuild();
|
||||
},
|
||||
updateGuild () {
|
||||
this.$store.state.editingGroup = this.group;
|
||||
this.$root.$emit('bv::show::modal', 'guild-form');
|
||||
},
|
||||
showInviteModal () {
|
||||
this.$root.$emit('inviteModal::inviteToGroup', this.group); // This event listener is initiated in ../header/index.vue
|
||||
},
|
||||
async fetchGuild () {
|
||||
if (this.searchId === 'party' && !this.user.party._id) {
|
||||
this.$root.$emit('bv::show::modal', 'create-party-modal');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isParty) {
|
||||
await this.$store.dispatch('party:getParty', true);
|
||||
this.group = this.$store.state.party.data;
|
||||
this.checkForAchievements();
|
||||
} else {
|
||||
const group = await this.$store.dispatch('guilds:getGroup', {groupId: this.searchId});
|
||||
this.$set(this, 'group', group);
|
||||
}
|
||||
|
||||
const groupId = this.searchId === 'party' ? this.user.party._id : this.searchId;
|
||||
if (this.hasUnreadMessages(groupId)) {
|
||||
// Delay by 1sec to make sure it returns after other requests that don't have the notification marked as read
|
||||
setTimeout(() => {
|
||||
this.$store.dispatch('chat:markChatSeen', {groupId});
|
||||
this.$delete(this.user.newMessages, groupId);
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
hasUnreadMessages (groupId) {
|
||||
if (this.user.newMessages[groupId]) return true;
|
||||
|
||||
return this.user.notifications.some(n => {
|
||||
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupId;
|
||||
});
|
||||
},
|
||||
checkForAchievements () {
|
||||
// Checks if user's party has reached 2 players for the first time.
|
||||
if (!this.user.achievements.partyUp && this.group.memberCount >= 2) {
|
||||
// @TODO
|
||||
// User.set({'achievements.partyUp':true});
|
||||
// Achievement.displayAchievement('partyUp');
|
||||
}
|
||||
|
||||
// Checks if user's party has reached 4 players for the first time.
|
||||
if (!this.user.achievements.partyOn && this.group.memberCount >= 4) {
|
||||
// @TODO
|
||||
// User.set({'achievements.partyOn':true});
|
||||
// Achievement.displayAchievement('partyOn');
|
||||
}
|
||||
},
|
||||
async join () {
|
||||
if (this.group.cancelledPlan && !confirm(this.$t('aboutToJoinCancelledGroupPlan'))) {
|
||||
return;
|
||||
}
|
||||
await this.$store.dispatch('guilds:join', {groupId: this.group._id, type: 'guild'});
|
||||
},
|
||||
clickLeave () {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Leave Party',
|
||||
});
|
||||
|
||||
// @TODO: Get challenges and ask to keep or remove
|
||||
if (!confirm('Are you sure you want to leave?')) return;
|
||||
let keep = true;
|
||||
this.leave(keep);
|
||||
},
|
||||
async leave (keepTasks) {
|
||||
let keepChallenges = 'remain-in-challenges';
|
||||
|
||||
let data = {
|
||||
groupId: this.group._id,
|
||||
keep: keepTasks,
|
||||
keepChallenges,
|
||||
};
|
||||
|
||||
if (this.isParty) {
|
||||
data.type = 'party';
|
||||
Analytics.updateUser({partySize: null, partyID: null});
|
||||
this.$store.state.partyMembers = [];
|
||||
}
|
||||
|
||||
await this.$store.dispatch('guilds:leave', data);
|
||||
|
||||
if (this.isParty) {
|
||||
this.$router.push({name: 'tasks'});
|
||||
}
|
||||
},
|
||||
upgradeGroup () {
|
||||
this.$store.state.upgradingGroup = this.group;
|
||||
this.$router.push('/group-plans');
|
||||
},
|
||||
showGroupGems () {
|
||||
this.$root.$emit('bv::show::modal', 'group-gems-modal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,454 @@
|
||||
<template lang="pug">
|
||||
b-modal#guild-form(:title="title", :hide-footer="true", size='lg')
|
||||
form(@submit.stop.prevent="submit")
|
||||
.form-group
|
||||
label
|
||||
strong(v-once) {{$t('name')}} *
|
||||
b-form-input(type="text", :placeholder="$t('newGuildPlaceholder')", v-model="workingGroup.name")
|
||||
.form-group
|
||||
label
|
||||
strong(v-once) {{$t('privacySettings')}} *
|
||||
br
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input#onlyLeaderCreatesChallenges(type="checkbox", v-model="workingGroup.onlyLeaderCreatesChallenges")
|
||||
label.custom-control-label(v-once, for="onlyLeaderCreatesChallenges") {{ $t('onlyLeaderCreatesChallenges') }}
|
||||
#groupPrivateDescription1.icon(:title="$t('privateDescription')")
|
||||
.svg-icon(v-html='icons.information')
|
||||
b-tooltip(
|
||||
:title="$t('onlyLeaderCreatesChallengesDetail')",
|
||||
target="groupPrivateDescription1",
|
||||
)
|
||||
|
||||
// br
|
||||
// @TODO Implement in V2 .custom-control.custom-checkbox
|
||||
input.custom-control-input(type="checkbox", v-model="workingGroup.guildLeaderCantBeMessaged")
|
||||
label.custom-control-label(v-once) {{ $t('guildLeaderCantBeMessaged') }}
|
||||
// "guildLeaderCantBeMessaged": "Leader can not be messaged directly",
|
||||
// @TODO discuss the impact of this with moderators before implementing
|
||||
|
||||
br
|
||||
.custom-control.custom-checkbox(v-if='!isParty && !this.workingGroup.id')
|
||||
input.custom-control-input#privateGuild(type="checkbox", v-model="workingGroup.privateGuild")
|
||||
label.custom-control-label(v-once, for="privateGuild") {{ $t('privateGuild') }}
|
||||
#groupPrivateDescription2.icon(:title="$t('privateDescription')")
|
||||
.svg-icon(v-html='icons.information')
|
||||
b-tooltip(
|
||||
:title="$t('privateDescription')",
|
||||
target="groupPrivateDescription2",
|
||||
)
|
||||
|
||||
// br
|
||||
// @TODO: Implement in v2 .custom-control.custom-checkbox(v-if='!creatingParty')
|
||||
input.custom-control-input(type="checkbox", v-model="workingGroup.allowGuildInvitationsFromNonMembers")
|
||||
label.custom-control-label(v-once) {{ $t('allowGuildInvitationsFromNonMembers') }}
|
||||
// "allowGuildInvitationsFromNonMembers": "Allow Guild invitations from non-members",
|
||||
|
||||
.form-group(v-if='!isParty')
|
||||
label
|
||||
strong(v-once) {{$t('guildSummary')}} *
|
||||
div.summary-count {{ $t('charactersRemaining', {characters: charactersRemaining}) }}
|
||||
textarea.form-control.summary-textarea(:placeholder="isParty ? $t('partyDescriptionPlaceholder') : $t('guildSummaryPlaceholder')", v-model="workingGroup.summary")
|
||||
// @TODO: need summary only for PUBLIC GUILDS, not for tavern, private guilds, or party
|
||||
|
||||
.form-group
|
||||
label
|
||||
strong(v-once) {{$t('groupDescription')}} *
|
||||
a.float-right(v-markdown='$t("markdownFormattingHelp")')
|
||||
textarea.form-control.description-textarea(type="text", textarea, :placeholder="isParty ? $t('partyDescriptionPlaceholder') : $t('guildDescriptionPlaceholder')", v-model="workingGroup.description")
|
||||
|
||||
.form-group(v-if='creatingParty && !workingGroup.id')
|
||||
span
|
||||
toggleSwitch(:label="$t('inviteMembersNow')", v-model='inviteMembers')
|
||||
|
||||
.form-group(style='position: relative;', v-if='!creatingParty && !isParty')
|
||||
label
|
||||
strong(v-once) {{$t('categories')}} *
|
||||
div.category-wrap(@click.prevent="toggleCategorySelect")
|
||||
span.category-select(v-if='workingGroup.categories.length === 0') {{$t('none')}}
|
||||
.category-label(v-for='category in workingGroup.categories') {{$t(categoriesHashByKey[category])}}
|
||||
.category-box(v-if="showCategorySelect")
|
||||
.form-check(
|
||||
v-for="group in categoryOptions",
|
||||
:key="group.key",
|
||||
v-if='group.key !== "habitica_official" || user.contributor.admin'
|
||||
)
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input(:id="`category-${group.key}`", type="checkbox", :value="group.key", v-model="workingGroup.categories")
|
||||
label.custom-control-label(v-once, :for="`category-${group.key}`") {{ $t(group.label) }}
|
||||
button.btn.btn-primary(@click.prevent="toggleCategorySelect") {{$t('close')}}
|
||||
// @TODO: need categories only for PUBLIC GUILDS, not for tavern, private guilds, or party
|
||||
|
||||
.form-group(v-if='inviteMembers && !workingGroup.id')
|
||||
label
|
||||
strong(v-once) Invite via Email or User ID
|
||||
p(v-once) {{$t('inviteMembersHowTo')}} *
|
||||
|
||||
div
|
||||
div(v-for='(member, index) in membersToInvite')
|
||||
input(type='text', v-model='member.value')
|
||||
button(@click.prevent='removeMemberToInvite(index)') Remove
|
||||
div
|
||||
input(type='text', placeholder='Email address or User ID', v-model='newMemberToInvite.value')
|
||||
button(@click.prevent='addMemberToInvite()') Add
|
||||
|
||||
.form-group.text-center
|
||||
div.item-with-icon(v-if='!this.workingGroup.id')
|
||||
.svg-icon(v-html="icons.gem")
|
||||
span.count 4
|
||||
button.btn.btn-primary.btn-md(v-if='!workingGroup.id', :disabled='!workingGroup.name || !workingGroup.description') {{ creatingParty ? $t('createParty') : $t('createGuild') }}
|
||||
button.btn.btn-primary.btn-md(v-if='workingGroup.id', :disabled='!workingGroup.name || !workingGroup.description') {{ isParty ? $t('updateParty') : $t('updateGuild') }}
|
||||
.gem-description(v-once, v-if='!this.workingGroup.id') {{ $t('guildGemCostInfo') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.custom-control-input {
|
||||
z-index: 1 !important;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.summary-count, .gem-description {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
margin-top: 1em;
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
.summary-count {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.gem-description {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.summary-textarea {
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
.description-textarea {
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
.item-with-icon {
|
||||
display: inline-block;
|
||||
|
||||
img {
|
||||
height: 20px;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-right: 1em;
|
||||
color: $green-10;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: .5em;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import toggleSwitch from 'client/components/ui/toggleSwitch';
|
||||
import groupMemberSearchDropdown from 'client/components/members/groupMemberSearchDropdown';
|
||||
import markdownDirective from 'client/directives/markdown';
|
||||
import gemIcon from 'assets/svg/gem.svg';
|
||||
import informationIcon from 'assets/svg/information.svg';
|
||||
|
||||
import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '../../../common/script/constants';
|
||||
|
||||
// @TODO: Not sure the best way to pass party creating status
|
||||
// Since we need the modal in the header, passing props doesn't work
|
||||
// because we can't import the create group in the index of groups
|
||||
// I think state is the correct spot, but maybe we should separate into
|
||||
// two modals?
|
||||
|
||||
export default {
|
||||
components: {
|
||||
toggleSwitch,
|
||||
groupMemberSearchDropdown,
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
data () {
|
||||
let data = {
|
||||
workingGroup: {
|
||||
id: '',
|
||||
name: '',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
summary: '',
|
||||
description: '',
|
||||
categories: [],
|
||||
onlyLeaderCreatesChallenges: true,
|
||||
guildLeaderCantBeMessaged: true,
|
||||
privateGuild: true,
|
||||
allowGuildInvitationsFromNonMembers: true,
|
||||
},
|
||||
categoryOptions: [
|
||||
{
|
||||
label: 'habitica_official',
|
||||
key: 'habitica_official',
|
||||
},
|
||||
{
|
||||
label: 'academics',
|
||||
key: 'academics',
|
||||
},
|
||||
{
|
||||
label: 'advocacy_causes',
|
||||
key: 'advocacy_causes',
|
||||
},
|
||||
{
|
||||
label: 'creativity',
|
||||
key: 'creativity',
|
||||
},
|
||||
{
|
||||
label: 'entertainment',
|
||||
key: 'entertainment',
|
||||
},
|
||||
{
|
||||
label: 'finance',
|
||||
key: 'finance',
|
||||
},
|
||||
{
|
||||
label: 'health_fitness',
|
||||
key: 'health_fitness',
|
||||
},
|
||||
{
|
||||
label: 'hobbies_occupations',
|
||||
key: 'hobbies_occupations',
|
||||
},
|
||||
{
|
||||
label: 'location_based',
|
||||
key: 'location_based',
|
||||
},
|
||||
{
|
||||
label: 'mental_health',
|
||||
key: 'mental_health',
|
||||
},
|
||||
{
|
||||
label: 'getting_organized',
|
||||
key: 'getting_organized',
|
||||
},
|
||||
{
|
||||
label: 'recovery_support_groups',
|
||||
key: 'recovery_support_groups',
|
||||
},
|
||||
{
|
||||
label: 'spirituality',
|
||||
key: 'spirituality',
|
||||
},
|
||||
{
|
||||
label: 'time_management',
|
||||
key: 'time_management',
|
||||
},
|
||||
],
|
||||
showCategorySelect: false,
|
||||
members: [],
|
||||
inviteMembers: false,
|
||||
newMemberToInvite: {
|
||||
value: '',
|
||||
type: '',
|
||||
},
|
||||
membersToInvite: [],
|
||||
};
|
||||
|
||||
let hashedCategories = {};
|
||||
data.categoryOptions.forEach((category) => {
|
||||
hashedCategories[category.key] = category.label;
|
||||
});
|
||||
data.categoriesHashByKey = hashedCategories;
|
||||
|
||||
data.icons = Object.freeze({
|
||||
gem: gemIcon,
|
||||
information: informationIcon,
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
editingGroup () {
|
||||
return this.$store.state.editingGroup;
|
||||
},
|
||||
charactersRemaining () {
|
||||
let currentLength = this.workingGroup.summary ? this.workingGroup.summary.length : 0;
|
||||
return MAX_SUMMARY_SIZE_FOR_GUILDS - currentLength;
|
||||
},
|
||||
title () {
|
||||
if (this.creatingParty) return this.$t('createParty');
|
||||
if (!this.workingGroup._id && !this.workingGroup.id) return this.$t('createGuild');
|
||||
if (this.isParty) return this.$t('updateParty');
|
||||
return this.$t('updateGuild');
|
||||
},
|
||||
creatingParty () {
|
||||
return this.$store.state.groupFormOptions.createParty;
|
||||
},
|
||||
isParty () {
|
||||
return this.workingGroup.type === 'party';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
editingGroup () {
|
||||
let editingGroup = this.editingGroup;
|
||||
|
||||
if (!editingGroup._id) {
|
||||
this.resetWorkingGroup();
|
||||
return;
|
||||
}
|
||||
|
||||
this.workingGroup.name = editingGroup.name;
|
||||
this.workingGroup.type = editingGroup.type;
|
||||
|
||||
this.workingGroup.privateGuild = true;
|
||||
if (editingGroup.privacy === 'public') {
|
||||
this.workingGroup.privateGuild = false;
|
||||
}
|
||||
|
||||
if (editingGroup.categories) {
|
||||
editingGroup.categories.forEach(category => {
|
||||
this.workingGroup.categories.push(category.slug);
|
||||
});
|
||||
}
|
||||
|
||||
if (editingGroup.summary) this.workingGroup.summary = editingGroup.summary;
|
||||
if (editingGroup.description) this.workingGroup.description = editingGroup.description;
|
||||
if (editingGroup._id) this.workingGroup.id = editingGroup._id;
|
||||
|
||||
this.workingGroup.onlyLeaderCreatesChallenges = editingGroup.leaderOnly.challenges;
|
||||
|
||||
this.workingGroup.leader = editingGroup.leader;
|
||||
|
||||
if (editingGroup._id) this.getMembers();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async getMembers () {
|
||||
if (!this.workingGroup.id) return;
|
||||
let members = await this.$store.dispatch('members:getGroupMembers', {
|
||||
groupId: this.workingGroup.id,
|
||||
includeAllPublicFields: true,
|
||||
});
|
||||
this.members = members;
|
||||
},
|
||||
addMemberToInvite () {
|
||||
// @TODO: determine type
|
||||
this.membersToInvite.push(this.newMemberToInvite);
|
||||
this.newMemberToInvite = {
|
||||
value: '',
|
||||
type: '',
|
||||
};
|
||||
},
|
||||
removeMemberToInvite (index) {
|
||||
this.membersToInvite.splice(index, 1);
|
||||
},
|
||||
toggleCategorySelect () {
|
||||
this.showCategorySelect = !this.showCategorySelect;
|
||||
},
|
||||
async submit () {
|
||||
if (this.$store.state.user.data.balance < 1 && !this.workingGroup.id) {
|
||||
// @TODO: Add proper notifications
|
||||
alert(this.$t('notEnoughGems'));
|
||||
return;
|
||||
// @TODO return $rootScope.openModal('buyGems', {track:"Gems > Gems > Create Group"});
|
||||
}
|
||||
|
||||
let errors = [];
|
||||
|
||||
if (!this.workingGroup.name) errors.push(this.$t('nameRequired'));
|
||||
if (!this.workingGroup.summary) errors.push(this.$t('summaryRequired'));
|
||||
if (this.workingGroup.summary.length > MAX_SUMMARY_SIZE_FOR_GUILDS) errors.push(this.$t('summaryTooLong'));
|
||||
if (!this.workingGroup.description) errors.push(this.$t('descriptionRequired'));
|
||||
if (!this.isParty && (!this.workingGroup.categories || this.workingGroup.categories.length === 0)) errors.push(this.$t('categoiresRequired'));
|
||||
|
||||
if (errors.length > 0) {
|
||||
alert(errors.join('\n'));
|
||||
return;
|
||||
}
|
||||
|
||||
// @TODO: Add proper notifications
|
||||
if (!this.workingGroup.id && !confirm(this.$t('confirmGuild'))) return;
|
||||
|
||||
if (!this.workingGroup.privateGuild) {
|
||||
this.workingGroup.privacy = 'public';
|
||||
}
|
||||
|
||||
this.workingGroup.leaderOnly = {
|
||||
challenges: this.workingGroup.onlyLeaderCreatesChallenges,
|
||||
};
|
||||
|
||||
let categoryKeys = this.workingGroup.categories;
|
||||
let serverCategories = [];
|
||||
categoryKeys.forEach(key => {
|
||||
let catName = this.categoriesHashByKey[key];
|
||||
serverCategories.push({
|
||||
slug: key,
|
||||
name: catName,
|
||||
});
|
||||
});
|
||||
this.workingGroup.categories = serverCategories;
|
||||
|
||||
let groupData = Object.assign({}, this.workingGroup);
|
||||
|
||||
let newgroup;
|
||||
if (groupData.id) {
|
||||
await this.$store.dispatch('guilds:update', {group: groupData});
|
||||
this.$root.$emit('updatedGroup', this.workingGroup);
|
||||
// @TODO: this doesn't work because of the async resource
|
||||
// if (updatedGroup.type === 'party') this.$store.state.party = {data: updatedGroup};
|
||||
} else {
|
||||
newgroup = await this.$store.dispatch('guilds:create', {group: groupData});
|
||||
this.$store.state.user.data.balance -= 1;
|
||||
}
|
||||
|
||||
this.$store.state.editingGroup = {};
|
||||
|
||||
this.workingGroup = {
|
||||
name: '',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
description: '',
|
||||
categories: [],
|
||||
onlyLeaderCreatesChallenges: true,
|
||||
guildLeaderCantBeMessaged: true,
|
||||
privateGuild: true,
|
||||
allowGuildInvitationsFromNonMembers: true,
|
||||
};
|
||||
|
||||
if (newgroup && newgroup._id) {
|
||||
this.$router.push(`/groups/guild/${newgroup._id}`);
|
||||
}
|
||||
this.$root.$emit('bv::hide::modal', 'guild-form');
|
||||
},
|
||||
resetWorkingGroup () {
|
||||
this.workingGroup = {
|
||||
id: '',
|
||||
name: '',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
summary: '',
|
||||
description: '',
|
||||
categories: [],
|
||||
onlyLeaderCreatesChallenges: true,
|
||||
guildLeaderCantBeMessaged: true,
|
||||
privateGuild: true,
|
||||
allowGuildInvitationsFromNonMembers: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,27 @@
|
||||
<template lang="pug">
|
||||
b-modal#group-gems-modal(:title="$t('groupGems')", size='md', :hide-footer="true")
|
||||
.modal-body
|
||||
.row
|
||||
.col-6.offset-3
|
||||
h3 {{ $t('groupGemsDesc') }}
|
||||
.modal-footer
|
||||
.col-12.text-center
|
||||
button.btn.btn-primary(@click='close()') {{$t('close')}}
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-body {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'group-gems-modal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,382 @@
|
||||
<template lang="pug">
|
||||
// @TODO: Move to group plans folder
|
||||
div
|
||||
div
|
||||
.header
|
||||
h1.text-center Need more for your Group?
|
||||
.row
|
||||
.col-8.offset-2.text-center
|
||||
h2.sub-text {{ $t('groupBenefitsDescription') }}
|
||||
.container.benefits
|
||||
.row
|
||||
.col-4
|
||||
.box
|
||||
img.box1(src='~client/assets/images/group-plans/group-14@3x.png')
|
||||
hr
|
||||
h2 {{ $t('teamBasedTasks') }}
|
||||
p Set up an easily-viewed shared task list for the group. Assign tasks to your fellow group members, or let them claim their own tasks to make it clear what everyone is working on!
|
||||
|
||||
.col-4
|
||||
.box
|
||||
img.box2(src='~client/assets/images/group-plans/group-12@3x.png')
|
||||
hr
|
||||
h2 Group Management Controls
|
||||
p Use task approvals to verify that a task that was really completed, add Group Managers to share responsibilities, and enjoy a private group chat for all team members.
|
||||
|
||||
.col-4
|
||||
.box
|
||||
img.box3(src='~client/assets/images/group-plans/group-13@3x.png')
|
||||
hr
|
||||
h2 In-Game Benefits
|
||||
p Group members get an exclusive Jackalope Mount, as well as full subscription benefits, including special monthly equipment sets and the ability to buy gems with gold.
|
||||
|
||||
#upgrading-group.container.payment-options(v-if='upgradingGroup._id')
|
||||
h1.text-center.purple-header Are you ready to upgrade?
|
||||
.row
|
||||
.col-12.text-center
|
||||
.purple-box
|
||||
.amount-section
|
||||
.dollar $
|
||||
.number 9
|
||||
.name Group Owner Subscription
|
||||
.plus
|
||||
.svg-icon(v-html="icons.positiveIcon")
|
||||
.amount-section
|
||||
.dollar $
|
||||
.number 3
|
||||
.name Each Individual Group Member
|
||||
|
||||
.box.payment-providers
|
||||
h3 Choose your payment method
|
||||
.payments-column
|
||||
button.purchase.btn.btn-primary.payment-button.payment-item(@click='pay(PAYMENTS.STRIPE)')
|
||||
.svg-icon.credit-card-icon(v-html="icons.creditCardIcon")
|
||||
| {{ $t('card') }}
|
||||
amazon-button.payment-item(:amazon-data="pay(PAYMENTS.AMAZON)")
|
||||
.container.col-6.offset-3.create-option(v-if='!upgradingGroup._id')
|
||||
.row
|
||||
h1.col-12.text-center.purple-header Create your Group today!
|
||||
.row
|
||||
.col-12.text-center
|
||||
button.btn.btn-primary.create-group(@click='launchModal("create")') Create Your New Group
|
||||
.row.pricing
|
||||
.col-5
|
||||
.dollar $
|
||||
.number 9
|
||||
.name
|
||||
div Group Owner
|
||||
div Subscription
|
||||
.col-1
|
||||
.plus +
|
||||
.col-6
|
||||
.dollar $
|
||||
.number 3
|
||||
.name
|
||||
div Each Additional
|
||||
div Member
|
||||
|
||||
b-modal#group-plan-modal(title="Select Payment", size='md', hide-footer=true)
|
||||
.col-12(v-if='activePage === PAGES.CREATE_GROUP')
|
||||
.form-group
|
||||
label.control-label(for='new-group-name') Name
|
||||
input.form-control#new-group-name.input-medium.option-content(required, type='text', placeholder="Name", v-model='newGroup.name')
|
||||
.form-group
|
||||
label(for='new-group-description') {{ $t('description') }}
|
||||
textarea.form-control#new-group-description.option-content(cols='3', :placeholder="$t('description')", v-model='newGroup.description')
|
||||
.form-group(v-if='type === "guild"')
|
||||
.custom-control.custom-radio
|
||||
input.custom-control-input(type='radio', name='new-group-privacy', value='private', v-model='newGroup.privacy')
|
||||
label.custom-control-label {{ $t('inviteOnly') }}
|
||||
.form-group
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input(type='checkbox', v-model='newGroup.leaderOnly.challenges' id='create-group-leaderOnlyChallenges-checkbox')
|
||||
label.custom-control-label(for='create-group-leaderOnlyChallenges-checkbox') {{ $t('leaderOnlyChallenges') }}
|
||||
.form-group(v-if='type === "party"')
|
||||
button.btn.btn-secondary.form-control(@click='createGroup()', :value="$t('createGroupPlan')")
|
||||
.form-group
|
||||
button.btn.btn-primary.btn-lg.btn-block(@click="createGroup()", :disabled="!newGroupIsReady") {{ $t('createGroupPlan') }}
|
||||
.col-12(v-if='activePage === PAGES.PAY')
|
||||
.text-center
|
||||
h3 Choose your payment method
|
||||
.payments-column.mx-auto
|
||||
button.purchase.btn.btn-primary.payment-button.payment-item(@click='pay(PAYMENTS.STRIPE)')
|
||||
.svg-icon.credit-card-icon(v-html="icons.creditCardIcon")
|
||||
| {{ $t('card') }}
|
||||
amazon-button.payment-item(:amazon-data="pay(PAYMENTS.AMAZON)")
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#upgrading-group {
|
||||
.amount-section {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dollar {
|
||||
position: absolute;
|
||||
left: -1em;
|
||||
top: 1em;
|
||||
}
|
||||
|
||||
.purple-box {
|
||||
color: #bda8ff;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.plus .svg-icon{
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.payment-providers {
|
||||
width: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #432874;
|
||||
background: linear-gradient(180deg, #4F2A93 0%, #432874 100%);
|
||||
color: #fff;
|
||||
padding: 2em;
|
||||
height: 340px;
|
||||
margin-bottom: 2em;
|
||||
margin-left: -12px;
|
||||
margin-right: -12px;
|
||||
|
||||
h1 {
|
||||
font-size: 48px;
|
||||
line-height: 1.16;
|
||||
margin-top: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
h2.sub-text {
|
||||
color: #D5C8FF;
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
line-height: 1.33;
|
||||
}
|
||||
}
|
||||
|
||||
.benefits {
|
||||
margin-top: -10em;
|
||||
|
||||
.box {
|
||||
height: 416px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #6133b4;
|
||||
}
|
||||
}
|
||||
|
||||
.box {
|
||||
border-radius: 2px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
padding: 2em;
|
||||
text-align: center;
|
||||
display: inline-block !important;
|
||||
vertical-align: bottom;
|
||||
margin-right: 1em;
|
||||
|
||||
img {
|
||||
margin: 0 auto;
|
||||
margin-top: 2em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
img.box1 {
|
||||
width: 266px;
|
||||
}
|
||||
|
||||
img.box2 {
|
||||
margin-top: 3.5em;
|
||||
width: 262px;
|
||||
margin-bottom: 3.7em;
|
||||
}
|
||||
|
||||
img.box3 {
|
||||
width: 225px;
|
||||
margin-bottom: 3.0em;
|
||||
}
|
||||
|
||||
button.create-group {
|
||||
width: 330px;
|
||||
height: 96px;
|
||||
}
|
||||
|
||||
.purple-header {
|
||||
color: #6133b4;
|
||||
font-size: 48px;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.pricing {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 4em;
|
||||
|
||||
.dollar, .number, .name {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
color: #a5a1ac;
|
||||
}
|
||||
|
||||
.plus {
|
||||
font-size: 34px;
|
||||
color: #a5a1ac;
|
||||
}
|
||||
|
||||
.dollar {
|
||||
margin-bottom: 1.5em;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 24px;
|
||||
margin-bottom: .8em;
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 72px;
|
||||
font-weight: bolder;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-options {
|
||||
margin-bottom: 4em;
|
||||
|
||||
.purple-box {
|
||||
background-color: #4f2a93;
|
||||
color: #fff;
|
||||
padding: .5em;
|
||||
border-radius: 8px;
|
||||
width: 200px;
|
||||
height: 215px;
|
||||
|
||||
.dollar {
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 60px;
|
||||
}
|
||||
|
||||
.name {
|
||||
width: 100px;
|
||||
margin-left: .3em;
|
||||
}
|
||||
|
||||
.plus {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.box, .purple-box {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import paymentsMixin from '../../mixins/payments';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import positiveIcon from 'assets/svg/positive.svg';
|
||||
import creditCardIcon from 'assets/svg/credit-card-icon.svg';
|
||||
import amazonButton from 'client/components/payments/amazonButton';
|
||||
|
||||
export default {
|
||||
mixins: [paymentsMixin],
|
||||
components: {
|
||||
amazonButton,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
amazonPayments: {},
|
||||
icons: Object.freeze({
|
||||
creditCardIcon,
|
||||
positiveIcon,
|
||||
}),
|
||||
PAGES: {
|
||||
CREATE_GROUP: 'create-group',
|
||||
UPGRADE_GROUP: 'upgrade-group',
|
||||
PAY: 'pay',
|
||||
},
|
||||
PAYMENTS: {
|
||||
AMAZON: 'amazon',
|
||||
STRIPE: 'stripe',
|
||||
},
|
||||
paymentMethod: '',
|
||||
newGroup: {
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
name: '',
|
||||
leaderOnly: {
|
||||
challenges: false,
|
||||
},
|
||||
},
|
||||
activePage: '',
|
||||
type: 'guild', // Guild or Party @TODO enum this
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.activePage = this.PAGES.BENEFITS;
|
||||
},
|
||||
computed: {
|
||||
newGroupIsReady () {
|
||||
return Boolean(this.newGroup.name);
|
||||
},
|
||||
upgradingGroup () {
|
||||
return this.$store.state.upgradingGroup;
|
||||
},
|
||||
// @TODO: can we move this to payment mixin?
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
launchModal () {
|
||||
this.changePage(this.PAGES.CREATE_GROUP);
|
||||
this.$root.$emit('bv::show::modal', 'group-plan-modal');
|
||||
},
|
||||
changePage (page) {
|
||||
this.activePage = page;
|
||||
window.scrollTo(0, 0);
|
||||
},
|
||||
createGroup () {
|
||||
this.changePage(this.PAGES.PAY);
|
||||
},
|
||||
pay (paymentMethod) {
|
||||
const subscriptionKey = 'group_monthly'; // @TODO: Get from content API?
|
||||
const paymentData = {
|
||||
subscription: subscriptionKey,
|
||||
coupon: null,
|
||||
};
|
||||
|
||||
if (this.upgradingGroup && this.upgradingGroup._id) {
|
||||
paymentData.groupId = this.upgradingGroup._id;
|
||||
paymentData.group = this.upgradingGroup;
|
||||
} else {
|
||||
paymentData.groupToCreate = this.newGroup;
|
||||
}
|
||||
|
||||
this.paymentMethod = paymentMethod;
|
||||
if (this.paymentMethod === this.PAYMENTS.STRIPE) {
|
||||
this.showStripe(paymentData);
|
||||
} else if (this.paymentMethod === this.PAYMENTS.AMAZON) {
|
||||
paymentData.type = 'subscription';
|
||||
return paymentData;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template lang="pug">
|
||||
.row
|
||||
secondary-menu.col-12
|
||||
router-link.nav-link(:to="{name: 'tavern'}", exact, :class="{'active': $route.name === 'tavern'}") {{ $t('tavern') }}
|
||||
router-link.nav-link(:to="{name: 'myGuilds'}", :class="{'active': $route.name === 'myGuilds'}") {{ $t('myGuilds') }}
|
||||
router-link.nav-link(:to="{name: 'guildsDiscovery'}", :class="{'active': $route.name === 'guildsDiscovery'}") {{ $t('guildsDiscovery') }}
|
||||
|
||||
.col-12
|
||||
router-view
|
||||
|
||||
group-form-modal
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import groupFormModal from './groupFormModal';
|
||||
import SecondaryMenu from 'client/components/secondaryMenu';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
groupFormModal,
|
||||
SecondaryMenu,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,199 @@
|
||||
<template lang="pug">
|
||||
b-modal#invite-modal(:title='$t(`inviteTo${groupType}`)', :hide-footer='true')
|
||||
div
|
||||
strong {{ $t('inviteEmailUsername') }}
|
||||
.small {{ $t('inviteEmailUsernameInfo') }}
|
||||
div(v-for='(invite, index) in invites')
|
||||
.input-group
|
||||
.d-flex.align-items-center.justify-content-center(v-if='index === invites.length - 1 && invite.text.length === 0')
|
||||
.svg-icon.positive-icon(v-html='icons.positiveIcon')
|
||||
input.form-control(
|
||||
type='text',
|
||||
:placeholder='$t("emailOrUsernameInvite")',
|
||||
v-model='invite.text',
|
||||
v-on:keyup='expandInviteList',
|
||||
v-on:change='checkInviteList',
|
||||
:class='{"input-valid": invite.valid, "is-invalid input-invalid": invite.valid === false}',
|
||||
)
|
||||
.input-error.text-center.mt-2(v-if="invite.error") {{ invite.error }}
|
||||
.modal-footer.d-flex.justify-content-center
|
||||
a.mr-3(@click='close()') {{ $t('cancel') }}
|
||||
button.btn.btn-primary(@click='sendInvites()', :class='{disabled: cannotSubmit}', :disabled='cannotSubmit') {{ $t('sendInvitations') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
#invite-modal___BV_modal_outer_ {
|
||||
.modal-content {
|
||||
padding: 0rem 0.25rem;
|
||||
}
|
||||
}
|
||||
#invite-modal___BV_modal_header_.modal-header {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
#invite-modal___BV_modal_header_ {
|
||||
.modal-title {
|
||||
color: #4F2A93;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
a:not([href]) {
|
||||
color: $blue-10;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border: 0px;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.input-error {
|
||||
color: $red-50;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
border-radius: 2px;
|
||||
border: solid 1px $gray-400;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: $gray-200;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.input-group:focus-within {
|
||||
border-color: $purple-500;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
.positive-icon {
|
||||
color: $green-10;
|
||||
width: 10px;
|
||||
margin: auto 0rem auto 1rem;
|
||||
}
|
||||
|
||||
.small {
|
||||
color: $gray-200;
|
||||
font-size: 12px;
|
||||
margin: 0.5rem 0rem 1rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import clone from 'lodash/clone';
|
||||
import debounce from 'lodash/debounce';
|
||||
import filter from 'lodash/filter';
|
||||
import forEach from 'lodash/forEach';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
import isUUID from 'validator/lib/isUUID';
|
||||
import notifications from 'client/mixins/notifications';
|
||||
import positiveIcon from 'assets/svg/positive.svg';
|
||||
|
||||
const INVITE_DEFAULTS = {text: '', error: null, valid: null};
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
cannotSubmit () {
|
||||
const filteredInvites = filter(this.invites, (invite) => {
|
||||
return invite.text.length > 0 && !invite.valid;
|
||||
});
|
||||
if (filteredInvites.length > 0) return true;
|
||||
},
|
||||
inviter () {
|
||||
return this.user.profile.name;
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
invites: [clone(INVITE_DEFAULTS), clone(INVITE_DEFAULTS)],
|
||||
icons: Object.freeze({
|
||||
positiveIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
checkInviteList: debounce(function checkList () {
|
||||
this.invites = filter(this.invites, (invite, index) => {
|
||||
return invite.text.length > 0 || index === this.invites.length - 1;
|
||||
});
|
||||
while (this.invites.length < 2) this.invites.push(clone(INVITE_DEFAULTS));
|
||||
forEach(this.invites, (value, index) => {
|
||||
if (value.text.length < 1 || isEmail(value.text)) {
|
||||
return this.fillErrors(index);
|
||||
}
|
||||
if (isUUID(value.text)) {
|
||||
this.$store.dispatch('user:userLookup', {uuid: value.text})
|
||||
.then(res => {
|
||||
return this.fillErrors(index, res);
|
||||
});
|
||||
} else {
|
||||
let searchUsername = value.text;
|
||||
if (searchUsername[0] === '@') searchUsername = searchUsername.slice(1, searchUsername.length);
|
||||
this.$store.dispatch('user:userLookup', {username: searchUsername})
|
||||
.then(res => {
|
||||
return this.fillErrors(index, res);
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 250),
|
||||
expandInviteList () {
|
||||
if (this.invites[this.invites.length - 1].text.length > 0) this.invites.push(clone(INVITE_DEFAULTS));
|
||||
},
|
||||
fillErrors (index, res) {
|
||||
if (!res || res.status === 200) {
|
||||
this.invites[index].error = null;
|
||||
if (this.invites[index].text.length < 1) return this.invites[index].valid = null;
|
||||
return this.invites[index].valid = true;
|
||||
}
|
||||
this.invites[index].error = res.response.data.message;
|
||||
return this.invites[index].valid = false;
|
||||
},
|
||||
close () {
|
||||
this.invites = [clone(INVITE_DEFAULTS), clone(INVITE_DEFAULTS)];
|
||||
this.$root.$emit('bv::hide::modal', 'invite-modal');
|
||||
},
|
||||
async sendInvites () {
|
||||
let invitationDetails = {
|
||||
inviter: this.inviter,
|
||||
emails: [],
|
||||
uuids: [],
|
||||
usernames: [],
|
||||
};
|
||||
forEach(this.invites, (invite) => {
|
||||
if (invite.text.length < 1) return;
|
||||
if (isEmail(invite.text)) {
|
||||
invitationDetails.emails.push({email: invite.text});
|
||||
} else if (isUUID(invite.text)) {
|
||||
invitationDetails.uuids.push(invite.text);
|
||||
} else {
|
||||
invitationDetails.usernames.push(invite.text);
|
||||
}
|
||||
});
|
||||
await this.$store.dispatch('guilds:invite', {
|
||||
invitationDetails,
|
||||
groupId: this.group._id,
|
||||
});
|
||||
|
||||
const invitesSent = invitationDetails.emails.length + invitationDetails.uuids.length + invitationDetails.usernames.length;
|
||||
let invitationString = invitesSent > 1 ? 'invitationsSent' : 'invitationSent';
|
||||
|
||||
this.text(this.$t(invitationString));
|
||||
this.close();
|
||||
},
|
||||
},
|
||||
mixins: [notifications],
|
||||
props: ['group', 'groupType'],
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,515 @@
|
||||
<template lang="pug">
|
||||
// @TODO: Move this to a member directory
|
||||
div
|
||||
remove-member-modal(:member-to-remove='memberToRemove', :group-id='this.groupId' @member-removed='memberRemoved')
|
||||
b-modal#members-modal(:title="$t('createGuild')", size='md', :hide-footer='true')
|
||||
.header-wrap(slot="modal-header")
|
||||
.row
|
||||
.col-6
|
||||
h1(v-once) {{$t('members')}}
|
||||
.col-6
|
||||
button(type="button" aria-label="Close" class="close", @click='close()')
|
||||
span(aria-hidden="true") ×
|
||||
.row.d-flex.align-items-center
|
||||
.col-4
|
||||
input.form-control.input-search(type="text", :placeholder="$t('search')", v-model='searchTerm')
|
||||
.col
|
||||
select.form-control(@change='changeSortOption($event)')
|
||||
option(v-for='sortOption in sortOptions', :value='sortOption.value') {{sortOption.text}}
|
||||
.col-3
|
||||
select.form-control(@change='changeSortDirection($event)')
|
||||
option(v-for='sortDirection in sortDirections', :value='sortDirection.value') {{sortDirection.text}}
|
||||
|
||||
.row.apply-options.d-flex.justify-content-center(v-if='sortDirty && group.type === "party"')
|
||||
a(@click='applySortOptions()') {{ $t('applySortToHeader') }}
|
||||
.row(v-if='invites.length > 0')
|
||||
.col-6.offset-3.nav
|
||||
.nav-item(@click='viewMembers()', :class="{active: selectedPage === 'members'}") {{ $t('members') }}
|
||||
.nav-item(@click='viewInvites()', :class="{active: selectedPage === 'invites'}") {{ $t('invites') }}
|
||||
div(v-if='selectedPage === "members"')
|
||||
.row(v-for='(member, index) in sortedMembers')
|
||||
.col-11.no-padding-left
|
||||
member-details(:member='member')
|
||||
.col-1.actions
|
||||
b-dropdown(right=true)
|
||||
.svg-icon.inline.dots(slot='button-content', v-html="icons.dots")
|
||||
b-dropdown-item(@click='sendMessage(member)')
|
||||
span.dropdown-icon-item
|
||||
.svg-icon.inline(v-html="icons.messageIcon")
|
||||
span.text {{$t('sendMessage')}}
|
||||
b-dropdown-item(@click='promoteToLeader(member)', v-if='shouldShowLeaderFunctions(member._id)')
|
||||
span.dropdown-icon-item
|
||||
.svg-icon.inline(v-html="icons.starIcon")
|
||||
span.text {{$t('promoteToLeader')}}
|
||||
b-dropdown-item(@click='addManager(member._id)', v-if='shouldShowAddManager(member._id)')
|
||||
span.dropdown-icon-item
|
||||
.svg-icon.inline(v-html="icons.starIcon")
|
||||
span.text {{$t('addManager')}}
|
||||
b-dropdown-item(@click='removeManager(member._id)', v-if='shouldShowRemoveManager(member._id)')
|
||||
span.dropdown-icon-item
|
||||
.svg-icon.inline(v-html="icons.removeIcon")
|
||||
span.text {{$t('removeManager2')}}
|
||||
b-dropdown-item(@click='viewProgress(member)', v-if='challengeId')
|
||||
span.dropdown-icon-item
|
||||
span.text {{ $t('viewProgress') }}
|
||||
b-dropdown-item(@click='removeMember(member, index)', v-if='shouldShowLeaderFunctions(member._id)')
|
||||
span.dropdown-icon-item
|
||||
.svg-icon.inline(v-html="icons.removeIcon")
|
||||
span.text {{$t('removeMember')}}
|
||||
.row(v-if='isLoadMoreAvailable')
|
||||
.col-12.text-center
|
||||
button.btn.btn-secondary(@click='loadMoreMembers()') {{ $t('loadMore') }}
|
||||
.row.gradient(v-if='members.length > 3')
|
||||
div(v-if='selectedPage === "invites"')
|
||||
.row(v-for='(member, index) in invites')
|
||||
.col-11.no-padding-left
|
||||
member-details(:member='member')
|
||||
.col-1.actions
|
||||
b-dropdown(right=true)
|
||||
.svg-icon.inline.dots(slot='button-content', v-html="icons.dots")
|
||||
b-dropdown-item(@click='removeInvite(member, index)', v-if='isLeader')
|
||||
span.dropdown-icon-item
|
||||
.svg-icon.inline(v-html="icons.removeIcon", v-if='isLeader')
|
||||
span.text {{$t('removeInvite')}}
|
||||
.modal-footer
|
||||
button.btn.btn-primary(@click='close()') {{ $t('close') }}
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
#members-modal {
|
||||
.modal-header {
|
||||
background-color: #edecee;
|
||||
border-radius: 8px 8px 0 0;
|
||||
box-shadow: 0 1px 2px 0 rgba(26, 24, 29, 0.24);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
background-color: #edecee;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.small-text, .character-name {
|
||||
color: #878190;
|
||||
}
|
||||
|
||||
.no-padding-left {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.member-details {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actions .dropdown-toggle::after {
|
||||
content: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.apply-options {
|
||||
padding: 1em;
|
||||
margin: 0;
|
||||
background-color: #f9f9f9;
|
||||
color: #2995cd;
|
||||
}
|
||||
|
||||
.header-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #4f2a93;
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding-top: 5em;
|
||||
|
||||
.dots {
|
||||
height: 16px;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
margin-left: -2em;
|
||||
margin-top: -2em;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
#members-modal_modal_body {
|
||||
padding: 0;
|
||||
max-height: 450px;
|
||||
|
||||
.col-8 {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.member-details {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.member-stats {
|
||||
width: 382px;
|
||||
height: 147px;
|
||||
}
|
||||
|
||||
.gradient {
|
||||
background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0), #ffffff);
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-icon-item .svg-icon {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
font-weight: bold;
|
||||
margin-bottom: .5em;
|
||||
margin-top: .5em;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
margin: 0 auto;
|
||||
padding: .5em;
|
||||
color: #878190;
|
||||
}
|
||||
|
||||
.nav-item:hover, .nav-item.active {
|
||||
color: #4f2a93;
|
||||
border-bottom: 2px solid #4f2a93;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
import removeMemberModal from 'client/components/members/removeMemberModal';
|
||||
import MemberDetails from '../memberDetails';
|
||||
import removeIcon from 'assets/members/remove.svg';
|
||||
import messageIcon from 'assets/members/message.svg';
|
||||
import starIcon from 'assets/members/star.svg';
|
||||
import dots from 'assets/svg/dots.svg';
|
||||
|
||||
export default {
|
||||
props: ['hideBadge'],
|
||||
components: {
|
||||
MemberDetails,
|
||||
removeMemberModal,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
sortOption: {},
|
||||
sortDirty: false,
|
||||
selectedPage: 'members',
|
||||
members: [],
|
||||
invites: [],
|
||||
memberToRemove: {},
|
||||
sortOptions: [
|
||||
{
|
||||
value: 'stats.class',
|
||||
text: this.$t('sortClass'),
|
||||
},
|
||||
{
|
||||
value: 'preferences.background',
|
||||
text: this.$t('sortBackground'),
|
||||
},
|
||||
{
|
||||
value: 'auth.timestamps.created',
|
||||
text: this.$t('sortDateJoined'),
|
||||
},
|
||||
{
|
||||
value: 'auth.timestamps.loggedin',
|
||||
text: this.$t('sortLogin'),
|
||||
},
|
||||
{
|
||||
value: 'stats.lvl',
|
||||
text: this.$t('sortLevel'),
|
||||
},
|
||||
{
|
||||
value: 'profile.name',
|
||||
text: this.$t('sortName'),
|
||||
},
|
||||
{
|
||||
value: 'contributor.level',
|
||||
text: this.$t('sortTier'),
|
||||
},
|
||||
],
|
||||
sortDirections: [
|
||||
{
|
||||
value: 'asc',
|
||||
text: this.$t('ascendingAbbrev'),
|
||||
},
|
||||
{
|
||||
value: 'desc',
|
||||
text: this.$t('descendingAbbrev'),
|
||||
},
|
||||
],
|
||||
searchTerm: '',
|
||||
icons: Object.freeze({
|
||||
removeIcon,
|
||||
messageIcon,
|
||||
starIcon,
|
||||
dots,
|
||||
}),
|
||||
userIdToMessage: '',
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('habitica:show-member-modal', (data) => {
|
||||
// @TODO: Remove store
|
||||
this.$store.state.memberModalOptions.challengeId = data.challengeId;
|
||||
this.$store.state.memberModalOptions.groupId = data.groupId;
|
||||
this.$store.state.memberModalOptions.group = data.group;
|
||||
this.$store.state.memberModalOptions.memberCount = data.memberCount;
|
||||
this.$store.state.memberModalOptions.viewingMembers = data.viewingMembers;
|
||||
this.$store.state.memberModalOptions.fetchMoreMembers = data.fetchMoreMembers;
|
||||
this.$root.$emit('bv::show::modal', 'members-modal');
|
||||
this.getMembers();
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
this.$root.$off('habitica:show-member-modal');
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
isLeader () {
|
||||
if (!this.group || !this.group.leader) return false;
|
||||
return this.user._id === this.group.leader || this.user._id === this.group.leader._id;
|
||||
},
|
||||
isAdmin () {
|
||||
return Boolean(this.user.contributor.admin);
|
||||
},
|
||||
isLoadMoreAvailable () {
|
||||
// Only available if the current length of `members` is less than the
|
||||
// total size of the Group/Challenge
|
||||
return this.members.length < this.$store.state.memberModalOptions.memberCount;
|
||||
},
|
||||
groupIsSubscribed () {
|
||||
return this.group.purchased && this.group.purchased.active;
|
||||
},
|
||||
group () {
|
||||
return this.$store.state.memberModalOptions.group;
|
||||
},
|
||||
groupId () {
|
||||
return this.$store.state.memberModalOptions.groupId || this.group._id;
|
||||
},
|
||||
challengeId () {
|
||||
return this.$store.state.memberModalOptions.challengeId;
|
||||
},
|
||||
sortedMembers () {
|
||||
let sortedMembers = this.members;
|
||||
|
||||
if (!isEmpty(this.sortOption)) {
|
||||
// Use the memberlist filtered by searchTerm
|
||||
sortedMembers = orderBy(sortedMembers, [this.sortOption.value], [this.sortOption.direction]);
|
||||
}
|
||||
|
||||
return sortedMembers;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
groupId () {
|
||||
// @TOOD: We might not need this since groupId is computed now
|
||||
this.getMembers();
|
||||
},
|
||||
challengeId () {
|
||||
this.getMembers();
|
||||
},
|
||||
group () {
|
||||
this.getMembers();
|
||||
},
|
||||
// Watches `searchTerm` and if present, performs a `searchMembers` action
|
||||
// and usual `getMembers` otherwise
|
||||
searchTerm () {
|
||||
if (this.searchTerm) {
|
||||
this.searchMembers(this.searchTerm);
|
||||
} else {
|
||||
this.getMembers();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sendMessage (member) {
|
||||
this.$root.$emit('habitica::new-inbox-message', {
|
||||
userIdToMessage: member._id,
|
||||
displayName: member.profile.name,
|
||||
username: member.auth.local.username,
|
||||
backer: member.backer,
|
||||
contributor: member.contributor,
|
||||
});
|
||||
},
|
||||
async searchMembers (searchTerm = '') {
|
||||
this.members = await this.$store.state.memberModalOptions.fetchMoreMembers({
|
||||
challengeId: this.challengeId,
|
||||
groupId: this.groupId,
|
||||
searchTerm,
|
||||
includeAllPublicFields: true,
|
||||
});
|
||||
},
|
||||
async getMembers () {
|
||||
let groupId = this.groupId;
|
||||
|
||||
if (groupId && groupId !== 'challenge') {
|
||||
let invites = await this.$store.dispatch('members:getGroupInvites', {
|
||||
groupId,
|
||||
includeAllPublicFields: true,
|
||||
});
|
||||
this.invites = invites;
|
||||
}
|
||||
|
||||
this.members = this.$store.state.memberModalOptions.viewingMembers;
|
||||
},
|
||||
async clickMember (uid, forceShow) {
|
||||
let user = this.$store.state.user.data;
|
||||
|
||||
if (user._id === uid && !forceShow) {
|
||||
if (this.$route.name === 'tasks') {
|
||||
this.$route.router.go('options.profile.avatar');
|
||||
return;
|
||||
}
|
||||
|
||||
this.$route.router.go('tasks');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.$store.dispatch('members:selectMember', {
|
||||
memberId: uid,
|
||||
});
|
||||
|
||||
this.$root.$emit('bv::show::modal', 'members-modal');
|
||||
},
|
||||
async removeMember (member, index) {
|
||||
this.memberToRemove = member;
|
||||
this.memberToRemove.index = index;
|
||||
this.$root.$emit('bv::show::modal', 'remove-member');
|
||||
},
|
||||
memberRemoved () {
|
||||
this.members.splice(this.memberToRemove.index, 1);
|
||||
this.group.memberCount -= 1;
|
||||
this.memberToRemove = {};
|
||||
},
|
||||
async quickReply (uid) {
|
||||
this.memberToReply = uid;
|
||||
await this.$store.dispatch('members:selectMember', {
|
||||
memberId: uid,
|
||||
});
|
||||
this.$root.$emit('bv::show::modal', 'private-message'); // MemberModalCtrl
|
||||
},
|
||||
async addManager (memberId) {
|
||||
await this.$store.dispatch('guilds:addManager', {
|
||||
groupId: this.groupId,
|
||||
memberId,
|
||||
});
|
||||
alert(this.$t('managerAdded'));
|
||||
},
|
||||
async removeManager (memberId) {
|
||||
await this.$store.dispatch('guilds:removeManager', {
|
||||
groupId: this.groupId,
|
||||
memberId,
|
||||
});
|
||||
alert(this.$t('managerRemoved'));
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'members-modal');
|
||||
},
|
||||
changeSortOption (e) {
|
||||
this.sortOption.value = e.target.value;
|
||||
this.sort();
|
||||
},
|
||||
changeSortDirection (e) {
|
||||
this.sortOption.direction = e.target.value;
|
||||
this.sort();
|
||||
},
|
||||
sort () {
|
||||
this.sortDirty = true;
|
||||
this.members = orderBy(this.members, [this.sortOption.value], [this.sortOption.direction]);
|
||||
},
|
||||
async applySortOptions () {
|
||||
const settings = {
|
||||
'party.order': this.sortOption.value,
|
||||
'party.orderAscending': this.sortOption.direction,
|
||||
};
|
||||
await this.$store.dispatch('user:set', settings);
|
||||
this.sortDirty = false;
|
||||
},
|
||||
async loadMoreMembers () {
|
||||
const lastMember = this.members[this.members.length - 1];
|
||||
if (!lastMember) return;
|
||||
|
||||
let newMembers = await this.$store.state.memberModalOptions.fetchMoreMembers({
|
||||
challengeId: this.challengeId,
|
||||
groupId: this.groupId,
|
||||
lastMemberId: lastMember._id,
|
||||
includeAllPublicFields: true,
|
||||
});
|
||||
|
||||
this.members = this.members.concat(newMembers);
|
||||
},
|
||||
viewMembers () {
|
||||
this.selectedPage = 'members';
|
||||
},
|
||||
viewInvites () {
|
||||
this.selectedPage = 'invites';
|
||||
},
|
||||
async removeInvite (member, index) {
|
||||
this.invites.splice(index, 1);
|
||||
await this.$store.dispatch('members:removeMember', {
|
||||
memberId: member._id,
|
||||
groupId: this.groupId,
|
||||
});
|
||||
this.viewMembers();
|
||||
},
|
||||
async promoteToLeader (member) {
|
||||
let groupData = Object.assign({}, this.group);
|
||||
|
||||
groupData.leader = member._id;
|
||||
await this.$store.dispatch('guilds:update', {group: groupData});
|
||||
|
||||
alert(this.$t('leaderChanged'));
|
||||
|
||||
groupData.leader = member;
|
||||
this.$root.$emit('updatedGroup', groupData);
|
||||
},
|
||||
viewProgress (member) {
|
||||
this.$root.$emit('habitica:challenge:member-progress', {
|
||||
progressMemberId: member._id,
|
||||
});
|
||||
},
|
||||
shouldShowAddManager (memberId) {
|
||||
if (!this.isLeader && !this.isAdmin) return false;
|
||||
if (memberId === this.group.leader || memberId === this.group.leader._id) return false;
|
||||
return this.groupIsSubscribed && !(this.group.managers && this.group.managers[memberId]);
|
||||
},
|
||||
shouldShowRemoveManager (memberId) {
|
||||
if (!this.isLeader && !this.isAdmin) return false;
|
||||
return this.group.managers && this.group.managers[memberId];
|
||||
},
|
||||
shouldShowLeaderFunctions (memberId) {
|
||||
return !this.challengeId && (this.isLeader || this.isAdmin) && this.user._id !== memberId;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,144 @@
|
||||
<template lang="pug">
|
||||
.row
|
||||
sidebar(v-on:search="updateSearch", v-on:filter="updateFilters")
|
||||
|
||||
.standard-page
|
||||
.row
|
||||
.col-md-8.text-left
|
||||
h1.page-header(v-once) {{ $t('myGuilds') }}
|
||||
h2(v-if='loading && guilds.length === 0') {{ $t('loading') }}
|
||||
.col-4
|
||||
button.btn.btn-secondary.create-group-button.float-right(@click='createGroup()')
|
||||
.svg-icon.positive-icon(v-html="icons.positiveIcon")
|
||||
span(v-once) {{$t('createGuild2')}}
|
||||
// @TODO: Add when we implement recent activity .float-right
|
||||
span.dropdown-label {{ $t('sortBy') }}
|
||||
b-dropdown(:text="$t('sort')", right=true)
|
||||
b-dropdown-item(v-for='sortOption in sortOptions', :key="sortOption.value", @click='sort(sortOption.value)') {{sortOption.text}}
|
||||
|
||||
.row
|
||||
.no-guilds.text-center.col-md-6.offset-md-3(v-if='!loading && guilds.length === 0')
|
||||
.svg-icon(v-html='icons.greyBadge')
|
||||
h2(v-once) {{$t('noGuildsTitle')}}
|
||||
p(v-once) {{$t('noGuildsParagraph1')}}
|
||||
p(v-once) {{$t('noGuildsParagraph2')}}
|
||||
|
||||
.row
|
||||
.no-guilds.text-center.col-md-6.offset-md-3(v-if='!loading && guilds.length > 0 && filteredGuilds.length === 0')
|
||||
h2(v-once) {{$t('noGuildsMatchFilters')}}
|
||||
|
||||
.row
|
||||
.col-md-12
|
||||
public-guild-item(v-for="guild in filteredGuilds", :key='guild._id', :guild="guild", :display-gem-bank='true')
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
.sort-select {
|
||||
margin: 2em;
|
||||
}
|
||||
|
||||
.positive-icon {
|
||||
color: $green-10;
|
||||
width: 10px;
|
||||
display: inline-block;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.no-guilds {
|
||||
color: $gray-200;
|
||||
margin-top: 10em;
|
||||
|
||||
h2 {
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 88.7px;
|
||||
margin: 1em auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import groupUtilities from 'client/mixins/groupsUtilities';
|
||||
|
||||
import MugenScroll from 'vue-mugen-scroll';
|
||||
|
||||
import PublicGuildItem from './publicGuildItem';
|
||||
import Sidebar from './sidebar';
|
||||
|
||||
import greyBadgeIcon from 'assets/svg/grey-badge.svg';
|
||||
import positiveIcon from 'assets/svg/positive.svg';
|
||||
|
||||
export default {
|
||||
mixins: [groupUtilities],
|
||||
components: { PublicGuildItem, MugenScroll, Sidebar },
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
greyBadge: greyBadgeIcon,
|
||||
positiveIcon,
|
||||
}),
|
||||
loading: false,
|
||||
search: '',
|
||||
filters: {},
|
||||
sort: 'none',
|
||||
sortOptions: [
|
||||
{
|
||||
text: this.$t('none'),
|
||||
value: 'none',
|
||||
},
|
||||
{
|
||||
text: this.$t('memberCount'),
|
||||
value: 'member_count',
|
||||
},
|
||||
{
|
||||
text: this.$t('recentActivity'),
|
||||
value: 'recent_activity',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
created () {
|
||||
this.fetchGuilds();
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
guilds: 'myGuilds',
|
||||
}),
|
||||
filteredGuilds () {
|
||||
let search = this.search;
|
||||
let filters = this.filters;
|
||||
let user = this.$store.state.user.data;
|
||||
let filterGuild = this.filterGuild;
|
||||
return this.guilds.filter((guild) => {
|
||||
if (guild.categories) {
|
||||
guild.categorySlugs = guild.categories.map(cat => {
|
||||
return cat.slug;
|
||||
});
|
||||
}
|
||||
return filterGuild(guild, filters, search, user);
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateSearch (eventData) {
|
||||
this.search = eventData.searchTerm;
|
||||
},
|
||||
updateFilters (eventData) {
|
||||
this.filters = eventData;
|
||||
},
|
||||
async fetchGuilds () {
|
||||
this.loading = true;
|
||||
await this.$store.dispatch('guilds:getMyGuilds');
|
||||
this.loading = false;
|
||||
},
|
||||
createGroup () {
|
||||
this.$store.state.editingGroup = {};
|
||||
this.$root.$emit('bv::show::modal', 'guild-form');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template lang="pug">
|
||||
div
|
||||
button.btn.btn-primary(b-btn, @click="$root.$emit('bv::show::modal','new-party-modal')") {{ $t('viewMembers') }}
|
||||
|
||||
b-modal#new-party-modal(:title="$t('createGuild')", size='lg')
|
||||
.header-wrap(slot="modal-header")
|
||||
.row
|
||||
.col-6
|
||||
h1(v-once) {{$t('members')}}
|
||||
.col-6
|
||||
button(type="button" aria-label="Close" class="close")
|
||||
span(aria-hidden="true") ×
|
||||
.row
|
||||
.form-group.col-6
|
||||
input.form-control.search(type="text", :placeholder="$t('search')", v-model='searchTerm')
|
||||
.col-4.offset-2
|
||||
span.dropdown-label {{ $t('sortBy') }}
|
||||
b-dropdown(:text="$t('sort')", right=true)
|
||||
b-dropdown-item(v-for='sortOption in sortOptions', @click='sort(sortOption.value)', :key='sortOption.value') {{sortOption.text}}
|
||||
</template>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template lang="pug">
|
||||
b-modal#participant-list(size='md', :hide-footer='true')
|
||||
.header-wrap(slot="modal-header")
|
||||
.row
|
||||
.col-6
|
||||
h1(v-once) {{ $t('participantsTitle') }}
|
||||
.col-6
|
||||
button(type="button" aria-label="Close" class="close", @click='close()')
|
||||
span(aria-hidden="true") ×
|
||||
.row(v-for='member in participants')
|
||||
.col-12.no-padding-left
|
||||
member-details(:member='member')
|
||||
.modal-footer
|
||||
button.btn.btn-primary(@click='close()') {{ $t('close') }}
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
#participant-list {
|
||||
.modal-header {
|
||||
background-color: #edecee;
|
||||
border-radius: 8px 8px 0 0;
|
||||
box-shadow: 0 1px 2px 0 rgba(26, 24, 29, 0.24);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
background-color: #edecee;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.small-text, .character-name {
|
||||
color: #878190;
|
||||
}
|
||||
|
||||
.no-padding-left {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.member-details {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.header-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #4f2a93;
|
||||
}
|
||||
|
||||
#participant-list_modal_body {
|
||||
padding: 0;
|
||||
max-height: 450px;
|
||||
|
||||
.member-details {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'client/libs/store';
|
||||
|
||||
import MemberDetails from '../memberDetails';
|
||||
|
||||
export default {
|
||||
props: ['group'],
|
||||
components: {
|
||||
MemberDetails,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
partyMembers: 'party:members',
|
||||
}),
|
||||
participants () {
|
||||
let partyMembers = this.partyMembers || [];
|
||||
return partyMembers.filter(member => this.group.quest.members[member._id] === true);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'participant-list');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,195 @@
|
||||
<template lang="pug">
|
||||
router-link.card-link(:to="{ name: 'guild', params: { groupId: guild._id } }")
|
||||
.card
|
||||
.card-body
|
||||
.row
|
||||
.col-md-2.badge-column
|
||||
.shield-wrap(:class="{gold: guild.memberCount >= 1000, silver: guild.memberCount >= 100 && guild.memberCount < 1000}")
|
||||
.svg-icon.shield(v-html="icons.goldGuildBadge", v-if='guild.memberCount >= 1000')
|
||||
.svg-icon.shield(v-html="icons.silverGuildBadgeIcon", v-if='guild.memberCount >= 100 && guild.memberCount < 1000')
|
||||
.svg-icon.shield(v-html="icons.bronzeGuildBadgeIcon", v-if='guild.memberCount < 100')
|
||||
.member-count {{ guild.memberCount | abbrNum }}
|
||||
.col-md-10
|
||||
.row
|
||||
.col-md-8
|
||||
router-link(:to="{ name: 'guild', params: { groupId: guild._id } }")
|
||||
h3 {{ guild.name }}
|
||||
p.summary(v-if='guild.summary') {{guild.summary.substr(0, MAX_SUMMARY_SIZE_FOR_GUILDS)}}
|
||||
p.summary(v-else) {{ guild.name }}
|
||||
.col-md-2.cta-container
|
||||
button.btn.btn-danger(v-if='isMember && displayLeave' @click.prevent='leave()', v-once) {{ $t('leave') }}
|
||||
button.btn.btn-success(v-if='!isMember' @click='join()', v-once) {{ $t('join') }}
|
||||
div.item-with-icon.gem-bank(v-if='displayGemBank')
|
||||
.svg-icon.gem(v-html="icons.gem")
|
||||
span.count {{ guild.balance * 4 }}
|
||||
div.guild-bank(v-if='displayGemBank', v-once) {{$t('guildBank')}}
|
||||
.row
|
||||
category-tags.col-md-12(:categories="guild.categories", :owner="isOwner", v-once)
|
||||
span.recommend-text(v-if='showSuggested(guild._id)') {{$t('suggestedGroup')}}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.card-link {
|
||||
color: #4E4A57 !important;
|
||||
}
|
||||
|
||||
.card-link:hover {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
min-height: 160px;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
box-shadow: 0 2px 2px 0 rgba($black, 0.15), 0 1px 4px 0 rgba($black, 0.1);
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.recommend-text {
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
line-height: 2;
|
||||
color: $gray-300;
|
||||
}
|
||||
|
||||
.cta-container {
|
||||
margin: 0 auto;
|
||||
|
||||
button {
|
||||
margin-top: 2.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.item-with-icon {
|
||||
.count {
|
||||
font-size: 20px;
|
||||
height: 37px;
|
||||
width: 37px;
|
||||
margin-left: .2em;
|
||||
}
|
||||
}
|
||||
|
||||
.shield {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.gold {
|
||||
color: #fdbb5a;
|
||||
|
||||
.member-count {
|
||||
color: #fdbb5a;
|
||||
}
|
||||
}
|
||||
|
||||
.silver {
|
||||
color: #c2c2c2;
|
||||
|
||||
.member-count {
|
||||
color: #c2c2c2;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-column {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.shield-wrap {
|
||||
display: inline-block;
|
||||
height: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.guild-bank {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
color: $gray-300;
|
||||
}
|
||||
|
||||
.member-count {
|
||||
position: relative;
|
||||
top: -3.7em;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
line-height: 1.1;
|
||||
text-align: center;
|
||||
color: #b36213;
|
||||
margin-top: 2.0em;
|
||||
}
|
||||
|
||||
.gem-bank {
|
||||
margin-top: 2em;
|
||||
|
||||
.gem {
|
||||
width: 25px;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
margin-right: .8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import categoryTags from '../categories/categoryTags';
|
||||
import groupUtilities from 'client/mixins/groupsUtilities';
|
||||
import markdown from 'client/directives/markdown';
|
||||
import gemIcon from 'assets/svg/gem.svg';
|
||||
import goldGuildBadgeIcon from 'assets/svg/gold-guild-badge-large.svg';
|
||||
import silverGuildBadgeIcon from 'assets/svg/silver-guild-badge-large.svg';
|
||||
import bronzeGuildBadgeIcon from 'assets/svg/bronze-guild-badge-large.svg';
|
||||
import { MAX_SUMMARY_SIZE_FOR_GUILDS } from 'common/script/constants';
|
||||
|
||||
export default {
|
||||
mixins: [groupUtilities],
|
||||
directives: {
|
||||
markdown,
|
||||
},
|
||||
props: ['guild', 'displayLeave', 'displayGemBank'],
|
||||
components: {
|
||||
categoryTags,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
isOwner () {
|
||||
return this.guild.leader && this.guild.leader === this.user._id;
|
||||
},
|
||||
isMember () {
|
||||
return this.isMemberOfGroup(this.user, this.guild);
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
MAX_SUMMARY_SIZE_FOR_GUILDS,
|
||||
icons: Object.freeze({
|
||||
gem: gemIcon,
|
||||
goldGuildBadge: goldGuildBadgeIcon,
|
||||
silverGuildBadgeIcon,
|
||||
bronzeGuildBadgeIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
showSuggested (guildId) {
|
||||
let habiticaHelpingGuildId = '5481ccf3-5d2d-48a9-a871-70a7380cee5a';
|
||||
let sixtyDaysAgoFromNow = moment().subtract(60, 'days');
|
||||
let isUserNew = moment(this.user.auth.timestamps.created).isAfter(sixtyDaysAgoFromNow);
|
||||
return guildId === habiticaHelpingGuildId && isUserNew;
|
||||
},
|
||||
async join () {
|
||||
// @TODO: This needs to be in the notifications where users will now accept invites
|
||||
if (this.guild.cancelledPlan && !confirm(window.env.t('aboutToJoinCancelledGroupPlan'))) {
|
||||
return;
|
||||
}
|
||||
await this.$store.dispatch('guilds:join', {groupId: this.guild._id, type: 'guild'});
|
||||
},
|
||||
async leave () {
|
||||
// @TODO: ask about challenges when we add challenges
|
||||
await this.$store.dispatch('guilds:leave', {groupId: this.guild._id, type: 'myGuilds'});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,224 @@
|
||||
<template lang="pug">
|
||||
b-modal#quest-details(title="Empty", size='md', :hide-footer="true", :hide-header="true")
|
||||
.left-panel.content
|
||||
h3.text-center {{ $t('participantsTitle') }}
|
||||
.row
|
||||
.col-10.offset-1.text-center
|
||||
span.description(v-once) {{ $t('participantDesc') }}
|
||||
.row
|
||||
.col-12.member(v-for='member in members')
|
||||
strong(:class="{'declined-name': member.accepted === false}") {{member.name}}
|
||||
.accepted.float-right(v-if='member.accepted === true') {{ $t('accepted') }}
|
||||
.declined.float-right(v-if='member.accepted === false') {{ $t('declined') }}
|
||||
.pending.float-right(v-if='member.accepted === null') {{ $t('pending') }}
|
||||
div(v-if='questData')
|
||||
questDialogContent(:item="questData")
|
||||
div.text-center.actions(v-if='canEditQuest')
|
||||
div
|
||||
button.btn.btn-secondary(v-once, @click="questConfirm()") {{ $t('begin') }}
|
||||
// @TODO don't allow the party leader to start the quest until the leader has accepted or rejected the invitation (users get confused and think "begin" means "join quest")
|
||||
div
|
||||
.cancel(v-once, @click="questCancel()") {{ $t('cancel') }}
|
||||
.side-panel(v-if='questData')
|
||||
questDialogDrops(:item="questData")
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
header {
|
||||
background-color: $white !important;
|
||||
border: none !important;
|
||||
|
||||
h5 {
|
||||
text-indent: -99999px;
|
||||
}
|
||||
}
|
||||
|
||||
.quest-details {
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
background: #4e4a57;
|
||||
color: $white;
|
||||
position: absolute;
|
||||
height: 460px;
|
||||
width: 320px;
|
||||
top: 2.5em;
|
||||
left: -22.8em;
|
||||
z-index: -1;
|
||||
padding: 2em;
|
||||
overflow-y: auto;
|
||||
h3 {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.selected .quest-wrapper {
|
||||
border: solid 1.5px #9a62ff;
|
||||
}
|
||||
|
||||
.quest-wrapper:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quest-col .quest-wrapper {
|
||||
background: $white;
|
||||
padding: .2em;
|
||||
margin-bottom: 1em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.description {
|
||||
text-align: center;
|
||||
color: #a5a1ac;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
position: absolute;
|
||||
right: -350px;
|
||||
top: 25px;
|
||||
border-radius: 8px;
|
||||
background-color: $gray-600;
|
||||
box-shadow: 0 2px 16px 0 rgba(26, 24, 29, 0.32);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 364px;
|
||||
z-index: -1;
|
||||
height: 93%;
|
||||
}
|
||||
|
||||
.member {
|
||||
padding: 1em .5em;
|
||||
border-top: 1px solid #686274;
|
||||
|
||||
.declined-name {
|
||||
color: #878190;
|
||||
}
|
||||
|
||||
.accepted {
|
||||
color: #1ed3a0;
|
||||
}
|
||||
|
||||
.declined {
|
||||
color: #f19595;
|
||||
}
|
||||
|
||||
.pending {
|
||||
color: #c3c0c7;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding-top: 2em;
|
||||
padding-bottom: 2em;
|
||||
|
||||
.cancel {
|
||||
color: #f74e52;
|
||||
margin-top: 3em;
|
||||
}
|
||||
|
||||
.cancel:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'client/libs/store';
|
||||
import quests from 'common/script/content/quests';
|
||||
|
||||
import copyIcon from 'assets/svg/copy.svg';
|
||||
import greyBadgeIcon from 'assets/svg/grey-badge.svg';
|
||||
import qrCodeIcon from 'assets/svg/qrCode.svg';
|
||||
import facebookIcon from 'assets/svg/facebook.svg';
|
||||
import twitterIcon from 'assets/svg/twitter.svg';
|
||||
import starIcon from 'assets/svg/star.svg';
|
||||
import goldIcon from 'assets/svg/gold.svg';
|
||||
import difficultyStarIcon from 'assets/svg/difficulty-star.svg';
|
||||
import questDialogDrops from '../shops/quests/questDialogDrops';
|
||||
import questDialogContent from '../shops/quests/questDialogContent';
|
||||
|
||||
export default {
|
||||
props: ['group'],
|
||||
components: {
|
||||
questDialogDrops,
|
||||
questDialogContent,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
selectedQuest: {},
|
||||
icons: Object.freeze({
|
||||
copy: copyIcon,
|
||||
greyBadge: greyBadgeIcon,
|
||||
qrCode: qrCodeIcon,
|
||||
facebook: facebookIcon,
|
||||
twitter: twitterIcon,
|
||||
starIcon,
|
||||
goldIcon,
|
||||
difficultyStarIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
}),
|
||||
...mapGetters({
|
||||
partyMembers: 'party:members',
|
||||
}),
|
||||
questData () {
|
||||
return quests.quests[this.group.quest.key];
|
||||
},
|
||||
members () {
|
||||
let partyMembers = this.partyMembers || [];
|
||||
return partyMembers.map(member => {
|
||||
return {
|
||||
name: member.profile.name,
|
||||
accepted: this.group.quest.members[member._id],
|
||||
};
|
||||
});
|
||||
},
|
||||
canEditQuest () {
|
||||
if (!this.group.quest) return false;
|
||||
let isQuestLeader = this.group.quest.leader === this.user._id;
|
||||
let isPartyLeader = this.group.leader._id === this.user._id;
|
||||
return isQuestLeader || isPartyLeader;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async questConfirm () {
|
||||
let count = 0;
|
||||
for (let uuid in this.group.quest.members) {
|
||||
if (this.group.quest.members[uuid]) count += 1;
|
||||
}
|
||||
if (!confirm(this.$t('questConfirm', { questmembers: count, totalmembers: this.group.memberCount}))) return;
|
||||
this.questForceStart();
|
||||
},
|
||||
async questForceStart () {
|
||||
let quest = await this.$store.dispatch('quests:sendAction', {groupId: this.group._id, action: 'quests/force-start'});
|
||||
this.group.quest = quest;
|
||||
this.close();
|
||||
},
|
||||
async questCancel () {
|
||||
if (!confirm(this.$t('sureCancel'))) return;
|
||||
let quest = await this.$store.dispatch('quests:sendAction', {groupId: this.group._id, action: 'quests/cancel'});
|
||||
this.group.quest = quest;
|
||||
this.close();
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'quest-details');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,294 @@
|
||||
<template lang="pug">
|
||||
sidebar-section(:title="$t('questDetailsTitle')")
|
||||
.row.no-quest-section(v-if='!onPendingQuest && !onActiveQuest')
|
||||
.col-12.text-center
|
||||
.svg-icon(v-html="icons.questIcon")
|
||||
h4(v-once) {{ $t('youAreNotOnQuest') }}
|
||||
p(v-once) {{ $t('questDescription') }}
|
||||
button.btn.btn-secondary(v-once, @click="openStartQuestModal()") {{ $t('startAQuest') }}
|
||||
.row.quest-active-section(v-if='onPendingQuest && !onActiveQuest')
|
||||
.col-2
|
||||
.quest(:class='`inventory_quest_scroll_${questData.key}`')
|
||||
.col-6.titles
|
||||
strong {{ questData.text() }}
|
||||
p {{acceptedCount}} / {{group.memberCount}}
|
||||
.col-4
|
||||
button.btn.btn-secondary(@click="openQuestDetails()") {{ $t('details') }}
|
||||
.row.quest-active-section.quest-invite(v-if='user.party.quest && user.party.quest.RSVPNeeded')
|
||||
span {{ $t('wouldYouParticipate') }}
|
||||
button.btn.btn-primary.accept(@click='questAccept(group._id)') {{$t('accept')}}
|
||||
button.btn.btn-primary.reject(@click='questReject(group._id)') {{$t('reject')}}
|
||||
.row.quest-active-section(v-if='!onPendingQuest && onActiveQuest')
|
||||
.col-12.text-center
|
||||
.quest-boss(:class="'quest_' + questData.key")
|
||||
h3(v-once) {{ questData.text() }}
|
||||
.quest-box
|
||||
.collect-info(v-if='questData.collect')
|
||||
.row
|
||||
.col-12
|
||||
a.float-right(@click="openParticipantList()") {{ $t('participantsTitle') }}
|
||||
.row(v-for='(value, key) in questData.collect')
|
||||
.col-2
|
||||
div(:class="'quest_' + questData.key + '_' + key")
|
||||
.col-10
|
||||
strong {{value.text()}}
|
||||
.grey-progress-bar
|
||||
.collect-progress-bar(:style="{width: (group.quest.progress.collect[key] / value.count) * 100 + '%'}")
|
||||
strong {{group.quest.progress.collect[key]}} / {{value.count}}
|
||||
div.text-right(v-if='userIsOnQuest') {{parseFloat(user.party.quest.progress.collectedItems) || 0}} items found
|
||||
.boss-info(v-if='questData.boss')
|
||||
.row
|
||||
.col-6
|
||||
h4.float-left(v-once) {{ questData.boss.name() }}
|
||||
.col-6
|
||||
a.float-right(@click="openParticipantList()") {{ $t('participantsTitle') }}
|
||||
.row
|
||||
.col-12
|
||||
.grey-progress-bar
|
||||
.boss-health-bar(:style="{width: bossHpPercent + '%'}")
|
||||
.row.boss-details
|
||||
.col-6
|
||||
span.float-left
|
||||
| {{ Math.ceil(parseFloat(group.quest.progress.hp) * 100) / 100 }} / {{ parseFloat(questData.boss.hp).toFixed(2) }}
|
||||
// current boss hp uses ceil so you don't underestimate damage needed to end quest
|
||||
.col-6(v-if='userIsOnQuest')
|
||||
// @TODO: Why do we not sync quest progress on the group doc? Each user could have different progress.
|
||||
span.float-right {{ user.party.quest.progress.up | floor(10) }} {{ $t('pendingDamageLabel') }}
|
||||
// player's pending damage uses floor so you don't overestimate damage you've already done
|
||||
.row.rage-bar-row(v-if='questData.boss.rage')
|
||||
.col-12
|
||||
.grey-progress-bar
|
||||
.boss-health-bar.rage-bar(:style="{width: (group.quest.progress.rage / questData.boss.rage.value) * 100 + '%'}")
|
||||
.row.boss-details.rage-details(v-if='questData.boss.rage')
|
||||
.col-6
|
||||
span.float-left {{ $t('rage') }} {{ parseFloat(group.quest.progress.rage).toFixed(2) }} / {{ questData.boss.rage.value }}
|
||||
button.btn.btn-secondary(v-once, @click="questAbort()", v-if='canEditQuest') {{ $t('abort') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.svg-icon {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.quest-boss {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.boss-health-bar {
|
||||
width: 80%;
|
||||
background-color: red;
|
||||
height: 15px;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
.rage-details {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.boss-health-bar.rage-bar {
|
||||
margin-top: 1em;
|
||||
background-color: orange;
|
||||
}
|
||||
|
||||
.grey-progress-bar {
|
||||
width: 100%;
|
||||
height: 15px;
|
||||
background-color: #e1e0e3;
|
||||
}
|
||||
|
||||
.collect-progress-bar {
|
||||
background-color: #24cc8f;
|
||||
height: 15px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.no-quest-section {
|
||||
padding: 2em;
|
||||
color: $gray-300;
|
||||
|
||||
h4 {
|
||||
color: $gray-300;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.quest-active-section {
|
||||
.titles {
|
||||
padding-top: .5em;
|
||||
}
|
||||
|
||||
.quest-box {
|
||||
background-image: url('~client/assets/svg/for-css/quest-border.svg');
|
||||
background-size: 100% 100%;
|
||||
width: 100%;
|
||||
padding: .5em;
|
||||
margin-bottom: 1em;
|
||||
|
||||
a {
|
||||
font-family: 'Roboto Condensed', sans-serif;
|
||||
font-weight: bold;
|
||||
color: $gray-10;
|
||||
}
|
||||
|
||||
svg: {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.boss-info, .collect-info {
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.quest-invite {
|
||||
background-color: #2995cd;
|
||||
color: #fff;
|
||||
padding: 1em;
|
||||
|
||||
span {
|
||||
margin-top: .3em;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.accept, .reject {
|
||||
padding: .2em 1em;
|
||||
font-size: 12px;
|
||||
height: 24px;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
}
|
||||
|
||||
.accept {
|
||||
background-color: #24cc8f;
|
||||
margin-left: 4em;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.reject {
|
||||
border-radius: 2px;
|
||||
background-color: #f74e52;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
import quests from 'common/script/content/quests';
|
||||
import percent from 'common/script/libs/percent';
|
||||
import sidebarSection from '../sidebarSection';
|
||||
|
||||
import questIcon from 'assets/svg/quest.svg';
|
||||
|
||||
export default {
|
||||
props: ['group'],
|
||||
components: {
|
||||
sidebarSection,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
questIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
userIsOnQuest () {
|
||||
if (!this.group.quest || !this.group.quest.members) return false;
|
||||
return Boolean(this.group.quest.members[this.user._id]);
|
||||
},
|
||||
onPendingQuest () {
|
||||
return Boolean(this.group.quest.key) && !this.group.quest.active;
|
||||
},
|
||||
onActiveQuest () {
|
||||
return this.group.quest.active;
|
||||
},
|
||||
bossHpPercent () {
|
||||
return percent(this.group.quest.progress.hp, this.questData.boss.hp);
|
||||
},
|
||||
questData () {
|
||||
if (!this.group.quest) return {};
|
||||
return quests.quests[this.group.quest.key];
|
||||
},
|
||||
canEditQuest () {
|
||||
if (!this.group.quest) return false;
|
||||
let isQuestLeader = this.group.quest.leader === this.user._id;
|
||||
let isPartyLeader = this.group.leader._id === this.user._id;
|
||||
return isQuestLeader || isPartyLeader;
|
||||
},
|
||||
isMemberOfPendingQuest () {
|
||||
let userid = this.user._id;
|
||||
let group = this.group;
|
||||
if (!group.quest || !group.quest.members) return false;
|
||||
if (group.quest.active) return false; // quest is started, not pending
|
||||
return userid in group.quest.members && group.quest.members[userid] !== false;
|
||||
},
|
||||
isMemberOfRunningQuest () {
|
||||
let userid = this.user._id;
|
||||
let group = this.group;
|
||||
if (!group.quest || !group.quest.members) return false;
|
||||
if (!group.quest.active) return false; // quest is pending, not started
|
||||
return group.quest.members[userid];
|
||||
},
|
||||
acceptedCount () {
|
||||
let count = 0;
|
||||
|
||||
if (!this.group || !this.group.quest) return count;
|
||||
|
||||
for (let uuid in this.group.quest.members) {
|
||||
if (this.group.quest.members[uuid]) count += 1;
|
||||
}
|
||||
|
||||
return count;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openStartQuestModal () {
|
||||
this.$root.$emit('bv::show::modal', 'start-quest-modal');
|
||||
},
|
||||
openQuestDetails () {
|
||||
this.$root.$emit('bv::show::modal', 'quest-details');
|
||||
},
|
||||
openParticipantList () {
|
||||
this.$root.$emit('bv::show::modal', 'participant-list');
|
||||
},
|
||||
async questAbort () {
|
||||
if (!confirm(this.$t('sureAbort'))) return;
|
||||
if (!confirm(this.$t('doubleSureAbort'))) return;
|
||||
let quest = await this.$store.dispatch('quests:sendAction', {groupId: this.group._id, action: 'quests/abort'});
|
||||
this.group.quest = quest;
|
||||
},
|
||||
async questLeave () {
|
||||
if (!confirm(this.$t('sureLeave'))) return;
|
||||
let quest = await this.$store.dispatch('quests:sendAction', {groupId: this.group._id, action: 'quests/leave'});
|
||||
this.group.quest = quest;
|
||||
},
|
||||
async questAccept (partyId) {
|
||||
let quest = await this.$store.dispatch('quests:sendAction', {groupId: partyId, action: 'quests/accept'});
|
||||
this.user.party.quest = quest;
|
||||
},
|
||||
async questReject (partyId) {
|
||||
let quest = await this.$store.dispatch('quests:sendAction', {groupId: partyId, action: 'quests/reject'});
|
||||
this.user.party.quest = quest;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,157 @@
|
||||
<template lang="pug">
|
||||
.standard-sidebar.d-none.d-sm-block
|
||||
.form-group
|
||||
input.form-control.search(type="text", :placeholder="$t('search')", v-model='searchTerm')
|
||||
|
||||
form
|
||||
h2(v-once) {{ $t('filter') }}
|
||||
.form-group
|
||||
h3 {{ $t('category') }}
|
||||
.form-check(
|
||||
v-for="group in categoryOptions",
|
||||
:key="group.key",
|
||||
)
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input(type="checkbox", :value='group.key' v-model="categoryFilters", :id="group.key")
|
||||
label.custom-control-label(v-once, :for="group.key") {{ $t(group.label) }}
|
||||
.form-group
|
||||
h3 {{ $t('role') }}
|
||||
.form-check(
|
||||
v-for="group in roleOptions",
|
||||
:key="group.key",
|
||||
)
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input(type="checkbox", :value='group.key' v-model="roleFilters", :id="group.key")
|
||||
label.custom-control-label(v-once, :for="group.key") {{ $t(group.label) }}
|
||||
.form-group
|
||||
h3 {{ $t('guildSize') }}
|
||||
.form-check(
|
||||
v-for="group in guildSizeOptions",
|
||||
:key="group.key",
|
||||
)
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input(type="checkbox", :value='group.key' v-model="guildSizeFilters", :id="group.key")
|
||||
label.custom-control-label(v-once, :for="group.key") {{ $t(group.label) }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import throttle from 'lodash/throttle';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
categoryFilters: [],
|
||||
categoryOptions: [
|
||||
{
|
||||
label: 'habitica_official',
|
||||
key: 'habitica_official',
|
||||
},
|
||||
{
|
||||
label: 'academics',
|
||||
key: 'academics',
|
||||
},
|
||||
{
|
||||
label: 'advocacy_causes',
|
||||
key: 'advocacy_causes',
|
||||
},
|
||||
{
|
||||
label: 'creativity',
|
||||
key: 'creativity',
|
||||
},
|
||||
{
|
||||
label: 'entertainment',
|
||||
key: 'entertainment',
|
||||
},
|
||||
{
|
||||
label: 'finance',
|
||||
key: 'finance',
|
||||
},
|
||||
{
|
||||
label: 'health_fitness',
|
||||
key: 'health_fitness',
|
||||
},
|
||||
{
|
||||
label: 'hobbies_occupations',
|
||||
key: 'hobbies_occupations',
|
||||
},
|
||||
{
|
||||
label: 'location_based',
|
||||
key: 'location_based',
|
||||
},
|
||||
{
|
||||
label: 'mental_health',
|
||||
key: 'mental_health',
|
||||
},
|
||||
{
|
||||
label: 'getting_organized',
|
||||
key: 'getting_organized',
|
||||
},
|
||||
{
|
||||
label: 'recovery_support_groups',
|
||||
key: 'recovery_support_groups',
|
||||
},
|
||||
{
|
||||
label: 'spirituality',
|
||||
key: 'spirituality',
|
||||
},
|
||||
{
|
||||
label: 'time_management',
|
||||
key: 'time_management',
|
||||
},
|
||||
],
|
||||
roleFilters: [],
|
||||
roleOptions: [
|
||||
{
|
||||
label: 'guildLeader',
|
||||
key: 'guild_leader',
|
||||
},
|
||||
{
|
||||
label: 'member',
|
||||
key: 'member',
|
||||
},
|
||||
],
|
||||
guildSizeFilters: [],
|
||||
guildSizeOptions: [
|
||||
{
|
||||
label: 'goldTier',
|
||||
key: 'gold_tier',
|
||||
},
|
||||
{
|
||||
label: 'silverTier',
|
||||
key: 'silver_tier',
|
||||
},
|
||||
{
|
||||
label: 'bronzeTier',
|
||||
key: 'bronze_tier',
|
||||
},
|
||||
],
|
||||
searchTerm: '',
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
categoryFilters: function categoryFilters () {
|
||||
this.emitFilters();
|
||||
},
|
||||
roleFilters: function roleFilters () {
|
||||
this.emitFilters();
|
||||
},
|
||||
guildSizeFilters: function guildSizeFilters () {
|
||||
this.emitFilters();
|
||||
},
|
||||
searchTerm: throttle(function searchTerm (newSearch) {
|
||||
this.$emit('search', {
|
||||
searchTerm: newSearch,
|
||||
});
|
||||
}, 1000),
|
||||
},
|
||||
methods: {
|
||||
emitFilters () {
|
||||
this.$emit('filter', {
|
||||
categories: this.categoryFilters,
|
||||
roles: this.roleFilters,
|
||||
guildSize: this.guildSizeFilters,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,197 @@
|
||||
<template lang="pug">
|
||||
b-modal#start-quest-modal(title="Empty", size='md', :hide-footer="true", :hide-header="true")
|
||||
.left-panel.content
|
||||
h3.text-center Quests
|
||||
.row
|
||||
.col-4.quest-col(
|
||||
v-for='(value, key, index) in user.items.quests',
|
||||
@click='selectQuest({key})',
|
||||
:class="{selected: key === selectedQuest}", v-if='value > 0')
|
||||
.quest-wrapper
|
||||
b-popover(
|
||||
:target="`inventory_quest_scroll_${key}`"
|
||||
placement="top"
|
||||
triggers="hover")
|
||||
h4.popover-content-title {{ quests.quests[key].text() }}
|
||||
questInfo(:quest="quests.quests[key]")
|
||||
.quest(:class="`inventory_quest_scroll_${key}`", :id="`inventory_quest_scroll_${key}`")
|
||||
.row
|
||||
.col-10.offset-1.text-center
|
||||
span.description(v-once) {{ $t('noQuestToStart') }}
|
||||
div(v-if='questData')
|
||||
questDialogContent(:item="questData")
|
||||
div.text-center
|
||||
button.btn.btn-primary(@click='questInit()', :disabled="!Boolean(selectedQuest) || loading") {{$t('inviteToPartyOrQuest')}}
|
||||
div.text-center
|
||||
p {{$t('inviteInformation')}}
|
||||
.side-panel(v-if='questData')
|
||||
questDialogDrops(:item="questData")
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
header {
|
||||
background-color: $white !important;
|
||||
border: none !important;
|
||||
|
||||
h5 {
|
||||
text-indent: -99999px;
|
||||
}
|
||||
}
|
||||
|
||||
.quest-details {
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
background: #4e4a57;
|
||||
color: $white;
|
||||
position: absolute;
|
||||
height: 460px;
|
||||
width: 320px;
|
||||
top: 2.5em;
|
||||
left: -23em;
|
||||
z-index: -1;
|
||||
padding: 2em;
|
||||
overflow-y: auto;
|
||||
|
||||
h3 {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.selected .quest-wrapper {
|
||||
border: solid 1.5px #9a62ff;
|
||||
}
|
||||
|
||||
.quest-wrapper:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quest-col .quest-wrapper {
|
||||
background: $white;
|
||||
padding: .2em;
|
||||
margin-bottom: 1em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.description {
|
||||
text-align: center;
|
||||
color: #a5a1ac;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
position: absolute;
|
||||
right: -350px;
|
||||
top: 25px;
|
||||
border-radius: 8px;
|
||||
background-color: $gray-600;
|
||||
box-shadow: 0 2px 16px 0 rgba(26, 24, 29, 0.32);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 364px;
|
||||
z-index: -1;
|
||||
height: 93%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
|
||||
import quests from 'common/script/content/quests';
|
||||
|
||||
import copyIcon from 'assets/svg/copy.svg';
|
||||
import greyBadgeIcon from 'assets/svg/grey-badge.svg';
|
||||
import qrCodeIcon from 'assets/svg/qrCode.svg';
|
||||
import facebookIcon from 'assets/svg/facebook.svg';
|
||||
import twitterIcon from 'assets/svg/twitter.svg';
|
||||
import starIcon from 'assets/svg/star.svg';
|
||||
import goldIcon from 'assets/svg/gold.svg';
|
||||
import difficultyStarIcon from 'assets/svg/difficulty-star.svg';
|
||||
import questDialogDrops from '../shops/quests/questDialogDrops';
|
||||
import questDialogContent from '../shops/quests/questDialogContent';
|
||||
import QuestInfo from '../shops/quests/questInfo';
|
||||
|
||||
export default {
|
||||
props: ['group'],
|
||||
components: {
|
||||
questDialogDrops,
|
||||
questDialogContent,
|
||||
QuestInfo,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
selectedQuest: {},
|
||||
icons: Object.freeze({
|
||||
copy: copyIcon,
|
||||
greyBadge: greyBadgeIcon,
|
||||
qrCode: qrCodeIcon,
|
||||
facebook: facebookIcon,
|
||||
twitter: twitterIcon,
|
||||
starIcon,
|
||||
goldIcon,
|
||||
difficultyStarIcon,
|
||||
}),
|
||||
shareUserIdShown: false,
|
||||
quests,
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
const userQuests = this.user.items.quests;
|
||||
for (const key in userQuests) {
|
||||
if (userQuests[key] > 0) {
|
||||
this.selectedQuest = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.$root.$on('selectQuest', this.selectQuest);
|
||||
},
|
||||
destroyed () {
|
||||
this.$root.$off('selectQuest', this.selectQuest);
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
questData () {
|
||||
return quests.quests[this.selectedQuest];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectQuest (quest) {
|
||||
this.selectedQuest = quest.key;
|
||||
},
|
||||
async questInit () {
|
||||
this.loading = true;
|
||||
|
||||
Analytics.updateUser({
|
||||
partyID: this.group._id,
|
||||
partySize: this.group.memberCount,
|
||||
});
|
||||
|
||||
let groupId = this.group._id || this.user.party._id;
|
||||
|
||||
const key = this.selectedQuest;
|
||||
try {
|
||||
const response = await this.$store.dispatch('guilds:inviteToQuest', {groupId, key});
|
||||
const quest = response.data.data;
|
||||
|
||||
if (this.$store.state.party.data) this.$store.state.party.data.quest = quest;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
this.$root.$emit('bv::hide::modal', 'start-quest-modal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,550 @@
|
||||
<template lang="pug">
|
||||
.row
|
||||
world-boss-info-modal
|
||||
world-boss-rage-modal
|
||||
.col-12.col-sm-8.clearfix.standard-page
|
||||
.row
|
||||
.col-6.title-details
|
||||
h1(v-once) {{ $t('welcomeToTavern') }}
|
||||
|
||||
chat(
|
||||
:label="$t('tavernChat')",
|
||||
:group="group",
|
||||
:placeholder="$t('tavernCommunityGuidelinesPlaceholder')",
|
||||
@fetchRecentMessages="fetchRecentMessages()"
|
||||
)
|
||||
.col-12.col-sm-4.sidebar
|
||||
.section
|
||||
.grassy-meadow-backdrop
|
||||
.daniel_front
|
||||
|
||||
.boss-section
|
||||
.world-boss(v-if='group && group.quest && group.quest.active', :style="{background: questData.colors.dark, 'border-color': questData.colors.extralight, 'outline-color': questData.colors.light}")
|
||||
.corner-decoration(:style="{top: '-2px', right: '-2px'}")
|
||||
.corner-decoration(:style="{top: '-2px', left: '-2px'}")
|
||||
.corner-decoration(:style="{bottom: '-2px', right: '-2px'}")
|
||||
.corner-decoration(:style="{bottom: '-2px', left: '-2px'}")
|
||||
.text-center.float-bar.d-flex.align-items-center
|
||||
span.diamond
|
||||
span.strong.reduce(:style="{background: questData.colors.dark}") {{ $t('worldBossEvent') }}
|
||||
span.diamond
|
||||
.boss-gradient.pb-3.pt-3
|
||||
p.text-center.reduce(:style="{color: questData.colors.extralight}") {{ $t(`${questData.key}ArtCredit`) }}
|
||||
.quest-boss(:class="'background_' + questData.key")
|
||||
.quest-boss(:class="'quest_' + questData.key")
|
||||
.quest-boss(:class="'phobia_' + questData.key", :style="{display: 'none'}")
|
||||
.p-3
|
||||
.row.d-flex.align-items-center.mb-2
|
||||
.col-sm-6
|
||||
strong.float-left {{ questData.boss.name() }}
|
||||
.col-sm-6
|
||||
span.d-flex.float-right
|
||||
.svg-icon.boss-icon(v-html="icons.swordIcon")
|
||||
span.ml-1.reduce(:style="{color: questData.colors.extralight}") {{ $t('pendingDamage', {damage: pendingDamage()}) }}
|
||||
.grey-progress-bar.mb-1
|
||||
.boss-health-bar(:style="{width: (group.quest.progress.hp / questData.boss.hp) * 100 + '%'}")
|
||||
span.d-flex.align-items-center
|
||||
.svg-icon.boss-icon(v-html="icons.healthIcon")
|
||||
span.reduce.ml-1.pt-1 {{ $t('bossHealth', {currentHealth: bossCurrentHealth(), maxHealth: questData.boss.hp.toLocaleString()}) }}
|
||||
.mt-3.mb-2
|
||||
strong.mr-1 {{ $t('rageAttack') }}
|
||||
span {{ questData.boss.rage.title() }}
|
||||
.grey-progress-bar.mb-1
|
||||
.boss-health-bar.rage-bar(:style="{width: (group.quest.progress.rage / questData.boss.rage.value) * 100 + '%'}")
|
||||
span.d-flex.align-items-center
|
||||
.svg-icon.boss-icon(v-html="icons.rageIcon")
|
||||
span.reduce.ml-1.pt-1 {{ $t('bossRage', {currentRage: bossCurrentRage(), maxRage: questData.boss.rage.value.toLocaleString()}) }}
|
||||
.row.d-flex.align-items-center.mb-2.mt-2
|
||||
.col-sm-4.d-flex
|
||||
strong.mr-2 {{ $t('rageStrikes') }}
|
||||
.svg-icon.boss-icon.information-icon.m-auto(v-html="icons.informationIcon", v-b-tooltip.hover.top="questData.boss.rage.description()")
|
||||
.col-sm-8.d-flex.align-items-center.justify-content-center
|
||||
.m-auto(@click="showWorldBossRage('seasonalShop')")
|
||||
img.rage-strike(src="~assets/images/world-boss/rage_strike@2x.png", v-if="!group.quest.extra.worldDmg.seasonalShop")
|
||||
img.rage-strike-active(src="~assets/images/world-boss/rage_strike-seasonalShop@2x.png", v-if="group.quest.extra.worldDmg.seasonalShop")
|
||||
.m-auto(@click="showWorldBossRage('market')")
|
||||
img.rage-strike(src="~assets/images/world-boss/rage_strike@2x.png", v-if="!group.quest.extra.worldDmg.market")
|
||||
img.rage-strike-active(src="~assets/images/world-boss/rage_strike-market@2x.png", v-if="group.quest.extra.worldDmg.market")
|
||||
.m-auto(@click="showWorldBossRage('quests')")
|
||||
img.rage-strike(src="~assets/images/world-boss/rage_strike@2x.png", v-if="!group.quest.extra.worldDmg.quests")
|
||||
img.rage-strike-active(src="~assets/images/world-boss/rage_strike-quests@2x.png", v-if="group.quest.extra.worldDmg.quests")
|
||||
.boss-description.p-3(:style="{'border-color': questData.colors.extralight}", @click="sections.worldBoss = !sections.worldBoss")
|
||||
strong.float-left {{ $t('worldBossDescription') }}
|
||||
.float-right
|
||||
.toggle-down(v-if="!sections.worldBoss")
|
||||
.svg-icon.boss-icon(v-html="icons.chevronIcon")
|
||||
.toggle-up(v-if="sections.worldBoss")
|
||||
.svg-icon.boss-icon.reverse(v-html="icons.chevronIcon")
|
||||
.mt-3(v-if="sections.worldBoss", v-html="questData.notes()")
|
||||
// .text-center.mt-4
|
||||
.world-boss-info-button(@click="showWorldBossInfo()") {{$t('whatIsWorldBoss') }}
|
||||
|
||||
.sleep.below-header-sections
|
||||
strong(v-once) {{ $t('sleepDescription') }}
|
||||
ul
|
||||
li(v-once) {{ $t('sleepBullet1') }}
|
||||
li(v-once) {{ $t('sleepBullet2') }}
|
||||
li(v-once) {{ $t('sleepBullet3') }}
|
||||
li(v-once) {{ $t('sleepBullet4') }}
|
||||
button.btn.btn-secondary.pause-button(v-if='!user.preferences.sleep', @click='toggleSleep()', v-once) {{ $t('pauseDailies') }}
|
||||
button.btn.btn-secondary.pause-button(v-if='user.preferences.sleep', @click='toggleSleep()', v-once) {{ $t('unpauseDailies') }}
|
||||
.px-3
|
||||
sidebar-section(:title="$t('staffAndModerators')")
|
||||
.row
|
||||
.col-4.staff(v-for='user in staff', :class='{staff: user.type === "Staff", moderator: user.type === "Moderator", bailey: user.name === "It\'s Bailey"}')
|
||||
div
|
||||
router-link.title(:to="{'name': 'userProfile', 'params': {'userId': user.uuid}}") {{user.name}}
|
||||
.svg-icon.staff-icon(v-html="icons.tierStaff", v-if='user.type === "Staff"')
|
||||
.svg-icon.mod-icon(v-html="icons.tierMod", v-if='user.type === "Moderator" && user.name !== "It\'s Bailey"')
|
||||
.svg-icon.npc-icon(v-html="icons.tierNPC", v-if='user.name === "It\'s Bailey"')
|
||||
.type {{user.type}}
|
||||
|
||||
sidebar-section(:title="$t('helpfulLinks')")
|
||||
ul
|
||||
li
|
||||
a(href='', @click.prevent='modForm()') {{ $t('contactForm') }}
|
||||
li
|
||||
router-link(to='/static/community-guidelines', v-once) {{ $t('communityGuidelinesLink') }}
|
||||
li
|
||||
router-link(to="/groups/guild/f2db2a7f-13c5-454d-b3ee-ea1f5089e601") {{ $t('lookingForGroup') }}
|
||||
li
|
||||
router-link(to='/static/faq', v-once) {{ $t('faq') }}
|
||||
li
|
||||
a(href='', v-html="$t('glossary')")
|
||||
li
|
||||
a(href='http://habitica.fandom.com/wiki/Habitica_Wiki' target='_blank', v-once) {{ $t('wiki') }}
|
||||
li
|
||||
a(href='https://oldgods.net/habitrpg/habitrpg_user_data_display.html', target='_blank', v-once) {{ $t('dataDisplayTool') }}
|
||||
li
|
||||
router-link(to="/groups/guild/a29da26b-37de-4a71-b0c6-48e72a900dac") {{ $t('reportProblem') }}
|
||||
li
|
||||
a(href='https://trello.com/c/odmhIqyW/440-read-first-table-of-contents', target='_blank', v-once) {{ $t('requestFeature') }}
|
||||
li
|
||||
a(href='', v-html="$t('communityForum')")
|
||||
li
|
||||
router-link(to="/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a") {{ $t('askQuestionGuild') }}
|
||||
|
||||
sidebar-section(:title="$t('playerTiers')")
|
||||
.row
|
||||
.col-12
|
||||
p(v-once) {{ $t('playerTiersDesc') }}
|
||||
ul.tier-list
|
||||
li.tier1(v-once)
|
||||
| {{ $t('tier1') }}
|
||||
.svg-icon.tier1-icon(v-html="icons.tier1")
|
||||
li.tier2(v-once)
|
||||
| {{ $t('tier2') }}
|
||||
.svg-icon.tier2-icon(v-html="icons.tier2")
|
||||
li.tier3(v-once)
|
||||
| {{ $t('tier3') }}
|
||||
.svg-icon.tier3-icon(v-html="icons.tier3")
|
||||
li.tier4(v-once)
|
||||
| {{ $t('tier4') }}
|
||||
.svg-icon.tier4-icon(v-html="icons.tier4")
|
||||
li.tier5(v-once)
|
||||
| {{ $t('tier5') }}
|
||||
.svg-icon.tier5-icon(v-html="icons.tier5")
|
||||
li.tier6(v-once)
|
||||
| {{ $t('tier6') }}
|
||||
.svg-icon.tier6-icon(v-html="icons.tier6")
|
||||
li.tier7(v-once)
|
||||
| {{ $t('tier7') }}
|
||||
.svg-icon.tier7-icon(v-html="icons.tier7")
|
||||
li.moderator(v-once)
|
||||
| {{ $t('tierModerator') }}
|
||||
.svg-icon.mod-icon(v-html="icons.tierMod")
|
||||
li.staff(v-once)
|
||||
| {{ $t('tierStaff') }}
|
||||
.svg-icon.staff-icon(v-html="icons.tierStaff")
|
||||
li.npc(v-once)
|
||||
| {{ $t('tierNPC') }}
|
||||
.svg-icon.npc-icon(v-html="icons.tierNPC")
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
@import '~client/assets/scss/variables.scss';
|
||||
|
||||
h1 {
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: $gray-600;
|
||||
padding: 0em;
|
||||
|
||||
.below-header-sections {
|
||||
padding: 1em 1.75em 1em 1.75em;
|
||||
}
|
||||
}
|
||||
|
||||
.pause-button {
|
||||
background-color: #ffb445 !important;
|
||||
color: $white;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.grassy-meadow-backdrop {
|
||||
background-image: url('~assets/images/npc/#{$npc_tavern_flavor}/tavern_background.png');
|
||||
background-repeat: repeat-x;
|
||||
width: 100%;
|
||||
height: 246px;
|
||||
}
|
||||
|
||||
.daniel_front {
|
||||
background-image: url('~assets/images/npc/#{$npc_tavern_flavor}/tavern_npc.png');
|
||||
height: 246px;
|
||||
width: 471px;
|
||||
background-repeat: no-repeat;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 10px;
|
||||
display: inline-block;
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.tier1-icon, .tier2-icon {
|
||||
width: 11px;
|
||||
}
|
||||
|
||||
.tier5-icon, .tier6-icon {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.tier7-icon {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.mod-icon {
|
||||
width: 13px;
|
||||
}
|
||||
|
||||
.npc-icon {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.boss-icon {
|
||||
width: 16px;
|
||||
margin-top: .1em;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.boss-icon-large {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.staff {
|
||||
margin-bottom: 1em;
|
||||
|
||||
.staff-icon {
|
||||
width: 11px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #6133b4;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.tier-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
width: 98%;
|
||||
|
||||
li {
|
||||
border-radius: 2px;
|
||||
background-color: #edecee;
|
||||
border: solid 1px #c3c0c7;
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
margin-bottom: 1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tier1 {
|
||||
color: #c42870;
|
||||
}
|
||||
|
||||
.tier2 {
|
||||
color: #b01515;
|
||||
}
|
||||
|
||||
.tier3 {
|
||||
color: #d70e14;
|
||||
}
|
||||
|
||||
.tier4 {
|
||||
color: #c24d00;
|
||||
}
|
||||
|
||||
.tier5 {
|
||||
color: #9e650f;
|
||||
}
|
||||
|
||||
.tier6 {
|
||||
color: #2b8363;
|
||||
}
|
||||
|
||||
.tier7 {
|
||||
color: #167e87;
|
||||
}
|
||||
|
||||
.tier8, .moderator {
|
||||
color: #277eab;
|
||||
}
|
||||
|
||||
.tier9, .staff {
|
||||
color: #6133b4;
|
||||
}
|
||||
|
||||
.npc {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
.staff .title {
|
||||
color: #6133b4;
|
||||
}
|
||||
|
||||
.moderator .title {
|
||||
color: #277eab;
|
||||
}
|
||||
|
||||
.bailey .title {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.boss-section {
|
||||
padding: 1.75em;
|
||||
}
|
||||
|
||||
.world-boss {
|
||||
color: $white;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
outline-style: solid;
|
||||
outline-width: 2px;
|
||||
margin: 2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quest-boss {
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
.grey-progress-bar {
|
||||
width: 100%;
|
||||
height: 15px;
|
||||
background-color: rgba(255, 255, 255, 0.24);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.boss-health-bar {
|
||||
width: 80%;
|
||||
height: 15px;
|
||||
margin-bottom: .5em;
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
background-color: #f74e52;
|
||||
}
|
||||
|
||||
.boss-health-bar.rage-bar {
|
||||
background-color: #ff944c;
|
||||
}
|
||||
|
||||
.boss-gradient {
|
||||
background-image: linear-gradient(to bottom, #401f2a, #931f4d);
|
||||
margin-top: -1.4em;
|
||||
}
|
||||
|
||||
.boss-description {
|
||||
border-top: 1px solid;
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
padding: .25em 0 0 .25em;
|
||||
}
|
||||
|
||||
.float-bar {
|
||||
position: relative;
|
||||
top: -16px;
|
||||
width: 162px;
|
||||
height: 28px;
|
||||
border-radius: 2px;
|
||||
background-color: inherit;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.corner-decoration {
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: inherit;
|
||||
border: inherit;
|
||||
outline: inherit;
|
||||
}
|
||||
|
||||
.reverse {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.diamond {
|
||||
margin: auto;
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
background-color: #dc4069;
|
||||
border: solid 2px #931f4d;
|
||||
}
|
||||
|
||||
.reduce {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rage-strike {
|
||||
max-width: 50px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.rage-strike-active {
|
||||
max-width: 75px;
|
||||
height: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.world-boss-info-button {
|
||||
width: 100%;
|
||||
background-color: $gray-500;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
color: $blue-10;
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import { goToModForm } from 'client/libs/modform';
|
||||
|
||||
import { TAVERN_ID } from '../../../common/script/constants';
|
||||
import worldBossInfoModal from '../world-boss/worldBossInfoModal';
|
||||
import worldBossRageModal from '../world-boss/worldBossRageModal';
|
||||
import sidebarSection from '../sidebarSection';
|
||||
import chat from './chat';
|
||||
|
||||
|
||||
import challengeIcon from 'assets/svg/challenge.svg';
|
||||
import chevronIcon from 'assets/svg/chevron-red.svg';
|
||||
import gemIcon from 'assets/svg/gem.svg';
|
||||
import healthIcon from 'assets/svg/health.svg';
|
||||
import informationIconRed from 'assets/svg/information-red.svg';
|
||||
import questBackground from 'assets/svg/quest-background-border.svg';
|
||||
import rageIcon from 'assets/svg/rage.svg';
|
||||
import swordIcon from 'assets/svg/sword.svg';
|
||||
|
||||
import tier1 from 'assets/svg/tier-1.svg';
|
||||
import tier2 from 'assets/svg/tier-2.svg';
|
||||
import tier3 from 'assets/svg/tier-3.svg';
|
||||
import tier4 from 'assets/svg/tier-4.svg';
|
||||
import tier5 from 'assets/svg/tier-5.svg';
|
||||
import tier6 from 'assets/svg/tier-6.svg';
|
||||
import tier7 from 'assets/svg/tier-7.svg';
|
||||
import tierMod from 'assets/svg/tier-mod.svg';
|
||||
import tierNPC from 'assets/svg/tier-npc.svg';
|
||||
import tierStaff from 'assets/svg/tier-staff.svg';
|
||||
|
||||
import quests from 'common/script/content/quests';
|
||||
import staffList from '../../libs/staffList';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
worldBossInfoModal,
|
||||
worldBossRageModal,
|
||||
sidebarSection,
|
||||
chat,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
groupId: TAVERN_ID,
|
||||
icons: Object.freeze({
|
||||
challengeIcon,
|
||||
chevronIcon,
|
||||
gem: gemIcon,
|
||||
healthIcon,
|
||||
informationIcon: informationIconRed,
|
||||
questBackground,
|
||||
rageIcon,
|
||||
swordIcon,
|
||||
tier1,
|
||||
tier2,
|
||||
tier3,
|
||||
tier4,
|
||||
tier5,
|
||||
tier6,
|
||||
tier7,
|
||||
tierMod,
|
||||
tierNPC,
|
||||
tierStaff,
|
||||
}),
|
||||
group: {
|
||||
chat: [],
|
||||
},
|
||||
sections: {
|
||||
worldBoss: true,
|
||||
},
|
||||
staff: staffList,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
questData () {
|
||||
if (!this.group.quest) return {};
|
||||
return quests.quests[this.group.quest.key];
|
||||
},
|
||||
},
|
||||
async mounted () {
|
||||
this.group = await this.$store.dispatch('guilds:getGroup', {groupId: TAVERN_ID});
|
||||
},
|
||||
methods: {
|
||||
modForm () {
|
||||
goToModForm(this.user);
|
||||
},
|
||||
toggleSleep () {
|
||||
this.$store.dispatch('user:sleep');
|
||||
},
|
||||
|
||||
pendingDamage () {
|
||||
if (!this.user.party.quest.progress.up) return 0;
|
||||
return this.$options.filters.floor(this.user.party.quest.progress.up, 10);
|
||||
// keep user's pending damage consistent with how it's displayed on the party page
|
||||
},
|
||||
bossCurrentHealth () {
|
||||
if (!this.group.quest.progress.hp) return 0;
|
||||
|
||||
return Math.ceil(parseFloat(this.group.quest.progress.hp)).toLocaleString();
|
||||
},
|
||||
bossCurrentRage () {
|
||||
if (!this.group.quest.progress.hp) return 0;
|
||||
|
||||
return Math.floor(parseFloat(this.group.quest.progress.rage)).toLocaleString();
|
||||
},
|
||||
showWorldBossInfo () {
|
||||
this.$root.$emit('bv::show::modal', 'world-boss-info');
|
||||
},
|
||||
showWorldBossRage (npc) {
|
||||
if (this.group.quest.extra.worldDmg[npc]) {
|
||||
this.$store.state.rageModalOptions.npc = npc;
|
||||
this.$root.$emit('bv::show::modal', 'world-boss-rage');
|
||||
}
|
||||
},
|
||||
async fetchRecentMessages () {
|
||||
this.group = await this.$store.dispatch('guilds:getGroup', {groupId: TAVERN_ID});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,210 @@
|
||||
<template lang="pug">
|
||||
.row.standard-page
|
||||
small.muted(v-html="$t('blurbHallContributors')")
|
||||
.well
|
||||
div(v-if='user.contributor.admin')
|
||||
h2 Reward User
|
||||
|
||||
.row
|
||||
.form.col-6(v-if='!hero.profile')
|
||||
.form-group
|
||||
input.form-control(type='text', v-model='heroID', :placeholder="'User ID or Username'")
|
||||
.form-group
|
||||
button.btn.btn-secondary(@click='loadHero(heroID)')
|
||||
| Load User
|
||||
|
||||
.row
|
||||
.form.col-6(v-if='hero && hero.profile', submit='saveHero(hero)')
|
||||
router-link(:to="{'name': 'userProfile', 'params': {'userId': hero._id}}")
|
||||
h3 @{{hero.auth.local.username}} / {{hero.profile.name}}
|
||||
.form-group
|
||||
label Contributor Title
|
||||
input.form-control(type='text', v-model='hero.contributor.text')
|
||||
small Common titles: <strong>Ambassador, Artisan, Bard, Blacksmith, Challenger, Comrade, Fletcher, Linguist, Linguistic Scribe, Scribe, Socialite, Storyteller</strong>. Rare titles: Advisor, Chamberlain, Designer, Mathematician, Shirtster, Spokesperson, Statistician, Tinker, Transcriber, Troubadour.
|
||||
.form-group
|
||||
label Contributor Tier
|
||||
input.form-control(type='number', v-model='hero.contributor.level')
|
||||
small 1-7 for normal contributors, 8 for moderators, 9 for staff. This determines which items, pets, and mounts are available, and name-tag coloring. Tiers 8 and 9 are automatically given admin status.
|
||||
|
|
||||
a(target='_blank', href='https://trello.com/c/wkFzONhE/277-contributor-gear') More details (1-7)
|
||||
|,
|
||||
a(target='_blank', href='https://github.com/HabitRPG/habitica/issues/3801') more details (8-9)
|
||||
.form-group
|
||||
label Contributions
|
||||
textarea.form-control(cols=5, v-model='hero.contributor.contributions')
|
||||
|
||||
.form-group
|
||||
label Balance
|
||||
input.form-control(type='number', step="any", v-model='hero.balance')
|
||||
small
|
||||
span '{{ hero.balance }}' is in USD, <em>not</em> in Gems. E.g., if this number is 1, it means 4 Gems. Only use this option when manually granting Gems to players, don't use it when granting contributor tiers. Contrib tiers will automatically add Gems.
|
||||
.accordion
|
||||
.accordion-group(heading='Items')
|
||||
h4.expand-toggle(:class="{'open': expandItems}", @click="expandItems = !expandItems") Update Item
|
||||
.form-group.well(v-if="expandItems")
|
||||
input.form-control(type='text',placeholder='Path (eg, items.pets.BearCub-Base)',v-model='hero.itemPath')
|
||||
small.muted Enter the <strong>item path</strong>. E.g., <code>items.pets.BearCub-Zombie</code> or <code>items.gear.owned.head_special_0</code> or <code>items.gear.equipped.head</code>. You can find all the item paths below.
|
||||
br
|
||||
input.form-control(type='text',placeholder='Value (eg, 5)',v-model='hero.itemVal')
|
||||
small.muted Enter the <strong>item value</strong>. E.g., <code>5</code> or <code>false</code> or <code>head_warrior_3</code>. All values are listed in the All Item Paths section below.
|
||||
.accordion
|
||||
.accordion-group(heading='All Item Paths')
|
||||
pre {{allItemPaths}}
|
||||
.accordion-group(heading='Current Items')
|
||||
pre {{hero.items}}
|
||||
.accordion-group(heading='Auth')
|
||||
h4.expand-toggle(:class="{'open': expandAuth}", @click="expandAuth = !expandAuth") Auth
|
||||
div(v-if="expandAuth")
|
||||
pre {{hero.auth}}
|
||||
.form-group
|
||||
.checkbox
|
||||
label
|
||||
input(type='checkbox', v-if='hero.flags', v-model='hero.flags.chatShadowMuted')
|
||||
strong Chat Shadow Muting On
|
||||
.form-group
|
||||
.checkbox
|
||||
label
|
||||
input(type='checkbox', v-if='hero.flags', v-model='hero.flags.chatRevoked')
|
||||
strong Chat Privileges Revoked
|
||||
.form-group
|
||||
.checkbox
|
||||
label
|
||||
input(type='checkbox', v-model='hero.auth.blocked')
|
||||
| Blocked
|
||||
|
||||
// h4 Backer Status
|
||||
// Add backer stuff like tier, disable adds, etcs
|
||||
.form-group
|
||||
button.form-control.btn.btn-primary(@click='saveHero()')
|
||||
| Save
|
||||
|
||||
.table-responsive
|
||||
table.table.table-striped
|
||||
thead
|
||||
tr
|
||||
th {{ $t('name') }}
|
||||
th(v-if='user.contributor && user.contributor.admin') {{ $t('UUID') }}
|
||||
th {{ $t('contribLevel') }}
|
||||
th {{ $t('title') }}
|
||||
th {{ $t('contributions') }}
|
||||
tbody
|
||||
tr(v-for='(hero, index) in heroes')
|
||||
td
|
||||
user-link(v-if='hero.contributor && hero.contributor.admin', :user='hero', :popover="$t('gamemaster')", popover-trigger='mouseenter', popover-placement='right')
|
||||
user-link(v-if='!hero.contributor || !hero.contributor.admin', :user='hero')
|
||||
td(v-if='user.contributor.admin', @click='populateContributorInput(hero._id, index)').btn-link {{hero._id}}
|
||||
td {{hero.contributor.level}}
|
||||
td {{hero.contributor.text}}
|
||||
td
|
||||
div(v-markdown='hero.contributor.contributions', target='_blank')
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h4.expand-toggle::after {
|
||||
margin-left: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import each from 'lodash/each';
|
||||
|
||||
import markdownDirective from 'client/directives/markdown';
|
||||
import styleHelper from 'client/mixins/styleHelper';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import quests from 'common/script/content/quests';
|
||||
import { mountInfo, petInfo } from 'common/script/content/stable';
|
||||
import { food, hatchingPotions, special } from 'common/script/content';
|
||||
import gear from 'common/script/content/gear';
|
||||
import notifications from 'client/mixins/notifications';
|
||||
import userLink from '../userLink';
|
||||
|
||||
export default {
|
||||
mixins: [notifications, styleHelper],
|
||||
components: {
|
||||
userLink,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
heroes: [],
|
||||
hero: {},
|
||||
heroID: '',
|
||||
currentHeroIndex: -1,
|
||||
allItemPaths: this.getAllItemPaths(),
|
||||
quests,
|
||||
mountInfo,
|
||||
petInfo,
|
||||
food,
|
||||
hatchingPotions,
|
||||
special,
|
||||
gear,
|
||||
expandItems: false,
|
||||
expandAuth: false,
|
||||
};
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
async mounted () {
|
||||
this.heroes = await this.$store.dispatch('hall:getHeroes');
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
getAllItemPaths () {
|
||||
// let questsFormat = this.getFormattedItemReference('items.quests', keys(this.quests), 'Numeric Quantity');
|
||||
// let mountsFormat = this.getFormattedItemReference('items.mounts', keys(this.mountInfo), 'Boolean');
|
||||
// let foodFormat = this.getFormattedItemReference('items.food', keys(this.food), 'Numeric Quantity');
|
||||
// let eggsFormat = this.getFormattedItemReference('items.eggs', keys(this.eggs), 'Numeric Quantity');
|
||||
// let hatchingPotionsFormat = this.getFormattedItemReference('items.hatchingPotions', keys(this.hatchingPotions), 'Numeric Quantity');
|
||||
// let petsFormat = this.getFormattedItemReference('items.pets', keys(this.petInfo), '-1: Owns Mount, 0: Not Owned, 1-49: Progress to mount');
|
||||
// let specialFormat = this.getFormattedItemReference('items.special', keys(this.special), 'Numeric Quantity');
|
||||
// let gearFormat = this.getFormattedItemReference('items.gear.owned', keys(this.gear.flat), 'Boolean');
|
||||
//
|
||||
// let equippedGearFormat = ''; // @TODO: '\nEquipped Gear:\n\titems.gear.{equipped/costume}.{head/headAccessory/eyewear/armor/body/back/shield/weapon}.{gearKey}\n';
|
||||
// let equippedPetFormat = ''; // @TODO: '\nEquipped Pet:\n\titems.currentPet.{petKey}\n';
|
||||
// let equippedMountFormat = ''; // @TODO: '\nEquipped Mount:\n\titems.currentMount.{mountKey}\n';
|
||||
//
|
||||
// let data = questsFormat.concat(mountsFormat, foodFormat, eggsFormat, hatchingPotionsFormat, petsFormat, specialFormat, gearFormat, equippedGearFormat, equippedPetFormat, equippedMountFormat);
|
||||
//
|
||||
// return data;
|
||||
},
|
||||
getFormattedItemReference (pathPrefix, itemKeys, values) {
|
||||
let finishedString = '\n'.concat('path: ', pathPrefix, ', ', 'value: {', values, '}\n');
|
||||
|
||||
each(itemKeys, (key) => {
|
||||
finishedString = finishedString.concat('\t', pathPrefix, '.', key, '\n');
|
||||
});
|
||||
|
||||
return finishedString;
|
||||
},
|
||||
async loadHero (uuid, heroIndex) {
|
||||
this.currentHeroIndex = heroIndex;
|
||||
let hero = await this.$store.dispatch('hall:getHero', { uuid });
|
||||
this.hero = Object.assign({}, hero);
|
||||
if (!this.hero.flags) {
|
||||
this.hero.flags = {
|
||||
chatRevoked: false,
|
||||
chatShadowMuted: false,
|
||||
};
|
||||
}
|
||||
this.expandItems = false;
|
||||
this.expandAuth = false;
|
||||
},
|
||||
async saveHero () {
|
||||
this.hero.contributor.admin = this.hero.contributor.level > 7 ? true : false;
|
||||
let heroUpdated = await this.$store.dispatch('hall:updateHero', { heroDetails: this.hero });
|
||||
this.text('User updated');
|
||||
this.hero = {};
|
||||
this.heroID = -1;
|
||||
this.heroes[this.currentHeroIndex] = heroUpdated;
|
||||
this.currentHeroIndex = -1;
|
||||
},
|
||||
populateContributorInput (id, index) {
|
||||
this.heroID = id;
|
||||
window.scrollTo(0, 200);
|
||||
this.loadHero(id, index);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,19 @@
|
||||
<template lang="pug">
|
||||
.row
|
||||
secondary-menu.col-12
|
||||
router-link.nav-link(:to="{name: 'contributors'}", exact, :class="{'active': $route.name === 'contributors'}") {{ $t('hallContributors') }}
|
||||
router-link.nav-link(:to="{name: 'patrons'}", :class="{'active': $route.name === 'patrons'}") {{ $t('hallPatrons') }}
|
||||
|
||||
.col-12
|
||||
router-view
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SecondaryMenu from 'client/components/secondaryMenu';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SecondaryMenu,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,41 @@
|
||||
<template lang="pug">
|
||||
.row
|
||||
small.muted {{ $t('blurbHallPatrons') }}
|
||||
.table-responsive
|
||||
table.table.table-striped(infinite-scroll="loadMore()")
|
||||
thead
|
||||
tr
|
||||
th {{ $t('name') }}
|
||||
th(v-if='user.contributor.admin') {{ $t('UUID') }}
|
||||
th {{ $t('backerTier') }}
|
||||
tbody
|
||||
tr(v-for='patron in patrons')
|
||||
td
|
||||
a.label.label-default(v-class='userLevelStyle(patron)', @click='clickMember(patron._id, true)')
|
||||
| {{patron.profile.name}}
|
||||
td(v-if='user.contributor.admin') {{patron._id}}
|
||||
td {{patron.backer.tier}}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import styleHelper from 'client/mixins/styleHelper';
|
||||
|
||||
export default {
|
||||
mixins: [styleHelper],
|
||||
data () {
|
||||
return {
|
||||
patrons: [],
|
||||
};
|
||||
},
|
||||
async mounted () {
|
||||
this.patrons = await this.$store.dispatch('hall:getPatrons', { page: 0 });
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
// @TODO: Import member modal - clickMember()
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,191 @@
|
||||
<template lang="pug">
|
||||
div
|
||||
invite-modal(:group='inviteModalGroup', :groupType='inviteModalGroupType')
|
||||
create-party-modal
|
||||
#app-header.row(:class="{'hide-header': $route.name === 'groupPlan'}")
|
||||
members-modal(:hide-badge="true")
|
||||
member-details(
|
||||
:member="user",
|
||||
:class-badge-position="'next-to-name'",
|
||||
:is-header="true",
|
||||
)
|
||||
.view-party.d-flex.align-items-center(
|
||||
v-if="hasParty",
|
||||
)
|
||||
button.btn.btn-primary.view-party-button(@click='showPartyMembers()') {{ $t('viewParty') }}
|
||||
.party-members.d-flex(
|
||||
v-if="hasParty",
|
||||
v-resize="1500",
|
||||
@resized="setPartyMembersWidth($event)"
|
||||
)
|
||||
member-details(
|
||||
v-for="(member, $index) in sortedPartyMembers",
|
||||
:key="member._id",
|
||||
v-if="member._id !== user._id && $index < membersToShow",
|
||||
:member="member",
|
||||
condensed=true,
|
||||
@onHover="expandMember(member._id)",
|
||||
:expanded="member._id === expandedMember",
|
||||
:is-header="true",
|
||||
:class-badge-position="'hidden'",
|
||||
)
|
||||
.no-party.d-flex.justify-content-center.text-center(v-else)
|
||||
.align-self-center
|
||||
h3 {{ $t('battleWithFriends') }}
|
||||
span.small-text(v-html="$t('inviteFriendsParty')")
|
||||
br
|
||||
button.btn.btn-primary(@click='createOrInviteParty()') {{ user.party._id ? $t('inviteFriends') : $t('startAParty') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
#app-header {
|
||||
padding-left: 24px;
|
||||
padding-top: 9px;
|
||||
padding-bottom: 8px;
|
||||
background: $purple-50;
|
||||
color: $header-color;
|
||||
flex-wrap: nowrap;
|
||||
position: relative;
|
||||
max-height: 10.25rem;
|
||||
}
|
||||
|
||||
.hide-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.no-party, .party-members {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.view-party {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
right: 0;
|
||||
padding-right: 40px;
|
||||
padding-left: 10px;
|
||||
height: calc(100% - 9px);
|
||||
background-image: linear-gradient(to right, rgba($purple-50, 0), $purple-50);
|
||||
}
|
||||
|
||||
.no-party {
|
||||
.small-text {
|
||||
color: $header-color;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: $white;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.view-party-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { mapGetters, mapActions } from 'client/libs/store';
|
||||
import MemberDetails from '../memberDetails';
|
||||
import createPartyModal from '../groups/createPartyModal';
|
||||
import inviteModal from '../groups/inviteModal';
|
||||
import membersModal from '../groups/membersModal';
|
||||
import ResizeDirective from 'client/directives/resize.directive';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
resize: ResizeDirective,
|
||||
},
|
||||
components: {
|
||||
MemberDetails,
|
||||
createPartyModal,
|
||||
inviteModal,
|
||||
membersModal,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
expandedMember: null,
|
||||
currentWidth: 0,
|
||||
inviteModalGroup: undefined,
|
||||
inviteModalGroupType: undefined,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
user: 'user:data',
|
||||
partyMembers: 'party:members',
|
||||
}),
|
||||
showHeader () {
|
||||
if (this.$store.state.hideHeader) return false;
|
||||
return true;
|
||||
},
|
||||
hasParty () {
|
||||
return this.user.party && this.user.party._id && this.partyMembers && this.partyMembers.length > 1;
|
||||
},
|
||||
membersToShow () {
|
||||
return Math.floor(this.currentWidth / 140) + 1;
|
||||
},
|
||||
sortedPartyMembers () {
|
||||
return orderBy(this.partyMembers, [this.user.party.order], [this.user.party.orderAscending]);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
getPartyMembers: 'party:getMembers',
|
||||
}),
|
||||
expandMember (memberId) {
|
||||
if (this.expandedMember === memberId) {
|
||||
this.expandedMember = null;
|
||||
} else {
|
||||
this.expandedMember = memberId;
|
||||
}
|
||||
},
|
||||
createOrInviteParty () {
|
||||
if (this.user.party._id) {
|
||||
this.$root.$emit('inviteModal::inviteToGroup', this.user.party);
|
||||
} else {
|
||||
this.$root.$emit('bv::show::modal', 'create-party-modal');
|
||||
}
|
||||
},
|
||||
async showPartyMembers () {
|
||||
const party = await this.$store.dispatch('party:getParty');
|
||||
|
||||
this.$root.$emit('habitica:show-member-modal', {
|
||||
groupId: party.data._id,
|
||||
viewingMembers: this.partyMembers,
|
||||
group: party.data,
|
||||
});
|
||||
},
|
||||
setPartyMembersWidth ($event) {
|
||||
if (this.currentWidth !== $event.width) {
|
||||
this.currentWidth = $event.width;
|
||||
}
|
||||
},
|
||||
},
|
||||
created () {
|
||||
if (this.user.party && this.user.party._id) {
|
||||
this.$store.state.memberModalOptions.groupId = this.user.party._id;
|
||||
this.getPartyMembers();
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('inviteModal::inviteToGroup', (group) => {
|
||||
this.inviteModalGroup = group;
|
||||
this.inviteModalGroupType = group.type === 'guild' ? 'Guild' : 'Party';
|
||||
this.$root.$emit('bv::show::modal', 'invite-modal');
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
this.$root.off('inviteModal::inviteToGroup');
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,531 @@
|
||||
<template lang="pug">
|
||||
div
|
||||
inbox-modal
|
||||
creator-intro
|
||||
profileModal
|
||||
report-flag-modal
|
||||
send-gems-modal
|
||||
b-navbar.topbar.navbar-inverse.static-top(toggleable="lg", type="dark", :class="navbarZIndexClass")
|
||||
b-navbar-brand.brand(aria-label="Habitica")
|
||||
.logo.svg-icon.d-none.d-xl-block(v-html="icons.logo")
|
||||
.svg-icon.gryphon.d-xs-block.d-xl-none
|
||||
b-navbar-toggle(target='menu_collapse').menu-toggle
|
||||
.quick-menu.mobile-only.form-inline
|
||||
a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')", :aria-label="$t('sync')")
|
||||
.top-menu-icon.svg-icon(v-html="icons.sync")
|
||||
notification-menu.item-with-icon
|
||||
user-dropdown.item-with-icon
|
||||
b-collapse#menu_collapse(v-model="menuIsOpen").collapse.navbar-collapse
|
||||
b-navbar-nav.menu-list
|
||||
b-nav-item.topbar-item(:class="{'active': $route.path === '/'}" tag="li", :to="{name: 'tasks'}", exact) {{ $t('tasks') }}
|
||||
li.topbar-item(:class="{'active': $route.path.startsWith('/inventory'), 'down': $route.path.startsWith('/inventory') && this.isDesktop()}").droppable
|
||||
.chevron.rotate(@click='dropdownMobile($event)')
|
||||
.chevron-icon-down(v-html="icons.chevronDown", v-once)
|
||||
router-link.nav-link(:to="{name: 'items'}") {{ $t('inventory') }}
|
||||
.topbar-dropdown
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'items'}", exact) {{ $t('items') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'equipment'}") {{ $t('equipment') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'stable'}") {{ $t('stable') }}
|
||||
li.topbar-item(:class="{'active': $route.path.startsWith('/shop'), 'down': $route.path.startsWith('/shop') && this.isDesktop()}").droppable
|
||||
.chevron.rotate(@click='dropdownMobile($event)')
|
||||
.chevron-icon-down(v-html="icons.chevronDown", v-once)
|
||||
router-link.nav-link(:to="{name: 'market'}") {{ $t('shops') }}
|
||||
.topbar-dropdown
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'market'}", exact) {{ $t('market') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'quests'}") {{ $t('quests') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'seasonal'}") {{ $t('titleSeasonalShop') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'time'}") {{ $t('titleTimeTravelers') }}
|
||||
b-nav-item.topbar-item(:class="{'active': $route.path.startsWith('/party')}" tag="li", :to="{name: 'party'}", v-if='this.user.party._id') {{ $t('party') }}
|
||||
b-nav-item.topbar-item(:class="{'active': $route.path.startsWith('/party')}" @click='openPartyModal()', v-if='!this.user.party._id') {{ $t('party') }}
|
||||
li.topbar-item(:class="{'active': $route.path.startsWith('/groups'), 'down': $route.path.startsWith('/groups') && this.isDesktop()}").droppable
|
||||
.chevron.rotate(@click='dropdownMobile($event)')
|
||||
.chevron-icon-down(v-html="icons.chevronDown", v-once)
|
||||
router-link.nav-link(:to="{name: 'tavern'}") {{ $t('guilds') }}
|
||||
.topbar-dropdown
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'tavern'}") {{ $t('tavern') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'myGuilds'}") {{ $t('myGuilds') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'guildsDiscovery'}") {{ $t('guildsDiscovery') }}
|
||||
li.topbar-item(:class="{'active': $route.path.startsWith('/group-plans'), 'down': $route.path.startsWith('/group-plans') && this.isDesktop()}").droppable
|
||||
.chevron.rotate(v-if="groupPlans.length > 0", @click='dropdownMobile($event)')
|
||||
.chevron-icon-down(v-html="icons.chevronDown", v-once)
|
||||
router-link.nav-link(:to="{name: 'groupPlan'}") {{ $t('group') }}
|
||||
.topbar-dropdown
|
||||
router-link.topbar-dropdown-item.dropdown-item(v-for='group in groupPlans', :key='group._id', :to="{name: 'groupPlanDetailTaskInformation', params: {groupId: group._id}}") {{ group.name }}
|
||||
li.topbar-item(:class="{'active': $route.path.startsWith('/challenges'), 'down': $route.path.startsWith('/challenges') && this.isDesktop()}").droppable
|
||||
.chevron.rotate(@click='dropdownMobile($event)')
|
||||
.chevron-icon-down(v-html="icons.chevronDown", v-once)
|
||||
router-link.nav-link(:to="{name: 'myChallenges'}") {{ $t('challenges') }}
|
||||
.topbar-dropdown
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'myChallenges'}") {{ $t('myChallenges') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'findChallenges'}") {{ $t('findChallenges') }}
|
||||
li.topbar-item(:class="{'active': $route.path.startsWith('/help'), 'down': $route.path.startsWith('/help') && this.isDesktop()}").droppable
|
||||
.chevron.rotate(@click='dropdownMobile($event)')
|
||||
.chevron-icon-down(v-html="icons.chevronDown", v-once)
|
||||
router-link.nav-link(:to="{name: 'faq'}") {{ $t('help') }}
|
||||
.topbar-dropdown
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'faq'}") {{ $t('faq') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'overview'}") {{ $t('overview') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(to="/groups/guild/a29da26b-37de-4a71-b0c6-48e72a900dac") {{ $t('reportBug') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(to="/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a") {{ $t('askAQuestion') }}
|
||||
a.topbar-dropdown-item.dropdown-item(href="https://trello.com/c/odmhIqyW/440-read-first-table-of-contents", target='_blank') {{ $t('requestAF') }}
|
||||
a.topbar-dropdown-item.dropdown-item(href="http://habitica.fandom.com/wiki/Contributing_to_Habitica", target='_blank') {{ $t('contributing') }}
|
||||
a.topbar-dropdown-item.dropdown-item(href="http://habitica.fandom.com/wiki/Habitica_Wiki", target='_blank') {{ $t('wiki') }}
|
||||
a.topbar-dropdown-item.dropdown-item(@click='modForm()') {{ $t('contactForm') }}
|
||||
.currency-tray.form-inline
|
||||
.item-with-icon(v-if="userHourglasses > 0")
|
||||
.top-menu-icon.svg-icon(v-html="icons.hourglasses", v-b-tooltip.hover.bottom="$t('mysticHourglassesTooltip')")
|
||||
span {{ userHourglasses }}
|
||||
.item-with-icon
|
||||
a.top-menu-icon.svg-icon.gem(:aria-label="$t('gems')", href="#buy-gems" v-html="icons.gem", @click.prevent='showBuyGemsModal("gems")', v-b-tooltip.hover.bottom="$t('gems')")
|
||||
span {{userGems}}
|
||||
.item-with-icon.gold
|
||||
.top-menu-icon.svg-icon(:aria-label="$t('gold')", v-html="icons.gold", v-b-tooltip.hover.bottom="$t('gold')")
|
||||
span {{Math.floor(user.stats.gp * 100) / 100}}
|
||||
.form-inline.desktop-only
|
||||
a.item-with-icon(@click="sync", @keyup.enter="sync", role="link", :aria-label="$t('sync')", tabindex="0", v-b-tooltip.hover.bottom="$t('sync')")
|
||||
.top-menu-icon.svg-icon(v-html="icons.sync")
|
||||
notification-menu.item-with-icon
|
||||
user-dropdown.item-with-icon
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
@import '~client/assets/scss/utils.scss';
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.chevron {
|
||||
display: none
|
||||
}
|
||||
|
||||
.gryphon {
|
||||
background-image: url('~assets/images/melior@3x.png');
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-size: cover;
|
||||
color: $white;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.topbar-item {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 992px) {
|
||||
.chevron {
|
||||
display: none
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
max-height: 56px;
|
||||
|
||||
.currency-tray {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.topbar-item {
|
||||
padding-top: 5px;
|
||||
height: 56px;
|
||||
|
||||
&:hover {
|
||||
background: $purple-200;
|
||||
}
|
||||
|
||||
&.active:not(:hover) {
|
||||
box-shadow: 0px -4px 0px $purple-300 inset;
|
||||
}
|
||||
}
|
||||
|
||||
.topbar-dropdown {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.brand {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gryphon {
|
||||
position: absolute;
|
||||
left: calc(50% - 30px);
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
#menu_collapse {
|
||||
margin: 0.6em -16px -8px;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
background-color: $purple-100;
|
||||
|
||||
.menu-list {
|
||||
width: 100%;
|
||||
order: 1;
|
||||
text-align: center;
|
||||
|
||||
.topbar-dropdown {
|
||||
transition: max-height 0.25s ease;
|
||||
}
|
||||
|
||||
.topbar-dropdown-item {
|
||||
background: #432874;
|
||||
border-bottom: #6133b4 solid 1px;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
width: 20%;
|
||||
height: 42px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.chevron-icon-down {
|
||||
width: 14px;
|
||||
top: 11px;
|
||||
right: 12px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.down .rotate .chevron-icon-down {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
.topbar-item {
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
background: #6133b4;
|
||||
}
|
||||
|
||||
background: #4f2a93;
|
||||
border-bottom: #6133b4 solid 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.currency-tray {
|
||||
justify-content: center;
|
||||
min-height: 40px;
|
||||
background: #271b3d;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.desktop-only {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
border: none;
|
||||
}
|
||||
|
||||
#menu_collapse {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
background: $purple-100 url(~assets/svg/for-css/bits.svg) right top no-repeat;
|
||||
min-height: 56px;
|
||||
box-shadow: 0 1px 2px 0 rgba($black, 0.24);
|
||||
|
||||
a {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-z-index {
|
||||
&-normal {
|
||||
z-index: 1080;
|
||||
}
|
||||
|
||||
&-modal {
|
||||
z-index: 1035;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
padding-left: 10px;
|
||||
width: 128px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.quick-menu {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.currency-tray {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.topbar-item {
|
||||
font-size: 16px;
|
||||
color: $white !important;
|
||||
font-weight: bold;
|
||||
transition: none;
|
||||
|
||||
.topbar-dropdown {
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
|
||||
.topbar-dropdown-item {
|
||||
line-height: 1.5;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
>a {
|
||||
padding: .8em 1em !important;
|
||||
}
|
||||
|
||||
&.down {
|
||||
color: $white !important;
|
||||
background: $purple-200;
|
||||
|
||||
.topbar-dropdown {
|
||||
margin-top: 0; // Remove gap between navbar and drop-down.
|
||||
background: $purple-200;
|
||||
border-radius: 0px;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 0px;
|
||||
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
|
||||
.topbar-dropdown-item {
|
||||
font-size: 16px;
|
||||
box-shadow: none;
|
||||
color: $white;
|
||||
border: none;
|
||||
line-height: 1.5;
|
||||
display: list-item;
|
||||
|
||||
&.active {
|
||||
background: $purple-300;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $purple-300;
|
||||
|
||||
&:last-child {
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown + .dropdown {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.item-with-icon {
|
||||
color: $white;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.gold {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
&:focus /deep/ .top-menu-icon.svg-icon,
|
||||
&:hover /deep/ .top-menu-icon.svg-icon {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
& /deep/ .top-menu-icon.svg-icon {
|
||||
color: $header-color;
|
||||
vertical-align: bottom;
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 12px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.gem:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message-count {
|
||||
background-color: $blue-50;
|
||||
border-radius: 50%;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
float: right;
|
||||
color: $white;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.message-count.top-count {
|
||||
background-color: $red-50;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -0.5em;
|
||||
padding: .2em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'client/libs/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import { goToModForm } from 'client/libs/modform';
|
||||
|
||||
import gemIcon from 'assets/svg/gem.svg';
|
||||
import goldIcon from 'assets/svg/gold.svg';
|
||||
import syncIcon from 'assets/svg/sync.svg';
|
||||
import svgHourglasses from 'assets/svg/hourglass.svg';
|
||||
import chevronDownIcon from 'assets/svg/chevron-down.svg';
|
||||
import logo from 'assets/svg/logo.svg';
|
||||
|
||||
import creatorIntro from '../creatorIntro';
|
||||
import InboxModal from '../userMenu/inbox.vue';
|
||||
import notificationMenu from './notificationsDropdown';
|
||||
import profileModal from '../userMenu/profileModal';
|
||||
import reportFlagModal from '../chat/reportFlagModal';
|
||||
import sendGemsModal from 'client/components/payments/sendGemsModal';
|
||||
import sync from 'client/mixins/sync';
|
||||
import userDropdown from './userDropdown';
|
||||
|
||||
|
||||
export default {
|
||||
components: {
|
||||
creatorIntro,
|
||||
InboxModal,
|
||||
notificationMenu,
|
||||
profileModal,
|
||||
reportFlagModal,
|
||||
sendGemsModal,
|
||||
userDropdown,
|
||||
},
|
||||
mixins: [sync],
|
||||
data () {
|
||||
return {
|
||||
isUserDropdownOpen: false,
|
||||
menuIsOpen: false,
|
||||
icons: Object.freeze({
|
||||
gem: gemIcon,
|
||||
gold: goldIcon,
|
||||
hourglasses: svgHourglasses,
|
||||
sync: syncIcon,
|
||||
logo,
|
||||
chevronDown: chevronDownIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
userGems: 'user:gems',
|
||||
}),
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
userHourglasses: 'user.data.purchased.plan.consecutive.trinkets',
|
||||
groupPlans: 'groupPlans',
|
||||
modalStack: 'modalStack',
|
||||
}),
|
||||
navbarZIndexClass () {
|
||||
if (this.modalStack.length > 0) {
|
||||
return 'navbar-z-index-modal';
|
||||
}
|
||||
return 'navbar-z-index-normal';
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.getUserGroupPlans();
|
||||
Array.from(document.getElementById('menu_collapse').getElementsByTagName('a')).forEach(link => {
|
||||
link.addEventListener('click', this.closeMenu);
|
||||
});
|
||||
Array.from(document.getElementsByClassName('topbar-item')).forEach(link => {
|
||||
link.addEventListener('mouseenter', this.dropdownDesktop);
|
||||
link.addEventListener('mouseleave', this.dropdownDesktop);
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
modForm () {
|
||||
goToModForm(this.user);
|
||||
},
|
||||
toggleUserDropdown () {
|
||||
this.isUserDropdownOpen = !this.isUserDropdownOpen;
|
||||
},
|
||||
async getUserGroupPlans () {
|
||||
this.$store.state.groupPlans = await this.$store.dispatch('guilds:getGroupPlans');
|
||||
},
|
||||
openPartyModal () {
|
||||
this.$root.$emit('bv::show::modal', 'create-party-modal');
|
||||
},
|
||||
showBuyGemsModal (startingPage) {
|
||||
this.$store.state.gemModalOptions.startingPage = startingPage;
|
||||
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Gems > Toolbar',
|
||||
});
|
||||
|
||||
this.$root.$emit('bv::show::modal', 'buy-gems', {alreadyTracked: true});
|
||||
},
|
||||
dropdownDesktop (hover) {
|
||||
if (this.isDesktop() && hover.target.classList.contains('droppable')) {
|
||||
this.dropdown(hover.target);
|
||||
}
|
||||
},
|
||||
dropdownMobile (click) {
|
||||
this.dropdown(click.currentTarget.parentElement);
|
||||
},
|
||||
dropdown (element) {
|
||||
let droppedElement = document.getElementsByClassName('down')[0];
|
||||
if (droppedElement && droppedElement !== element) {
|
||||
droppedElement.classList.remove('down');
|
||||
if (droppedElement.lastChild) {
|
||||
droppedElement.lastChild.style.maxHeight = 0;
|
||||
}
|
||||
}
|
||||
|
||||
element.classList.toggle('down');
|
||||
element.lastChild.style.maxHeight = element.classList.contains('down') ? `${element.lastChild.scrollHeight}px` : 0;
|
||||
},
|
||||
closeMenu () {
|
||||
if (this.isMobile()) {
|
||||
this.menuIsOpen = false;
|
||||
|
||||
Array.from(document.getElementsByClassName('droppable')).forEach(droppableElement => {
|
||||
droppableElement.classList.remove('down');
|
||||
droppableElement.lastChild.style.maxHeight = 0;
|
||||
});
|
||||
}
|
||||
},
|
||||
isMobile () {
|
||||
return document.documentElement.clientWidth < 992;
|
||||
},
|
||||
isDesktop () {
|
||||
return !this.isMobile();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template lang="pug" functional>
|
||||
span.message-count(
|
||||
:class="{'top-count': props.top === true, 'top-count-gray': props.gray === true}"
|
||||
) {{props.count}}
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.message-count {
|
||||
background-color: $blue-50;
|
||||
border-radius: 50%;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
float: right;
|
||||
color: $white;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.message-count.top-count {
|
||||
position: absolute;
|
||||
right: 0.3em;
|
||||
top: -0.8em;
|
||||
padding: 0.2em;
|
||||
background-color: $red-50;
|
||||
}
|
||||
|
||||
.message-count.top-count-gray {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,162 @@
|
||||
<template lang="pug">
|
||||
.notification.dropdown-item.dropdown-separated.d-flex.justify-content-between(
|
||||
@click="clicked"
|
||||
)
|
||||
.notification-icon.d-flex.justify-content-center.align-items-center(
|
||||
v-if="hasIcon",
|
||||
:class="{'is-not-bailey': isNotBailey}",
|
||||
)
|
||||
slot(name="icon")
|
||||
.notification-content
|
||||
slot(name="content")
|
||||
.notification-remove(@click.stop="canRemove ? remove() : null",)
|
||||
.svg-icon(
|
||||
v-if="canRemove",
|
||||
v-html="icons.close",
|
||||
)
|
||||
</template>
|
||||
|
||||
<style lang="scss"> // Not scoped because the classes could be used in i18n strings
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
.notification-small {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
.notification-ellipses {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.notification-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.notification-bold-blue {
|
||||
font-weight: bold;
|
||||
color: $blue-10;
|
||||
}
|
||||
|
||||
.notification-bold-purple {
|
||||
font-weight: bold;
|
||||
color: $purple-300;
|
||||
}
|
||||
|
||||
.notification-yellow {
|
||||
color: #bf7d1a;
|
||||
}
|
||||
|
||||
.notification-green {
|
||||
color: #1CA372;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.notification {
|
||||
width: 378px;
|
||||
max-width: 100%;
|
||||
padding: 9px 20px 10px 24px;
|
||||
overflow: hidden;
|
||||
|
||||
&:active, &:hover {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
margin-right: 16px;
|
||||
|
||||
&.is-not-bailey {
|
||||
width: 31px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.notifications-buttons {
|
||||
margin-top: 12px;
|
||||
|
||||
.btn {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
// total distance from notification top and bottom edges are 15 and 16 pixels
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
flex-grow: 1;
|
||||
white-space: normal;
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 1.43;
|
||||
color: $gray-50;
|
||||
|
||||
max-width: calc(100% - 26px); // to make space for the close icon
|
||||
}
|
||||
|
||||
.notification-remove {
|
||||
// total distance from the notification top edge is 20 pixels
|
||||
margin-top: 7px;
|
||||
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-left: 12px;
|
||||
padding: 4px;
|
||||
|
||||
.svg-icon {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import closeIcon from 'assets/svg/close.svg';
|
||||
import { mapActions, mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove', 'hasIcon', 'readAfterClick'],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
close: closeIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
isNotBailey () {
|
||||
return this.notification.type !== 'NEW_STUFF';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
readNotification: 'notifications:readNotification',
|
||||
}),
|
||||
clicked () {
|
||||
if (this.readAfterClick === true) {
|
||||
this.readNotification({notificationId: this.notification.id});
|
||||
}
|
||||
|
||||
this.$emit('click');
|
||||
},
|
||||
remove () {
|
||||
if (this.notification.type === 'NEW_CHAT_MESSAGE') {
|
||||
const groupId = this.notification.data.group.id;
|
||||
this.$store.dispatch('chat:markChatSeen', {groupId});
|
||||
if (this.user.newMessages[groupId]) {
|
||||
this.$delete(this.user.newMessages, groupId);
|
||||
}
|
||||
} else {
|
||||
this.readNotification({notificationId: this.notification.id});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,35 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="true",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action"
|
||||
)
|
||||
div(slot="content", v-html="$t('cardReceived', {card: cardString})")
|
||||
div(slot="icon", :class="cardClass")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
computed: {
|
||||
cardString () {
|
||||
return this.$t(`${this.notification.data.card}Card`);
|
||||
},
|
||||
cardClass () {
|
||||
return `notif_inventory_special_${this.notification.data.card}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$router.push({name: 'items'});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template lang="pug" functional>
|
||||
div {{ props.notification }}
|
||||
</template>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="false",
|
||||
:notification="notification",
|
||||
@click="action",
|
||||
)
|
||||
div(slot="content")
|
||||
div(v-html="notification.data.message")
|
||||
.notifications-buttons
|
||||
.btn.btn-small.btn-success(@click.stop="approve()") {{ $t('approve') }}
|
||||
.btn.btn-small.btn-warning(@click.stop="needsWork()") {{ $t('needsWork') }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import sync from 'client/mixins/sync';
|
||||
|
||||
export default {
|
||||
mixins: [sync],
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
// Check that the notification has all the necessary data (old ones are missing some fields)
|
||||
notificationHasData () {
|
||||
return Boolean(this.notification.data.groupTaskId && this.notification.data.userId);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
const groupId = this.notification.data.group.id;
|
||||
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: { groupId }});
|
||||
},
|
||||
async approve () {
|
||||
// Redirect users to the group tasks page if the notification doesn't have data
|
||||
if (!this.notificationHasData) {
|
||||
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: {
|
||||
groupId: this.notification.data.groupId,
|
||||
}});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.$store.dispatch('tasks:approve', {
|
||||
taskId: this.notification.data.groupTaskId,
|
||||
userId: this.notification.data.userId,
|
||||
});
|
||||
|
||||
this.sync();
|
||||
},
|
||||
async needsWork () {
|
||||
// Redirect users to the group tasks page if the notification doesn't have data
|
||||
if (!this.notificationHasData) {
|
||||
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: {
|
||||
groupId: this.notification.data.groupId,
|
||||
}});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(this.$t('confirmNeedsWork'))) return;
|
||||
|
||||
await this.$store.dispatch('tasks:needsWork', {
|
||||
taskId: this.notification.data.groupTaskId,
|
||||
userId: this.notification.data.userId,
|
||||
});
|
||||
|
||||
this.sync();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,27 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="false",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action",
|
||||
)
|
||||
.notification-green(slot="content", v-html="notification.data.message")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
const groupId = this.notification.data.groupId;
|
||||
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: { groupId }});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,26 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="false",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action",
|
||||
)
|
||||
div(slot="content", v-html="notification.data.message")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$router.push({ name: 'tasks'});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,27 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="false",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action",
|
||||
)
|
||||
div(slot="content", v-html="notification.data.message")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
const groupId = this.notification.data.groupId;
|
||||
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: { groupId }});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,27 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="false",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action",
|
||||
)
|
||||
.notification-yellow(slot="content", v-html="notification.data.message")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
const groupId = this.notification.data.group.id;
|
||||
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: { groupId }});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user