old client structure

This commit is contained in:
Matteo Pagliazzi
2019-10-01 11:44:38 +02:00
parent 5b61f65711
commit b44fbdcb14
906 changed files with 44 additions and 44 deletions
@@ -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>