Update Party / Group Sidebar / Quest states (#12793)

* move groups/sidebar to groupSidebar.vue

* lint files

* extract group/party sidebar to rightSidebar.vue

* wip stories with example data

* update stories - wip sidebar re-styling

* message party / group leader + move items to the menu

* update paddings /place for quest section

* invite to party / guild

* update labels (* Party / Guild )

* guild-background to group-background

* correct menu order + missing a label based on the group type

* no quest - styles / layout applied

* quest owner / not started - styles applied   + extracted questActions from questDetailsModal.vue to a mixin

* no challenge style

* hover with underlines

* quest-pending area layout / margins

* "Collection Quest/Quest Owner Participating" Styling Done

* group sidebar menu with icons / background

* remove most participate button styles

* fix quest-invite panel

* move "Start Quest" + add "Leave Quest"

* Not Participating + Boss + Rage Quests restyling

* party quest changes - invitedToQuest + button styles + no-items style + view details

* fix icons + rage value + colors

* fix duplicate key

* hide items label if 0 items found + hide pending damage if there is none + sidebar section margin + fix percent calculation 0 => 0%

* combine quest abandon / cancel to one call + hide begin if quest has already started + close modal if quest was canceled

* remove unused translate string

* allow leaving an accepted but inactive quest + disable leave when user is quest leader

* update "are you sure" questions - remove "doubleSureAbort" - add "sureLeaveInactive"

* sidebar margins + menu icon color

* refactored css rules

* improve some styles

* fix button spacing

* fix dropmenu with icon hover

* hide leave quest for leaders + fix quest buttons spacing

* add pending items label

* remove "X items found" label

* first round of fixes

* last v-once

* Update Quest Dialogs (#13112)

* new quest rewards panel + extract questPopover and itemWithLabel

* WIP: questInfo still not applying the row-height..

* split up start-quest-modal into select and detail modal - also rename the current quest-details to be the group-quest-details modal

* remove start-quest-modal from modal-scss

* update package-lock

* WIP before using the quest sidebar branch as a base

* move quest detail actions to the "new" details dialog

* quest details layout for owner / participant

* fix quest rewards - open details modal from sidebar

* apply quest-details dialog styles to the buyQuestModal one

* fix quest reward icons / popover / texts

* WIP back to quest selection

* fix lint

* merge selectQuestModal.vue with questDetailModal.vue + UI for the select quest

* fix margins / layout / labels

* fix quest detail + wip invitationListModal.vue / participantListModal.vue

* fix questmodal user label centered

* fix centered reward items + grouping items and adding a count-badge

* sort quests by AZ or quantity

* invitations modal

* remove console.info

* complete participantListModal.vue + extracted getClassName

* missed a file for getClassName extraction

* fix invitations

* select the actual quest on details

* fix margins on invite to party / start quest buttons

* replace buyQuestModal close button and title

* fix recursion due to the same name

* missing import

* sort quantity by highest first

* fix "Can't find a Quest to start" styles

* fix "your balance" padding

* fix quest collections / drop items

* fix member details in participants list

* fix quest info

* remove nullable because the build doesn't like it (on this file..)

* add questCompleted to the stories + fix getDropName

* replace quest-rewards in questCompleted.vue

* fix questCompleted.vue style

* delete obsolete components

* add missing spritesheets to storebook

* requested pr changes

* refactored fetchMember

* revert optional chaining

* fix merge conflicts

* fix rightSidebar hover colors - $scss var to css var

* overflow auto instead of scroll

* prevent wrapping of quest collections

* rollback to multi line quest items

* use min-width for the quest popover
This commit is contained in:
negue
2021-05-28 23:11:43 +02:00
committed by GitHub
parent 33e7a378f7
commit a8b58815b4
69 changed files with 3967 additions and 1327 deletions

View File

@@ -61,15 +61,6 @@ describe('POST /groups/:groupId/quests/leave', () => {
});
});
it('returns an error when quest is not active', async () => {
await expect(partyMembers[0].post(`/groups/${questingGroup._id}/quests/leave`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('noActiveQuestToLeave'),
});
});
it('returns an error when quest leader attempts to leave', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
@@ -97,18 +88,14 @@ describe('POST /groups/:groupId/quests/leave', () => {
});
});
it('leaves a quest', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
const leaveResult = await partyMembers[0].post(`/groups/${questingGroup._id}/quests/leave`);
async function letPartyMemberLeaveAndCheckChanges (partyMember) {
const leaveResult = await partyMember.post(`/groups/${questingGroup._id}/quests/leave`);
await Promise.all([
partyMembers[0].sync(),
partyMember.sync(),
questingGroup.sync(),
]);
expect(partyMembers[0].party.quest).to.eql({
expect(partyMember.party.quest).to.eql({
key: null,
progress: {
up: 0,
@@ -120,6 +107,29 @@ describe('POST /groups/:groupId/quests/leave', () => {
RSVPNeeded: false,
});
expect(questingGroup.quest).to.deep.equal(leaveResult);
expect(questingGroup.quest.members[partyMembers[0]._id]).to.be.false;
expect(questingGroup.quest.members[partyMember._id]).to.be.false;
}
it('leaves an active quest', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
await questingGroup.sync();
expect(questingGroup.quest.active).to.eql(true);
await letPartyMemberLeaveAndCheckChanges(partyMembers[0]);
});
it('leaves an inactive quest ', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await questingGroup.sync();
expect(questingGroup.quest.active).to.eql(false);
await letPartyMemberLeaveAndCheckChanges(partyMembers[0]);
});
});

View File

@@ -31,19 +31,45 @@ import '../../src/assets/css/sprites/spritesmith-main-23.css';
import '../../src/assets/css/sprites/spritesmith-main-24.css';
import '../../src/assets/css/sprites/spritesmith-main-25.css';
import '../../src/assets/css/sprites/spritesmith-main-26.css';
import '../../src/assets/css/sprites/spritesmith-main-27.css';
import '../../src/assets/css/sprites/spritesmith-main-28.css';
import '../../src/assets/css/sprites/spritesmith-main-29.css';
import Vue from 'vue';
import BootstrapVue from 'bootstrap-vue';
import StoreModule from '@/libs/store';
import getStore from '@/store';
import i18n from '../../../common/script/i18n';
// couldn't inject the languages easily,
// so just a "$t()" string to show that this will be translated
Vue.prototype.$t = function translateString (...args) {
i18n.t = function translateString (...args) {
return `$t(${JSON.stringify(args)})`;
};
Vue.prototype.$t = i18n.t;
Vue.use(BootstrapVue);
Vue.use(StoreModule);
const store = getStore();
store.state.user.data = {
stats: {},
tags: [],
items: {
quests: {
moon1: 3,
},
},
party: {
quest: {
},
},
};
Vue.prototype.$store = store;
const req = require.context('../../src', true, /.stories.js$/);
function loadStories () {

View File

@@ -3,11 +3,12 @@
display: inline-block;
}
.content {
color: white;
background: grey;
}
.inline-block {
display: inline-block;
}
.component-showcase {
position: absolute;
margin: 20px;
width: calc(100% - 40px);
}

View File

@@ -1,3 +1,5 @@
import { v4 as generateUUID } from 'uuid';
export const userStyles = {
contributor: {
admin: true,
@@ -72,4 +74,11 @@ export const userStyles = {
maxHealth: 50,
maxMP: 158,
},
profile: {
name: 'user',
},
_id: generateUUID(),
flags: {
classSelected: true,
},
};

View File

@@ -39,6 +39,7 @@
// shared dropdown-item styles
.dropdown-item {
// header items & not selectList-items
padding-left: 24px;
padding-top: 8px;
padding-bottom: 8px;
@@ -53,14 +54,13 @@
background-color: inherit;
}
&:active, &:hover, &:focus, &.active {
background-color: inherit !important;
color: $purple-300 !important;
color: var(--hover-color, $purple-300) !important;
}
&:hover {
background-color: rgba($purple-600, 0.25) !important;
background-color: var(--hover-background, rgba($purple-600, 0.25)) !important;
}
&.dropdown-inactive {
@@ -71,6 +71,21 @@
color: inherit !important;
}
}
.with-icon {
display: flex;
align-items: center;
.svg-icon {
margin: 0 0.5rem 0 0;
}
}
&:not(:hover) {
.with-icon .svg-icon {
color: $gray-200;
}
}
}
.dropdown + .dropdown {

View File

@@ -16,6 +16,12 @@
fill: currentColor;
}
}
&.color-stroke {
svg path {
stroke: currentColor;
}
}
}
.icon-16 {
@@ -33,6 +39,11 @@
height: 24px;
}
.icon-48 {
width: 48px;
height: 48px;
}
.icon-10 {
width: 10px;
height: 10px;

View File

@@ -107,5 +107,5 @@
}
.questPopover {
width: 200px;
min-width: 200px;
}

View File

@@ -41,88 +41,6 @@
}
}
#start-quest-modal, #buy-quest-modal {
@media only screen and (max-width: 1200px) {
.modal-dialog {
max-width: 33%;
.left-panel {
left: initial;
width: 100%;
right: 100%;
.col-4 {
width: 100px;
}
}
.side-panel, .right-sidebar {
left: calc(100% - 10px);
max-width: 100%;
right: initial;
.questRewards {
width: 90%;
.reward-item {
width: 100%;
}
}
}
}
}
@media only screen and (max-width: 1000px) {
.modal-dialog {
max-width: 80%;
width: 80% !important;
.modal-body {
flex-direction: column;
display: flex;
div:nth-child(1) { order: 3; }
div:nth-child(2) { order: 1; }
div:nth-child(3) { order: 4; }
div:nth-child(4) { order: 5; }
div:nth-child(5) { order: 2; }
.left-panel {
border-radius: 8px;
position: static;
right: initial;
margin: 20px 0;
height: auto;
width: 100%;
z-index: 0;
order: 3;
.col-4 {
max-width: 100px;
}
}
.side-panel, .right-sidebar {
margin: 20px 0 0 0;
position: static;
box-shadow: none;
height: auto;
width: 100%;
z-index: 0;
order: 2;
left: 0;
.questRewards {
padding: 0 2em 2em 2em;
width: 100%;
z-index: 0;
}
}
}
}
}
}
#subscription-cancel-modal, #subscription-canceled-modal {
.modal-content {
background: transparent;

View File

@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<g fill="none" fill-rule="evenodd">
<g>
<g>
<g>
<path fill="#F74E52" d="M0 2L3.333 0 8 2.533 12.667 0 16 2 16 8 12.667 12.667 8 16 3.333 12.667 0 8z" transform="translate(-40 -2020) translate(0 1632) translate(40 388)"/>
<path fill="#FF6165" d="M4.267 11.733L1.333 7.6 1.333 2.733 3.333 1.533 8 4.067 12.667 1.533 14.667 2.733 14.667 7.6 11.733 11.733 8 14.333z" transform="translate(-40 -2020) translate(0 1632) translate(40 388)"/>
<path fill="#FFF" d="M8 9.667L11.733 11.733 8 14.333z" opacity=".5" transform="translate(-40 -2020) translate(0 1632) translate(40 388)"/>
<path fill="#B52428" d="M8 9.667L4.267 11.733 8 14.333z" opacity=".35" transform="translate(-40 -2020) translate(0 1632) translate(40 388)"/>
<path fill="#FFF" d="M4.267 11.733L1.333 7.6 8 9.667z" opacity=".25" transform="translate(-40 -2020) translate(0 1632) translate(40 388)"/>
<path fill="#B52428" d="M11.733 11.733L14.667 7.6 8 9.667z" opacity=".5" transform="translate(-40 -2020) translate(0 1632) translate(40 388)"/>
<path fill="#B52428" d="M8 9.667L12.667 1.533 14.667 2.733 14.667 7.6z" opacity=".35" transform="translate(-40 -2020) translate(0 1632) translate(40 388)"/>
<path fill="#B52428" d="M8 9.667L3.333 1.533 1.333 2.733 1.333 7.6z" opacity=".5" transform="translate(-40 -2020) translate(0 1632) translate(40 388)"/>
<path fill="#FFF" d="M8 9.667L3.333 1.533 8 4.067z" opacity=".5" transform="translate(-40 -2020) translate(0 1632) translate(40 388)"/>
<path fill="#FFF" d="M8 9.667L12.667 1.533 8 4.067z" opacity=".25" transform="translate(-40 -2020) translate(0 1632) translate(40 388)"/>
<path fill="#FFF" d="M5.733 10.267L3.333 6.933 3.333 3.867 3.4 3.867 8 6.333 12.6 3.867 12.667 3.867 12.667 6.933 10.267 10.267 8 11.933z" opacity=".5" transform="translate(-40 -2020) translate(0 1632) translate(40 388)"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<path id="xnmy9c4dda" d="M11 6H6V3L0 8l6 5v-3h5V6zm5-4v12c0 1.104-.896 2-2 2H9v-2h5V2H9V0h5c1.104 0 2 .896 2 2z"/>
</svg>

After

Width:  |  Height:  |  Size: 253 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="17"><defs><path id="a" d="M10 13v1H6v-1h4zm0-2v1H6v-1h4zM8 2l5 6h-3v2H6V8H3l5-6z"/></defs><g transform="rotate(-90 8 8)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#BDA8FF" xlink:href="#a"/><g fill="#878190" mask="url(#b)"><path d="M0 0h16v16H0z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 419 B

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="9" height="14" viewBox="0 0 9 14">
<defs>
<path id="lvd11k063a" d="M3.75 13.36L5.487 15.125 12.25 8.25 5.487 1.375 3.75 3.14 8.776 8.25z"/>
</defs>
<g fill="none" fill-rule="evenodd">
<g>
<g>
<g transform="translate(-640 -837) translate(496 56) rotate(90 -311.5 468.5)">
<use fill="#46a7d9" fill-rule="nonzero" transform="rotate(90 8 8.25)" xlink:href="#lvd11k063a"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 601 B

View File

@@ -1,18 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="16" viewBox="0 0 14 16">
<g fill="none" fill-rule="nonzero">
<path fill="#24CC8F" d="M0 6l6.667-6 6.666 6-6.666 10z"/>
<path fill="#24CC8F" d="M0 6l6.667-6 6.666 6-6.666 10z"/>
<path fill="#FFF" d="M11.6 6.2l-4.933-1V1.8z" opacity=".25"/>
<path fill="#FFF" d="M6.667 5.2l-4.934 1 4.934-4.4z" opacity=".5"/>
<path fill="#5AE4B2" d="M6.667 5.2v8.4L1.733 6.2z"/>
<path fill="#1B996B" d="M11.6 6.2l-4.933 7.4V5.2z" opacity=".35"/>
<path fill="#F47825" d="M13.333 6L6.667 0 0 6l1.333 2L0 10l6.667 6 6.666-6L12 8z"/>
<path fill="#FFF" d="M11.6 6.2l-4.933-1V1.8z" opacity=".25"/>
<path fill="#FFF" d="M6.667 5.2l-4.934 1 4.934-4.4z" opacity=".5"/>
<path fill="#FFF" d="M6.667 5.2v8.4L5.37 11.653 1.733 6.2zM6.667 10.8V2.4l1.296 1.947L11.6 9.8z" opacity=".25"/>
<path fill="#B4591B" d="M11.6 6.2l-4.933 7.4V5.2zM1.733 9.8l4.934 1v3.4z" opacity=".35"/>
<path fill="#FFF" d="M6.667 10.8l4.933-1-4.933 4.4z" opacity=".5"/>
<path fill="#FFF" d="M1.733 9.8l4.934-7.4v8.4z" opacity=".25"/>
<path fill="#FFF" d="M3.5 9.533L4.54 8 3.5 6.467l3.167-2.8 3.166 2.8L8.793 8l1.04 1.533-3.166 2.8z" opacity=".5"/>
<g fill="none" fill-rule="evenodd">
<g fill-rule="nonzero">
<g>
<g>
<g>
<path fill="#24CC8F" d="M0 6L5.833 0 11.667 6 5.833 16z" transform="translate(-524 -2070) translate(484 1632) translate(40 438) translate(.875)"/>
<path fill="#24CC8F" d="M0 6L5.833 0 11.667 6 5.833 16z" transform="translate(-524 -2070) translate(484 1632) translate(40 438) translate(.875)"/>
<path fill="#FFF" d="M10.15 6.2L5.833 5.2 5.833 1.8z" opacity=".25" transform="translate(-524 -2070) translate(484 1632) translate(40 438) translate(.875)"/>
<path fill="#FFF" d="M5.833 5.2L1.517 6.2 5.833 1.8z" opacity=".5" transform="translate(-524 -2070) translate(484 1632) translate(40 438) translate(.875)"/>
<path fill="#5AE4B2" d="M5.833 5.2L5.833 13.6 1.517 6.2z" transform="translate(-524 -2070) translate(484 1632) translate(40 438) translate(.875)"/>
<path fill="#1B996B" d="M10.15 6.2L5.833 13.6 5.833 5.2z" opacity=".35" transform="translate(-524 -2070) translate(484 1632) translate(40 438) translate(.875)"/>
<path fill="#F47825" d="M11.667 6L5.833 0 0 6 1.167 8 0 10 5.833 16 11.667 10 10.5 8z" transform="translate(-524 -2070) translate(484 1632) translate(40 438) translate(.875)"/>
<path fill="#FFF" d="M10.15 6.2L5.833 5.2 5.833 1.8z" opacity=".25" transform="translate(-524 -2070) translate(484 1632) translate(40 438) translate(.875)"/>
<path fill="#FFF" d="M5.833 5.2L1.517 6.2 5.833 1.8z" opacity=".5" transform="translate(-524 -2070) translate(484 1632) translate(40 438) translate(.875)"/>
<path fill="#FFF" d="M5.833 5.2L5.833 13.6 4.699 11.653 1.517 6.2zM5.833 10.8L5.833 2.4 6.968 4.347 10.15 9.8z" opacity=".25" transform="translate(-524 -2070) translate(484 1632) translate(40 438) translate(.875)"/>
<path fill="#B4591B" d="M10.15 6.2L5.833 13.6 5.833 5.2zM1.517 9.8L5.833 10.8 5.833 14.2z" opacity=".35" transform="translate(-524 -2070) translate(484 1632) translate(40 438) translate(.875)"/>
<path fill="#FFF" d="M5.833 10.8L10.15 9.8 5.833 14.2z" opacity=".5" transform="translate(-524 -2070) translate(484 1632) translate(40 438) translate(.875)"/>
<path fill="#FFF" d="M1.517 9.8L5.833 2.4 5.833 10.8z" opacity=".25" transform="translate(-524 -2070) translate(484 1632) translate(40 438) translate(.875)"/>
<path fill="#FFF" d="M3.063 9.533L3.973 8 3.063 6.467 5.833 3.667 8.604 6.467 7.694 8 8.604 9.533 5.833 12.333z" opacity=".5" transform="translate(-524 -2070) translate(484 1632) translate(40 438) translate(.875)"/>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<path id="re6owe1t5a" d="M7 10l1.06 1.94L10 13l-1.94 1.06L7 16l-1.06-1.94L4 13l1.94-1.06L7 10zm5.5-6l1.237 2.263L16 7.5l-2.263 1.237L12.5 11l-1.237-2.263L9 7.5l2.263-1.237L12.5 4zM4 0l1.414 2.586L8 4 5.414 5.414 4 8 2.586 5.414 0 4l2.586-1.414L4 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 394 B

View File

@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<defs>
<path id="rt3mruthma" d="M11.2 13.2c0-1.231-.467-2.35-1.23-3.2h1.23c1.765 0 3.2 1.435 3.2 3.2h-3.2zm-9.6 0c0-1.765 1.435-3.2 3.2-3.2h1.6c1.765 0 3.2 1.435 3.2 3.2h-8zm4-9.6C6.926 3.6 8 4.674 8 6c0 1.326-1.074 2.4-2.4 2.4-1.326 0-2.4-1.074-2.4-2.4 0-1.326 1.074-2.4 2.4-2.4zm3.452.415C9.436 3.754 9.9 3.6 10.4 3.6c1.326 0 2.4 1.074 2.4 2.4 0 1.326-1.074 2.4-2.4 2.4-.5 0-.964-.154-1.348-.415.34-.587.548-1.26.548-1.985 0-.726-.209-1.398-.548-1.985zm4.154 4.829C13.942 8.118 14.4 7.112 14.4 6c0-2.206-1.794-4-4-4-.904 0-1.73.313-2.4.82C7.33 2.314 6.504 2 5.6 2c-2.206 0-4 1.794-4 4 0 1.112.458 2.118 1.194 2.844C1.146 9.604 0 11.266 0 13.2c0 .884.716 1.6 1.6 1.6h12.8c.884 0 1.6-.716 1.6-1.6 0-1.934-1.146-3.596-2.794-4.356z"/>
</defs>
<g fill="none" fill-rule="evenodd">
<g>
<g>
<g transform="translate(-1228 -1456) translate(1216 1416) translate(12 40)">
<mask id="6szqyv7o1b" fill="#fff">
<use xlink:href="#rt3mruthma"/>
</mask>
<use fill="#878190" xlink:href="#rt3mruthma"/>
<g fill="#878190" mask="url(#6szqyv7o1b)">
<path d="M0 0H16V16H0z"/>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -18,11 +18,11 @@
v-if="questData.completion && typeof questData.completion === 'function'"
v-html="questData.completion()"
></p>
<div class="quest-rewards text-center">
<div class="text-center">
<h3 v-once>
{{ $t('paymentYouReceived') }}
</h3>
<questDialogDrops :item="questData" />
<quest-rewards :quest="questData" />
</div>
</div>
<div class="modal-footer">
@@ -36,27 +36,32 @@
</b-modal>
</template>
<style scoped>
<style scoped lang="scss">
.quest {
margin: 0 auto;
}
</style>
.quest-rewards .questRewards {
margin: 0 auto;
<style lang="scss">
#quest-completed {
.quest-rewards {
margin-left: -2rem;
margin-right: -2rem;
}
}
</style>
<script>
import * as quests from '@/../../common/script/content/quests';
import questDialogDrops from '@/components/shops/quests/questDialogDrops';
import { mapState } from '@/libs/store';
import percent from '@/../../common/script/libs/percent';
import { MAX_HEALTH as maxHealth } from '@/../../common/script/constants';
import QuestRewards from '../shops/quests/questRewards';
export default {
components: {
questDialogDrops,
QuestRewards,
},
data () {
return {

View File

@@ -6,11 +6,11 @@
/>
<div
v-if="challenges.length === 0"
class="row no-quest-section"
class="row no-challenge-section"
>
<div class="col-12 text-center">
<div
class="svg-icon challenge-icon"
class="svg-icon challenge-icon color"
v-html="icons.challengeIcon"
></div>
<h4 v-once>
@@ -51,23 +51,27 @@
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.no-quest-section {
.no-challenge-section {
padding: 2em;
color: $gray-300;
h4 {
color: $gray-300;
margin-bottom: 0;
}
p {
margin-bottom: 2em;
margin-bottom: 1em;
color: $gray-100;
font-size: 0.75rem;
line-height: 1.33;
}
.svg-icon {
height: 30px;
width: 30px;
margin: 0 auto;
margin-bottom: 2em;
.challenge-icon {
width: 1.125rem;
height: 1.25rem;
margin: 0 auto 0.5em;
object-fit: contain;
border-radius: 2px;
color: $gray-200;
}
}
</style>

View File

@@ -47,7 +47,7 @@
class="btn btn-success btn-success"
@click="upgradeGroup()"
>
{{ $t('upgrade') }}
{{ $t('upgradeToGroup') }}
</button>
<div
v-if="!group.purchased.plan.dateTerminated

View File

@@ -92,7 +92,7 @@ b-dropdown(:text="$t('sort')", right=true)
import MugenScroll from 'vue-mugen-scroll';
import debounce from 'lodash/debounce';
import PublicGuildItem from './publicGuildItem';
import Sidebar from './sidebar';
import Sidebar from './groupSidebar';
import groupUtilities from '@/mixins/groupsUtilities';
import positiveIcon from '@/assets/svg/positive.svg';

View File

@@ -0,0 +1,362 @@
/* eslint-disable import/no-extraneous-dependencies */
import { storiesOf } from '@storybook/vue';
import {
collectionQuestLeaderParticipating,
collectionQuestNotParticipating,
createStory,
groupBossQuestParticipating,
groupBossQuestRage,
groupCollectionQuest,
groupCollectionQuestPending,
} from './group.stories.utils';
storiesOf('Group Components|Party/Quest States', module)
.add('Not a Member', () => createStory({
template: `
<div class="component-showcase">
<right-sidebar :group="group" :is-party="true"
:is-leader="false" :is-member="false"
class="col-12"/>
</div>
`,
data () {
return {
group: {
leader: {
},
quest: {
},
purchased: {
},
},
};
},
user: {
data: {
_id: 'some-user',
party: {
},
},
},
challengeOptions: {},
}))
.add('Member/No Quest', () => createStory({
template: `
<div class="component-showcase">
<right-sidebar :group="group" :is-party="true"
:is-leader="false" :is-member="true"
class="col-12"/>
</div>
`,
data () {
return {
group: {
leader: {
},
quest: {
},
purchased: {
},
},
};
},
user: {
data: {
_id: 'some-user',
party: {
},
},
},
challengeOptions: {},
}))
.add('Leader/No Quest', () => createStory({
template: `
<div class="component-showcase">
<right-sidebar :group="group" :is-party="true"
:is-leader="true" :is-member="true"
class="col-12"/>
</div>
`,
data () {
return {
group: {
description: 'Some text',
leader: {
},
quest: {
},
purchased: {
},
privacy: 'private',
},
};
},
user: {
data: {
_id: 'some-user',
party: {
},
},
},
challengeOptions: {},
}))
.add('Quest Owner/Quest Not Started', () => createStory({
template: `
<div class="component-showcase">
<right-sidebar :group="group" :is-party="true" :is-member="true" class="col-12"/>
</div>
`,
data () {
return {
group: groupCollectionQuest(false),
};
},
user: {
data: {
_id: '05ca98f4-4706-47b5-8d02-142e6e78ba2e',
party: {
},
},
},
challengeOptions: {},
}))
.add('Member/Quest accepted/Quest Not Started', () => createStory({
template: `
<div class="component-showcase">
<right-sidebar :group="group" :is-party="true" :is-member="true" class="col-12"/>
</div>
`,
data () {
return {
group: groupCollectionQuest(false),
};
},
user: {
data: {
_id: 'just-a-member',
party: {
},
},
},
challengeOptions: {},
}))
.add('Member/Quest accepted/Started', () => createStory({
template: `
<div class="component-showcase">
<right-sidebar :group="group" :is-party="true" :is-member="true" class="col-12"/>
</div>
`,
data () {
return {
group: groupCollectionQuest(true),
};
},
user: {
data: {
_id: 'just-a-member',
party: {
},
},
},
challengeOptions: {},
}))
.add('Member/Quest Invite Pending', () => createStory({
template: `
<div class="component-showcase">
<right-sidebar :group="group" :is-party="true"
:is-member="true"
class="col-12"/>
</div>
`,
data () {
return {
group: groupCollectionQuestPending,
};
},
user: {
data: {
_id: 'some-user',
party: {
quest: {
RSVPNeeded: true,
},
},
},
},
challengeOptions: {},
}))
.add('Collection Quest/Quest Owner Participating', () => createStory({
template: `
<div class="component-showcase">
<right-sidebar :group="group" :is-party="true" :is-member="true" :is-leader="true" class="col-12"/>
</div>
`,
data () {
return {
group: collectionQuestLeaderParticipating,
};
},
user: {
data: {
_id: '05ca98f4-4706-47b5-8d02-142e6e78ba2e',
party: {
quest: {
progress: {
up: 0,
down: 0,
collectedItems: 2,
collect: {},
},
},
},
},
},
challengeOptions: {},
}))
.add('Collection Quest/Not Participating', () => createStory({
template: `
<div class="component-showcase">
<right-sidebar :group="group" :is-party="true" :is-member="true" class="col-12"/>
</div>
`,
data () {
return {
group: collectionQuestNotParticipating,
};
},
user: {
data: {
_id: 'not-the-leader',
party: {
quest: {
progress: {
up: 0,
down: 0,
collectedItems: 2,
collect: {},
},
},
},
},
},
challengeOptions: {},
}))
.add('Boss Quest/Participating', () => createStory({
template: `
<div class="component-showcase">
<right-sidebar :group="group" :is-party="true" :is-member="true" class="col-12"/>
</div>
`,
data () {
return {
group: groupBossQuestParticipating,
};
},
user: {
data: {
_id: 'acc2950e-9919-49bc-be7f-0ec4103e9f2b',
party: {
quest: {
progress: {
up: 20,
down: 0,
collectedItems: 2,
collect: {},
},
},
},
},
},
challengeOptions: {},
}))
.add('Boss Quest/Participating - No Pending', () => createStory({
template: `
<div class="component-showcase">
<right-sidebar :group="group" :is-party="true" :is-member="true" class="col-12"/>
</div>
`,
data () {
return {
group: groupBossQuestParticipating,
};
},
user: {
data: {
_id: 'acc2950e-9919-49bc-be7f-0ec4103e9f2b',
party: {
quest: {
progress: {
up: 0,
down: 0,
collectedItems: 2,
collect: {},
},
},
},
},
},
challengeOptions: {},
}))
.add('Boss Quest/Rage Enabled', () => createStory({
template: `
<div class="component-showcase">
<right-sidebar :group="group" :is-party="true" :is-member="true" class="col-12"/>
</div>
`,
data () {
return {
group: groupBossQuestRage,
};
},
user: {
data: {
_id: 'acc2950e-9919-49bc-be7f-0ec4103e9f2b',
party: {
quest: {
progress: {
up: 20,
down: 0,
collectedItems: 2,
collect: {},
},
},
},
},
},
challengeOptions: {},
}))
.add('Not a party', () => createStory({
template: `
<div class="component-showcase">
<right-sidebar :group="group" :is-party="false" :is-member="true" class="col-12"/>
</div>
`,
data () {
return {
group: {
quest: {},
leader: {
},
},
};
},
user: {
data: {
_id: 'some-user',
party: {
},
},
},
challengeOptions: {},
}));

View File

@@ -0,0 +1,342 @@
import rightSidebar from '@/components/groups/rightSidebar';
import getters from '@/store/getters';
import content from '../../../../common/script/content';
export function createStory ({
template,
data,
user = null,
challengeOptions = {},
}) {
return {
components: { rightSidebar },
template,
data,
store: {
getters,
dispatch (id) {
if (id === 'challenges:getGroupChallenges') {
return [];
}
return null;
},
state: {
content,
user: {
data: {
party: {},
},
...user,
},
challengeOptions,
},
},
};
}
export const groupBossQuestParticipating = {
leaderOnly: { challenges: false, getGems: false },
quest: {
progress: { collect: {}, hp: 30 },
active: true,
members: { 'acc2950e-9919-49bc-be7f-0ec4103e9f2b': true },
extra: {},
key: 'moon2',
leader: 'acc2950e-9919-49bc-be7f-0ec4103e9f2b',
},
tasksOrder: {
habits: [], dailys: [], todos: [], rewards: [],
},
purchased: {
plan: {
consecutive: {
count: 0, offset: 0, gemCapExtra: 0, trinkets: 0,
},
quantity: 1,
extraMonths: 0,
gemsBought: 0,
mysteryItems: [],
},
},
privacy: 'private',
chat: [],
memberCount: 1,
challengeCount: 0,
balance: 0,
_id: '6b125aa8-ef98-4307-b5b4-181091b747c9',
type: 'party',
name: 'Testings Party',
managers: {},
categories: [],
leader: {
auth: { local: { username: 'test' } },
flags: { verifiedUsername: true },
profile: { name: 'Testing' },
_id: 'acc2950e-9919-49bc-be7f-0ec4103e9f2b',
id: 'acc2950e-9919-49bc-be7f-0ec4103e9f2b',
},
summary: 'Testings Party',
id: '6b125aa8-ef98-4307-b5b4-181091b747c9',
};
export const groupBossQuestRage = {
leaderOnly: { challenges: false, getGems: false },
quest: {
progress: { collect: {}, hp: 30, rage: 20.33434535 },
active: true,
members: { 'acc2950e-9919-49bc-be7f-0ec4103e9f2b': true },
extra: {},
key: 'dilatoryDistress2',
leader: 'acc2950e-9919-49bc-be7f-0ec4103e9f2b',
},
tasksOrder: {
habits: [], dailys: [], todos: [], rewards: [],
},
purchased: {
plan: {
consecutive: {
count: 0, offset: 0, gemCapExtra: 0, trinkets: 0,
},
quantity: 1,
extraMonths: 0,
gemsBought: 0,
mysteryItems: [],
},
},
privacy: 'private',
chat: [],
memberCount: 1,
challengeCount: 0,
balance: 0,
_id: '6b125aa8-ef98-4307-b5b4-181091b747c9',
type: 'party',
name: 'Testings Party',
managers: {},
categories: [],
leader: {
auth: { local: { username: 'test' } },
flags: { verifiedUsername: true },
profile: { name: 'Testing' },
_id: 'acc2950e-9919-49bc-be7f-0ec4103e9f2b',
id: 'acc2950e-9919-49bc-be7f-0ec4103e9f2b',
},
summary: 'Testings Party',
id: '6b125aa8-ef98-4307-b5b4-181091b747c9',
};
export function groupCollectionQuest (active) {
return {
leaderOnly: { challenges: false, getGems: false },
quest: {
progress: { collect: {} },
active,
members: {
'05ca98f4-4706-47b5-8d02-142e6e78ba2e': true,
'just-a-member': true,
'b3b0be03-3f62-49ae-b776-b16419ef32cf': null,
},
extra: {},
key: 'atom1',
leader: '05ca98f4-4706-47b5-8d02-142e6e78ba2e',
},
tasksOrder: {
habits: ['320496be-d663-4711-a7da-03205a2204b2'],
dailys: ['0c6a3ecd-dbaf-4a34-bb61-1a2ecd3daa0e', '686e7766-9cef-4b77-8c8f-f4d6c5b63a85'],
todos: ['76b3ef3e-1b01-4f24-a37e-0320f31d8132'],
rewards: ['76dad8ea-0d95-47c3-ad9a-8e136ad80b7f'],
},
purchased: {
active: true,
plan: {
consecutive: {
count: 0, offset: 0, gemCapExtra: 0, trinkets: 0,
},
quantity: 3,
extraMonths: 0,
gemsBought: 0,
mysteryItems: [],
customerId: 'group-unlimited',
dateCreated: null,
dateTerminated: null,
dateUpdated: null,
owner: '05ca98f4-4706-47b5-8d02-142e6e78ba2e',
paymentMethod: 'Group Unlimited',
planId: 'group_monthly',
subscriptionId: '',
},
},
privacy: 'private',
chat: [],
memberCount: 3,
challengeCount: 0,
balance: 0,
_id: '96ea599a-737b-47e2-ac17-8bd85b6ab62a',
type: 'party',
name: 'Party',
managers: {},
categories: [],
leader: {
auth: { local: { username: 'test2' } },
flags: { verifiedUsername: true },
profile: { name: 'MyDisplay2' },
_id: '05ca98f4-4706-47b5-8d02-142e6e78ba2e',
id: '05ca98f4-4706-47b5-8d02-142e6e78ba2e',
},
summary: 'Party',
id: '96ea599a-737b-47e2-ac17-8bd85b6ab62a',
};
}
export const groupCollectionQuestPending = {
leaderOnly: { challenges: false, getGems: false },
quest: {
progress: { collect: {} },
active: false,
members: { '05ca98f4-4706-47b5-8d02-142e6e78ba2e': true, 'b3b0be03-3f62-49ae-b776-b16419ef32cf': null },
extra: {},
key: 'atom1',
leader: '05ca98f4-4706-47b5-8d02-142e6e78ba2e',
},
tasksOrder: {
habits: ['320496be-d663-4711-a7da-03205a2204b2'],
dailys: ['0c6a3ecd-dbaf-4a34-bb61-1a2ecd3daa0e', '686e7766-9cef-4b77-8c8f-f4d6c5b63a85'],
todos: ['76b3ef3e-1b01-4f24-a37e-0320f31d8132'],
rewards: ['76dad8ea-0d95-47c3-ad9a-8e136ad80b7f'],
},
purchased: { active: true },
privacy: 'private',
chat: [],
memberCount: 2,
challengeCount: 0,
balance: 0,
_id: '96ea599a-737b-47e2-ac17-8bd85b6ab62a',
type: 'party',
name: "MyDisplay2's Party",
managers: {},
categories: [],
leader: {
auth: { local: { username: 'test2' } },
flags: { verifiedUsername: true },
profile: { name: 'MyDisplay2' },
_id: '05ca98f4-4706-47b5-8d02-142e6e78ba2e',
id: '05ca98f4-4706-47b5-8d02-142e6e78ba2e',
},
summary: "MyDisplay2's Party",
id: '96ea599a-737b-47e2-ac17-8bd85b6ab62a',
};
export const collectionQuestLeaderParticipating = {
leaderOnly: { challenges: false, getGems: false },
quest: {
progress: { collect: { fireCoral: 4, blueFins: 0 } },
active: true,
members: { '05ca98f4-4706-47b5-8d02-142e6e78ba2e': true },
extra: {},
key: 'dilatoryDistress1',
leader: '05ca98f4-4706-47b5-8d02-142e6e78ba2e',
},
tasksOrder: {
habits: ['320496be-d663-4711-a7da-03205a2204b2'],
dailys: ['0c6a3ecd-dbaf-4a34-bb61-1a2ecd3daa0e', '686e7766-9cef-4b77-8c8f-f4d6c5b63a85'],
todos: ['76b3ef3e-1b01-4f24-a37e-0320f31d8132'],
rewards: ['76dad8ea-0d95-47c3-ad9a-8e136ad80b7f'],
},
purchased: {
active: true,
plan: {
consecutive: {
count: 0, offset: 0, gemCapExtra: 0, trinkets: 0,
},
quantity: 3,
extraMonths: 0,
gemsBought: 0,
mysteryItems: [],
customerId: 'group-unlimited',
dateCreated: null,
dateTerminated: null,
dateUpdated: null,
owner: '05ca98f4-4706-47b5-8d02-142e6e78ba2e',
paymentMethod: 'Group Unlimited',
planId: 'group_monthly',
subscriptionId: '',
},
},
privacy: 'private',
chat: [],
memberCount: 2,
challengeCount: 0,
balance: 0,
_id: '96ea599a-737b-47e2-ac17-8bd85b6ab62a',
type: 'party',
name: "MyDisplay2's Party",
managers: {},
categories: [],
leader: {
auth: { local: { username: 'test2' } },
flags: { verifiedUsername: true },
profile: { name: 'MyDisplay2' },
_id: '05ca98f4-4706-47b5-8d02-142e6e78ba2e',
id: '05ca98f4-4706-47b5-8d02-142e6e78ba2e',
},
summary: "MyDisplay2's Party",
id: '96ea599a-737b-47e2-ac17-8bd85b6ab62a',
};
export const collectionQuestNotParticipating = {
leaderOnly: { challenges: false, getGems: false },
quest: {
progress: { collect: { fireCoral: 4, blueFins: 3 } },
active: true,
members: { },
extra: {},
key: 'dilatoryDistress1',
leader: '05ca98f4-4706-47b5-8d02-142e6e78ba2e',
},
tasksOrder: {
habits: ['320496be-d663-4711-a7da-03205a2204b2'],
dailys: ['0c6a3ecd-dbaf-4a34-bb61-1a2ecd3daa0e', '686e7766-9cef-4b77-8c8f-f4d6c5b63a85'],
todos: ['76b3ef3e-1b01-4f24-a37e-0320f31d8132'],
rewards: ['76dad8ea-0d95-47c3-ad9a-8e136ad80b7f'],
},
purchased: {
active: true,
plan: {
consecutive: {
count: 0, offset: 0, gemCapExtra: 0, trinkets: 0,
},
quantity: 3,
extraMonths: 0,
gemsBought: 0,
mysteryItems: [],
customerId: 'group-unlimited',
dateCreated: null,
dateTerminated: null,
dateUpdated: null,
owner: '05ca98f4-4706-47b5-8d02-142e6e78ba2e',
paymentMethod: 'Group Unlimited',
planId: 'group_monthly',
subscriptionId: '',
},
},
privacy: 'private',
chat: [],
memberCount: 2,
challengeCount: 0,
balance: 0,
_id: '96ea599a-737b-47e2-ac17-8bd85b6ab62a',
type: 'party',
name: "MyDisplay2's Party",
managers: {},
categories: [],
leader: {
auth: { local: { username: 'test2' } },
flags: { verifiedUsername: true },
profile: { name: 'MyDisplay2' },
_id: '05ca98f4-4706-47b5-8d02-142e6e78ba2e',
id: '05ca98f4-4706-47b5-8d02-142e6e78ba2e',
},
summary: "MyDisplay2's Party",
id: '96ea599a-737b-47e2-ac17-8bd85b6ab62a',
};

View File

@@ -4,9 +4,9 @@
class="row"
>
<group-form-modal v-if="isParty" />
<start-quest-modal :group="group" />
<quest-details-modal :group="group" />
<quest-detail-modal :group="group" />
<participant-list-modal :group="group" />
<invitation-list-modal :group="group" />
<group-gems-modal />
<div class="col-12 col-sm-8 standard-page">
<div class="row">
@@ -96,99 +96,21 @@
</template>
</chat>
</div>
<div class="col-12 col-sm-4 sidebar">
<div
class="row"
:class="{'guild-background': !isParty}"
>
<div class="col-12 buttons-wrapper">
<div class="button-container">
<button
v-if="isLeader && !group.purchased.active && group.privacy === 'private'"
class="btn btn-success btn-success"
@click="upgradeGroup()"
>
{{ $t('upgrade') }}
</button>
</div>
<div class="button-container">
<button
v-if="isLeader || isAdmin"
v-once
class="btn btn-primary"
b-btn="b-btn"
@click="updateGuild"
>
{{ $t('edit') }}
</button>
</div>
<div class="button-container">
<button
v-if="!isMember"
class="btn btn-success btn-success"
@click="join()"
>
{{ $t('join') }}
</button>
</div>
<div class="button-container">
<button
v-once
class="btn btn-primary"
@click="showInviteModal()"
>
{{ $t('invite') }}
</button>
<!-- @TODO: hide the invitation button
if there's an active group plan and the player is not the leader-->
</div>
<div class="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-->
</div>
<div class="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>
</div>
</div>
<div class="px-3 py-3">
<quest-sidebar-section
v-if="isParty"
<right-sidebar
:is-admin="isAdmin"
:is-leader="isLeader"
:is-member="isMember"
:is-party="isParty"
:group="group"
:search-id="searchId"
class="col-12 col-sm-4"
@leave="clickLeave()"
@join="join()"
@messageLeader="messageLeader()"
@upgradeGroup="upgradeGroup"
@updateGuild="updateGuild"
@showInviteModal="showInviteModal()"
/>
<sidebar-section
v-if="!isParty"
:title="$t('guildSummary')"
>
<p v-markdown="group.summary"></p>
</sidebar-section>
<sidebar-section :title="$t('groupDescription')">
<p v-markdown="group.description"></p>
</sidebar-section>
<sidebar-section
:title="$t('challenges')"
:tooltip="$t('challengeDetails')"
>
<group-challenges :group="group" />
</sidebar-section>
</div>
<div class="text-center">
<button
v-if="isMember"
class="btn btn-danger"
@click="clickLeave()"
>
{{ isParty ? $t('leaveParty') : $t('leaveGroup') }}
</button>
</div>
</div>
</div>
</template>
@@ -199,24 +121,12 @@
.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;
@@ -254,11 +164,6 @@
cursor: pointer;
}
.sidebar {
background-color: $gray-600;
padding-bottom: 2em;
}
.buttons-wrapper {
padding: 2.8em 24px 0em 24px;
}
@@ -278,11 +183,6 @@
}
}
.guild-background {
background-image: url('~@/assets/images/groups/grassy-meadow-backdrop.png');
height: 246px;
}
textarea {
height: 150px;
width: 100%;
@@ -390,16 +290,11 @@ import groupUtilities from '@/mixins/groupsUtilities';
import styleHelper from '@/mixins/styleHelper';
import { mapState, mapGetters } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import startQuestModal from './startQuestModal';
import questDetailsModal from './questDetailsModal';
import participantListModal from './participantListModal';
import groupFormModal from './groupFormModal';
import groupChallenges from '../challenges/groupChallenges';
import groupGemsModal from '@/components/groups/groupGemsModal';
import questSidebarSection from '@/components/groups/questSidebarSection';
import markdownDirective from '@/directives/markdown';
import chat from './chat';
import sidebarSection from '../sidebarSection';
import userLink from '../userLink';
import deleteIcon from '@/assets/svg/delete.svg';
@@ -413,17 +308,18 @@ 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';
import QuestDetailModal from './questDetailModal';
import RightSidebar from '@/components/groups/rightSidebar';
import InvitationListModal from './invitationListModal';
export default {
components: {
startQuestModal,
InvitationListModal,
QuestDetailModal,
RightSidebar,
groupFormModal,
groupChallenges,
questDetailsModal,
participantListModal,
groupGemsModal,
questSidebarSection,
sidebarSection,
userLink,
chat,
},
@@ -657,6 +553,9 @@ export default {
showGroupGems () {
this.$root.$emit('bv::show::modal', 'group-gems-modal');
},
messageLeader () {
window.open(`/private-messages?uuid=${this.group.leader.id}`);
},
},
};
</script>

View File

@@ -0,0 +1,273 @@
<template>
<b-modal
id="invitation-list"
size="md"
:hide-header="true"
:hide-footer="true"
>
<div class="dialog-close">
<close-icon @click="close()" />
</div>
<h2 class="text-center textCondensed" v-once>
{{ $t('invitations') }}
</h2>
<div
v-for="member in members"
:key="member._id"
class="member-row"
>
<div class="class-icon">
<class-badge
v-if="member.stats"
:member-class="member.stats.class"
:badge-size="40"
/>
</div>
<div class="usernames">
<user-label :user="member" class="user-label" /> <br>
<span class="username">
@{{ member.auth.local.username }}
</span>
</div>
<div :class="{
'status': true,
'accepted': member.accepted === true,
'declined': member.accepted === false,
'pending': member.accepted === null
}">
<div
v-if="member.accepted === true"
class="accepted float-right"
>
{{ $t('accepted') }}
</div>
<div
v-if="member.accepted === false"
class="declined float-right"
>
{{ $t('declined') }}
</div>
<div
v-if="member.accepted === null"
class="pending float-right"
>
{{ $t('pending') }}
</div>
<div class="circle">
<div
v-if="member.accepted === true"
class="svg-icon color"
v-html="icons.check"
></div>
<div
v-if="member.accepted === false"
class="svg-icon color"
v-html="icons.close"
></div>
</div>
</div>
</div>
</b-modal>
</template>
<style lang='scss'>
@import '~@/assets/scss/colors.scss';
#invitation-list {
.modal-header {
background-color: $gray-600;
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;
padding-bottom: 0;
}
.member-details {
margin: 0;
}
.modal-content {
// so that the rounded corners still apply
overflow: hidden;
}
.user-label {
font-size: 14px;
}
.username {
font-size: 12px;
}
}
</style>
<style lang='scss' scoped>
@import '~@/assets/scss/colors.scss';
.header-wrap {
width: 100%;
}
h2 {
color: $purple-300;
margin-top: 1rem;
}
.member-row {
padding: 0.75rem 1.5rem;
background-color: $gray-700;
display: flex;
flex-direction: row;
&:not(:last-of-type) {
border-bottom: 1px solid $gray-500;
}
}
.class-icon {
margin-right: 1rem;
}
.usernames {
flex: 3;
}
.status {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
}
.class-icon {
display: flex;
align-items: center;
}
.circle {
height: 2rem;
width: 2rem;
margin-left: 0.5rem;
border: 2px solid $gray-300;
border-radius: 1rem;
display: flex;
align-items: center;
justify-content: center;
}
.accepted {
color: $green-10;
.circle {
border-color: $green-50;
background: $green-50;
color: white;
.svg-icon {
height: 10px;
width: 13px;
}
}
}
.pending {
color: $gray-100;
}
.declined {
color: $maroon-10;
.circle {
border-color: $maroon-100;
background: $maroon-100;
color: white;
.svg-icon {
height: 12px;
width: 12px;
::v-deep {
path {
stroke: white;
}
}
}
}
}
#invitation-list_modal_body {
padding: 0;
max-height: 450px;
.member-details {
margin: 0;
}
}
</style>
<script>
import { mapGetters } from '@/libs/store';
import CloseIcon from '../shared/closeIcon';
import ClassBadge from '../members/classBadge';
import UserLabel from '../userLabel';
import svgClose from '@/assets/svg/close.svg';
import svgCheck from '@/assets/svg/check.svg';
export default {
components: {
UserLabel,
ClassBadge,
CloseIcon,
},
data () {
return {
icons: Object.freeze({
close: svgClose,
check: svgCheck,
}),
};
},
props: ['group'],
computed: {
...mapGetters({
partyMembers: 'party:members',
}),
members () {
const partyMembers = this.partyMembers || [];
return partyMembers.map(member => ({
...member,
accepted: this.group.quest.members[member._id],
}));
},
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'invitation-list');
},
},
};
</script>

View File

@@ -114,7 +114,7 @@ import groupUtilities from '@/mixins/groupsUtilities';
import PublicGuildItem from './publicGuildItem';
import Sidebar from './sidebar';
import Sidebar from './groupSidebar';
import greyBadgeIcon from '@/assets/svg/grey-badge.svg';
import positiveIcon from '@/assets/svg/positive.svg';

View File

@@ -2,8 +2,15 @@
<b-modal
id="participant-list"
size="md"
:hide-header="true"
:hide-footer="true"
>
<div class="dialog-close">
<close-icon @click="close()" />
</div>
<h2 class="text-center textCondensed" v-once>
{{ $t('participantsTitle') }}
</h2>
<div
slot="modal-header"
class="header-wrap"
@@ -29,20 +36,12 @@
<div
v-for="member in participants"
:key="member._id"
class="row"
class="member-row"
>
<div class="col-12 no-padding-left">
<member-details :member="member" />
<div class="no-padding-left">
<member-details-new :member="member" />
</div>
</div>
<div class="modal-footer">
<button
class="btn btn-primary"
@click="close()"
>
{{ $t('close') }}
</button>
</div>
</b-modal>
</template>
@@ -70,23 +69,49 @@
.modal-body {
padding-left: 0;
padding-right: 0;
padding-bottom: 0;
}
.member-details {
margin: 0;
}
.modal-content {
// so that the rounded corners still apply
overflow: hidden;
}
}
</style>
<style lang='scss' scoped>
@import '~@/assets/scss/colors.scss';
.header-wrap {
width: 100%;
}
h1 {
color: #4f2a93;
h2 {
color: $purple-300;
margin-top: 1rem;
}
.member-row {
background-color: $gray-700;
&:not(:last-of-type) {
border-bottom: 1px solid $gray-500;
}
}
.member-row {
::v-deep {
.col-4 {
padding-left: 0;
}
}
}
#participant-list_modal_body {
padding: 0;
max-height: 450px;
@@ -100,11 +125,13 @@
<script>
import { mapGetters } from '@/libs/store';
import MemberDetails from '../memberDetails';
import MemberDetailsNew from '../memberDetailsNew';
import CloseIcon from '../shared/closeIcon';
export default {
components: {
MemberDetails,
CloseIcon,
MemberDetailsNew,
},
props: ['group'],
computed: {
@@ -113,7 +140,10 @@ export default {
}),
participants () {
const partyMembers = this.partyMembers || [];
return partyMembers.filter(member => this.group.quest.members[member._id] === true);
const membersAccepted = partyMembers
.filter(member => this.group.quest.members[member._id] === true);
return membersAccepted;
},
},
methods: {

View File

@@ -0,0 +1,89 @@
/* eslint-disable import/no-extraneous-dependencies */
import { storiesOf } from '@storybook/vue';
import { withKnobs } from '@storybook/addon-knobs';
import { quests } from '@/../../common/script/content/quests';
import content from '@/../../common/script/content';
import questDetailModal from './questDetailModal';
import questCompleted from '../achievements/questCompleted';
const stories = storiesOf('Quests/Dialog', module);
stories.addDecorator(withKnobs);
stories
.add('selectQuestDialog', () => ({
components: { questDetailModal },
data () {
return {
quest: quests.goldenknight2,
questWithDrop: quests.moon1,
};
},
template: `
<div>
<quest-detail-modal :group="{}"></quest-detail-modal>
</div>
`,
mounted () {
this.$root.$emit('bv::show::modal', 'quest-detail-modal');
},
}))
.add('questDetailModal', () => ({
components: { questDetailModal },
data () {
return {
quest: quests.goldenknight2,
questWithDrop: quests.moon1,
};
},
template: `
<div>
<quest-detail-modal :group="{}"></quest-detail-modal>
</div>
`,
mounted () {
this.$root.$emit('bv::show::modal', 'quest-detail-modal', {
key: 'moon1',
from: 'sidebar',
});
},
}))
.add('quest-completed', () => ({
components: { questCompleted },
data () {
return {
quest: quests.goldenknight2,
questWithDrop: quests.moon1,
};
},
template: `
<div>
<quest-completed></quest-completed>
</div>
`,
mounted () {
this.$root.$emit('bv::show::modal', 'quest-completed');
},
store: {
state: {
content,
user: {
data: {
stats: {},
tags: [],
items: {
quests: {
moon1: 3,
},
},
party: {
quest: {
completed: 'vice3',
},
},
},
},
},
},
}));

View File

@@ -0,0 +1,72 @@
export default {
computed: {
onActiveQuest () {
return this.group.quest.active;
},
groupHasQuest () {
return this.group.quest && Boolean(this.group.quest.key);
},
canEditQuest () {
if (!this.group.quest) return false;
const isQuestLeader = this.group.quest.leader === this.user._id;
const isPartyLeader = this.group.leader && this.group.leader._id === this.user._id;
return isQuestLeader || isPartyLeader;
},
},
methods: {
async questActionsConfirmQuest () {
let count = 0;
for (const uuid in this.group.quest.members) {
if (this.group.quest.members[uuid]) count += 1;
}
if (!window.confirm(this.$t('questConfirm', {
questmembers: count,
totalmembers: this.group.memberCount,
}))) {
return false;
}
await this._questForceStart();
return true;
},
async _questForceStart () {
const quest = await this.$store.dispatch('quests:sendAction', { groupId: this.group._id, action: 'quests/force-start' });
this.group.quest = quest;
},
// this method combines both if a quest is active or not
// it'll call the appropriate api endpoint
async questActionsCancelOrAbortQuest () {
const partyState = this.$store.state.party;
if (!partyState.data) {
partyState.data = {};
}
if (this.onActiveQuest) {
if (!window.confirm(this.$t('sureAbort'))) {
return false;
}
const quest = await this.$store.dispatch('quests:sendAction', {
groupId: this.group._id,
action: 'quests/abort',
});
this.group.quest = quest;
partyState.data.quest = quest;
} else {
if (!window.confirm(this.$t('sureCancel'))) {
return false;
}
const quest = await this.$store.dispatch('quests:sendAction', {
groupId: this.group._id,
action: 'quests/cancel',
});
this.group.quest = quest;
partyState.data.quest = quest;
}
return true;
},
},
};

View File

@@ -0,0 +1,450 @@
<template>
<b-modal
id="quest-detail-modal"
title="Empty"
size="md"
:hide-footer="true"
:hide-header="true"
:modal-class="dialogClass"
>
<div class="dialog-close">
<close-icon @click="close()" />
</div>
<h2 class="text-center textCondensed">
{{ selectMode ? $t('selectQuest') : $t('questDetailsTitle') }}
</h2>
<div class="quest-panel" v-if="selectMode">
<div class="quest-panel-header">
<h3>
{{ $t('yourQuests') }}
</h3>
<div class="sort-by">
<span class="dropdown-label">{{ $t('sort') }}</span>
<select-translated-array
:right="true"
:items="['quantity', 'AZ']"
:value="sortBy"
class="inline"
:inline-dropdown="false"
@select="sortBy = $event"
/>
</div>
</div>
<div class="quest-items">
<div
v-for="item in questsInfoList"
:key="item.key"
class="quest-col"
@click="selectQuest(item)"
>
<item
:key="item.key"
:item="item"
:item-content-class="item.class"
>
<countBadge
slot="itemBadge"
:show="item.amount !== 1"
:count="item.amount"
/>
<template
slot="popoverContent"
slot-scope="context"
>
<div
class="questPopover"
>
<h4 class="popover-content-title">
{{ context.item.text }}
</h4>
<questInfo :quest="context.item" />
</div>
</template>
</item>
</div>
</div>
<div class="row">
<div class="col-10 offset-1 text-center">
<span
v-once
class="no-quest-to-start"
>
<b>{{ $t('noQuestToStartTitle') }}</b> <br>
<span v-html="$t('noQuestToStart', { questShop: '/shops/quests' })"></span>
</span>
</div>
</div>
</div>
<div v-else>
<div v-if="questData" class="quest-combined-content">
<questDialogContent
:item="questData"
:group="group"
class="quest-detail"
/>
<quest-rewards :quest="questData" class="mt-4" />
</div>
<div
v-if="!groupHasQuest"
class="text-center"
>
<button
class="btn btn-primary mt-0 invite-btn"
:disabled="!Boolean(selectedQuest) || loading"
@click="questInit()"
>
{{ $t('inviteToPartyOrQuest') }}
</button>
</div>
<div
v-if="fromSelectionDialog"
class="text-center back-to-selection"
@click="goBackToQuestSelection()"
>
<span
v-once
class="svg-icon color"
v-html="icons.navigationBack"
>
</span>
<span>
{{ $t('backToSelection') }}
</span>
</div>
<div
v-if="groupHasQuest && canEditQuest"
class="text-center actions"
>
<div>
<button
v-if="!onActiveQuest"
v-once
class="btn btn-success mb-3"
@click="questConfirm()"
>
{{ $t('startQuest') }}
</button>
</div>
<div>
<div
v-once
class="cancel"
@click="questCancel()"
>
{{ $t('cancelQuest') }}
</div>
</div>
</div>
</div>
</b-modal>
</template>
<style lang='scss' scoped>
@import '~@/assets/scss/colors.scss';
h2 {
color: $purple-300;
margin-top: 1rem;
}
.invite-btn {
margin-bottom: 1rem;
}
.back-to-selection {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
cursor: pointer;
color: $blue-10;
font-size: 14px;
line-height: 1.71;
text-align: right;
&:hover {
text-decoration: underline;
}
.svg-icon {
color: $blue-50;
height: 14px;
width: 9px;
margin-right: 0.5rem;
}
}
.quest-panel {
background-color: $gray-700;
// reset margin
margin-left: -1rem;
margin-right: -1rem;
margin-bottom: -1rem;
// add padding back
padding-top: 1rem;
padding-left: 1.5rem;
padding-right: 1.5rem;
padding-bottom: 2rem;
border-bottom-left-radius: 1rem;
border-bottom-right-radius: 1rem;
}
.quest-panel-header {
padding-bottom: 1rem;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.quest-items {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.5rem;
// somehow the browser felt like setting this 398px instead
// now its fixed to 400 :)
width: 400px;
margin-bottom: 1.5rem;
.quest-col {
::v-deep {
.item-wrapper {
margin-bottom: 0;
}
}
}
}
.quest-detail {
margin-left: 1rem;
margin-right: 1rem;
}
.no-quest-to-start {
font-size: 12px;
line-height: 1.33;
text-align: center;
color: $gray-100;
a {
color: $blue-10;
}
}
#quest-detail-modal {
::v-deep & {
.modal-dialog {
width: 448px !important;
}
}
.quest-combined-content {
margin-bottom: 1.5rem;
}
::v-deep &:not(.need-bottom-padding) {
.modal-content {
// so that the rounded corners still apply
overflow: hidden;
}
.modal-body {
padding-bottom: 0;
}
.quest-combined-content {
margin-bottom: 0;
}
}
@media only screen and (max-width: 1000px) {
.modal-dialog {
max-width: 80%;
width: 80% !important;
.modal-body {
flex-direction: column;
display: flex;
}
}
}
.actions {
padding-bottom: .5em;
.cancel {
color: $maroon-50;
}
.cancel:hover {
cursor: pointer;
text-decoration: underline;
}
}
}
</style>
<script>
import orderBy from 'lodash/orderBy';
import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import * as quests from '@/../../common/script/content/quests';
import navigationBack from '@/assets/svg/navigation_back.svg';
import questDialogContent from '../shops/quests/questDialogContent';
import closeIcon from '../shared/closeIcon';
import QuestRewards from '../shops/quests/questRewards';
import questActionsMixin from './questActions.mixin';
import SelectTranslatedArray from '../tasks/modal-controls/selectTranslatedArray';
import QuestInfo from '../shops/quests/questInfo';
import Item from '@/components/inventory/item';
import getItemInfo from '../../../../common/script/libs/getItemInfo';
import CountBadge from '../ui/countBadge';
export default {
components: {
CountBadge,
QuestRewards,
questDialogContent,
closeIcon,
SelectTranslatedArray,
QuestInfo,
Item,
},
mixins: [questActionsMixin],
props: ['group'],
data () {
return {
loading: false,
selectMode: true,
selectedQuest: {},
fromSelectionDialog: false,
icons: Object.freeze({
navigationBack,
}),
shareUserIdShown: false,
quests,
sortBy: 'AZ',
};
},
computed: {
...mapState({ user: 'user.data' }),
questData () {
return quests.quests[this.selectedQuest];
},
dialogClass () {
if (!this.groupHasQuest || this.fromSelectionDialog || this.canEditQuest) {
return 'need-bottom-padding';
}
return '';
},
questsInfoList () {
const availableQuests = Object.entries(this.user.items.quests)
.filter(([, amount]) => amount > 0);
return orderBy(availableQuests.map(([key, amount]) => {
const questItem = quests.quests[key];
return {
...getItemInfo(this.user, 'quests', questItem),
amount,
};
}), item => (this.sortBy === 'AZ' ? item.text : -item.amount));
},
},
mounted () {
const userQuests = this.user.items.quests;
for (const key in userQuests) {
if (userQuests[key] > 0) {
this.selectedQuest = key;
break;
}
}
this.$root.$on('bv::show::modal', this.handleOpen);
},
beforeDestroy () {
},
methods: {
selectQuest (selectQuestPayload) {
this.selectMode = false;
this.selectedQuest = selectQuestPayload.key;
this.fromSelectionDialog = true;
},
async questInit () {
this.loading = true;
Analytics.updateUser({
partyID: this.group._id,
partySize: this.group.memberCount,
});
const 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;
// TODO move the state updates to the action itself
const partyState = this.$store.state.party;
if (!partyState.data) {
partyState.data = {};
}
partyState.data.quest = quest;
} finally {
this.loading = false;
}
this.close();
},
close () {
this.$root.$emit('bv::hide::modal', 'quest-detail-modal');
},
async questConfirm () {
const accepted = await this.questActionsConfirmQuest();
if (accepted) {
this.close();
}
},
async questCancel () {
const accepted = await this.questActionsCancelOrAbortQuest();
if (accepted) {
this.close();
}
},
goBackToQuestSelection () {
this.selectMode = true;
},
handleOpen (_, selectQuestPayload) {
this.fromSelectionDialog = false;
if (selectQuestPayload) {
this.selectMode = false;
this.selectedQuest = selectQuestPayload.key;
} else {
this.selectMode = true;
}
},
},
};
</script>

View File

@@ -1,287 +0,0 @@
<template>
<b-modal
id="quest-details"
title="Empty"
size="md"
:hide-footer="true"
:hide-header="true"
>
<div class="left-panel content">
<h3 class="text-center">
{{ $t('participantsTitle') }}
</h3>
<div class="row">
<div class="col-10 offset-1 text-center">
<span
v-once
class="description"
>{{ $t('participantDesc') }}</span>
</div>
</div>
<div class="row">
<div
v-for="member in members"
:key="member._id"
class="col-12 member"
>
<strong :class="{'declined-name': member.accepted === false}">{{ member.name }}</strong>
<div
v-if="member.accepted === true"
class="accepted float-right"
>
{{ $t('accepted') }}
</div>
<div
v-if="member.accepted === false"
class="declined float-right"
>
{{ $t('declined') }}
</div>
<div
v-if="member.accepted === null"
class="pending float-right"
>
{{ $t('pending') }}
</div>
</div>
</div>
</div>
<div v-if="questData">
<questDialogContent :item="questData" />
</div>
<div
v-if="canEditQuest"
class="text-center actions"
>
<div>
<button
v-once
class="btn btn-secondary"
@click="questConfirm()"
>
{{ $t('begin') }}
</button>
<!-- @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>
<div>
<div
v-once
class="cancel"
@click="questCancel()"
>
{{ $t('cancel') }}
</div>
</div>
</div>
<div
v-if="questData"
class="side-panel"
>
<questDialogDrops :item="questData" />
</div>
</b-modal>
</template>
<style lang='scss' scoped>
@import '~@/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 '@/libs/store';
import * as 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 {
components: {
questDialogDrops,
questDialogContent,
},
props: ['group'],
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 () {
const partyMembers = this.partyMembers || [];
return partyMembers.map(member => ({
name: member.profile.name,
accepted: this.group.quest.members[member._id],
_id: member._id,
}));
},
canEditQuest () {
if (!this.group.quest) return false;
const isQuestLeader = this.group.quest.leader === this.user._id;
const isPartyLeader = this.group.leader._id === this.user._id;
return isQuestLeader || isPartyLeader;
},
},
methods: {
async questConfirm () {
let count = 0;
for (const uuid in this.group.quest.members) {
if (this.group.quest.members[uuid]) count += 1;
}
if (!window.confirm(this.$t('questConfirm', { // eslint-disable-line no-alert
questmembers: count,
totalmembers: this.group.memberCount,
}))) return;
this.questForceStart();
},
async questForceStart () {
const quest = await this.$store.dispatch('quests:sendAction', { groupId: this.group._id, action: 'quests/force-start' });
this.group.quest = quest;
this.close();
},
async questCancel () {
if (!window.confirm(this.$t('sureCancel'))) return; // eslint-disable-line no-alert
const 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>

View File

@@ -6,11 +6,12 @@
>
<div class="col-12 text-center">
<div
class="svg-icon"
class="svg-icon quest-icon color"
v-html="icons.questIcon"
v-once
></div>
<h4 v-once>
{{ $t('youAreNotOnQuest') }}
{{ $t('yourPartyIsNotOnQuest') }}
</h4>
<p v-once>
{{ $t('questDescription') }}
@@ -18,102 +19,78 @@
<button
v-once
class="btn btn-secondary"
@click="openStartQuestModal()"
@click="openSelectQuestModal()"
>
{{ $t('startAQuest') }}
</button>
</div>
</div>
<div
v-if="onPendingQuest && !onActiveQuest"
class="row quest-active-section"
>
<div class="col-2">
<div
class="quest"
:class="`inventory_quest_scroll_${questData.key}`"
></div>
</div>
<div class="col-6 titles">
<strong>{{ questData.text() }}</strong>
<p>{{ acceptedCount }} / {{ group.memberCount }}</p>
</div>
<div class="col-4">
<button
class="btn btn-secondary"
@click="openQuestDetails()"
>
{{ $t('details') }}
{{ $t('selectQuest') }}
</button>
</div>
</div>
<div
v-if="user.party.quest && user.party.quest.RSVPNeeded"
class="row quest-active-section quest-invite"
class="quest-active-section quest-invite"
>
<span>{{ $t('wouldYouParticipate') }}</span>
<span class="participate">{{ $t('invitedToThisQuest') }}</span>
<div class="buttons">
<button
class="btn btn-primary accept"
class="btn btn-success accept"
@click="questAccept(group._id)"
>
{{ $t('accept') }}
</button>
<button
class="btn btn-primary reject"
class="btn btn-danger reject"
@click="questReject(group._id)"
>
{{ $t('reject') }}
</button>
</div>
</div>
<div
v-if="!onPendingQuest && onActiveQuest"
class="row quest-active-section"
:class="{'not-participating': !userIsOnQuest}"
>
<div class="col-12 text-center">
<div
class="quest-boss"
:class="'quest_' + questData.key"
></div>
<h3 v-once>
{{ questData.text() }}
</h3>
<div class="quest-box">
<div
v-if="questData.collect"
class="collect-info"
>
<div class="row">
<div class="col-12">
<a
class="float-right"
@click="openParticipantList()"
>{{ $t('participantsTitle') }}</a>
</div>
</div>
<div
v-for="(value, key) in questData.collect"
:key="key"
class="row"
class="quest-item-row"
>
<div class="col-2">
<div class="quest-item-icon">
<div :class="'quest_' + questData.key + '_' + key"></div>
</div>
<div class="col-10">
<strong>{{ value.text() }}</strong>
<div class="quest-item-info">
<span class="label quest-label">{{ value.text() }}</span>
<div class="grey-progress-bar">
<div
class="collect-progress-bar"
:style="{width: (group.quest.progress.collect[key] / value.count) * 100 + '%'}"
></div>
</div>
<strong>{{ group.quest.progress.collect[key] }} / {{ value.count }}</strong>
</div>
</div>
<div
v-if="userIsOnQuest"
class="text-right"
<div class="item-progress-row">
<span
class="label item-progress"
:class="{'no-items': group.quest.progress.collect[key] === 0}"
>
{{ parseFloat(user.party.quest.progress.collectedItems) || 0 }} items found
{{ group.quest.progress.collect[key] }} / {{ value.count }}
</span>
</div>
</div>
</div>
<div v-if="hasPendingQuestItems"
class="item-progress-pending mb-2">
<div class="pending-amount pt-2 pb-2">
{{ $t('questItemsPending', { amount: user.party.quest.progress.collectedItems }) }}
</div>
</div>
</div>
<div
@@ -121,20 +98,14 @@
class="boss-info"
>
<div class="row">
<div class="col-6">
<div class="col-12">
<h4
v-once
class="float-left"
class="float-left boss-name"
>
{{ questData.boss.name() }}
</h4>
</div>
<div class="col-6">
<a
class="float-right"
@click="openParticipantList()"
>{{ $t('participantsTitle') }}</a>
</div>
</div>
<div class="row">
<div class="col-12">
@@ -143,12 +114,22 @@
class="boss-health-bar"
:style="{width: bossHpPercent + '%'}"
></div>
<div
class="pending-health-bar"
:style="{width: pendingHpPercent + '%'}"
>
</div>
</div>
</div>
</div>
<div class="row boss-details">
<div class="col-6">
<span class="float-left">
<span class="float-left hp-value">
<div
class="svg-icon health-icon"
v-html="icons.healthNoPaddingIcon"
v-once
></div>
{{
(Math.ceil(parseFloat(group.quest.progress.hp) * 100) / 100)
| localizeNumber(user.preferences.language, { toFixed:2 })
@@ -156,22 +137,30 @@
parseFloat(questData.boss.hp)
| localizeNumber(user.preferences.language, { toFixed:2 })
}}
<strong>HP</strong>
<!-- current boss hp uses ceil so
you don't underestimate damage needed to end quest-->
</span>
</div>
<div
v-if="userIsOnQuest"
v-if="userIsOnQuest && user.party.quest.progress.up"
class="col-6"
>
<!-- @TODO: Why do we not sync quest
progress on the group doc? Each user could have different progress.-->
<span class="float-right">
<span class="float-right pending-value">
<div
class="svg-icon sword-icon"
v-html="icons.swordIcon"
v-once
></div>
{{
(user.party.quest.progress.up || 0)
| floor(10)
| localizeNumber(user.preferences.language, { toFixed:1 })
}} {{ $t('pendingDamageLabel') }}
}}
{{ $t('pendingDamageLabel') }}
</span>
<!-- player's pending damage uses floor so you
don't overestimate damage you've already done-->
@@ -196,6 +185,13 @@
class="row boss-details rage-details"
>
<div class="col-6">
<span class="float-left rage-value">
<div
class="svg-icon rage-icon icon-16"
v-html="icons.rageIcon"
v-once
>
</div>
<span
class="float-left"
>{{ $t('rage') }} {{
@@ -205,19 +201,63 @@
questData.boss.rage.value
| localizeNumber(user.preferences.language)
}}</span>
<strong v-once>{{ $t('rage') }}</strong>
</span>
</div>
</div>
</div>
</div>
<button
v-if="canEditQuest"
v-once
class="btn btn-secondary"
@click="questAbort()"
</div>
</div>
<div
v-if="onPendingQuest || onActiveQuest"
class="quest-pending-section"
>
{{ $t('abort') }}
<div class="titles">
<strong>{{ questData.text() }} </strong>
<a
class="members-invited"
@click="openParticipantList()"
>
{{ $t('membersParticipating', {accepted: acceptedCount, invited: group.memberCount}) }}
</a>
</div>
<div class="quest-icon">
<div
class="quest"
:class="`inventory_quest_scroll_${questData.key}`"
></div>
</div>
</div>
<div v-if="onPendingQuest || onActiveQuest"
class="quest-buttons">
<button
class="btn btn-secondary w-100"
@click="openQuestDetails()"
>
{{ $t('viewDetails') }}
</button>
</div>
<div v-if="userIsQuestLeader && !onActiveQuest"
class="quest-buttons">
<button
class="btn btn-success w-100"
@click="startQuest()"
>
{{ $t('startQuest') }}
</button>
</div>
<div
v-if="userIsOnQuest && !userIsQuestLeader"
class="leave-quest-holder"
>
<a
v-once
class="leave-quest text-center"
@click="questLeave()"
>
{{ $t('leaveQuest') }}
</a>
</div>
</sidebar-section>
</template>
@@ -230,30 +270,49 @@
width: 25px;
}
.quest-buttons {
margin-bottom: 0.25rem;
&:nth-last-of-type(2) {
margin-bottom: 0;
}
}
.quest-buttons + .quest-buttons {
margin-top: 0.25rem;
}
.quest-boss {
margin: 0 auto;
margin: 0 auto 1.188rem;
}
.boss-health-bar {
width: 80%;
background-color: red;
height: 15px;
margin-bottom: .5em;
background-color: $red-50;
height: 0.75rem;
display: inline-block;
}
.pending-health-bar {
height: 0.75rem;
background-color: $yellow-50;
display: inline-block;
}
.rage-details {
margin-bottom: 1em;
}
.boss-health-bar.rage-bar {
margin-top: 1em;
background-color: orange;
background-color: $orange-50;
}
.grey-progress-bar {
width: 100%;
height: 15px;
height: 0.75rem;
background-color: #e1e0e3;
border-radius: 2px;
overflow: hidden;
display: flex;
}
.collect-progress-bar {
@@ -264,35 +323,84 @@
.no-quest-section {
padding: 2em;
color: $gray-300;
h4 {
color: $gray-300;
margin-bottom: 0;
}
p {
margin-bottom: 2em;
margin-bottom: 1em;
color: $gray-100;
font-size: 0.75rem;
line-height: 1.33;
}
.svg-icon {
height: 30px;
width: 30px;
margin: 0 auto;
margin-bottom: 2em;
.quest-icon {
width: 1.125rem;
height: 1.25rem;
margin: 0 auto 0.5em;
object-fit: contain;
border-radius: 2px;
color: $gray-200;
}
}
.quest-pending-section {
display: flex;
margin-bottom: 0.5rem;
.titles {
flex: 1;
margin-top: 1rem;
font-size: 0.75rem;
line-height: 1.33;
strong {
display: block;
min-height: 1rem;
font-weight: bold;
font-size: 0.75rem;
line-height: 1.33;
color: $gray-100;
margin-bottom: 0.25rem;
}
.members-invited {
min-height: 1rem;
color: $blue-10;
margin: 0;
&:hover, &:focus {
color: $blue-10;
text-decoration: underline;
}
}
}
.quest-icon {
width: 4.25rem;
height: 4.25rem;
}
}
.quest-active-section {
margin-bottom: 0.5rem;
.participate {
font-size: 0.75rem;
font-weight: bold;
line-height: 1.33;
color: $white;
}
.titles {
padding-top: .5em;
}
.quest-box {
background-image: url('~@/assets/svg/for-css/quest-border.svg');
background-size: 100% 100%;
width: 100%;
padding: .5em;
margin-bottom: 1em;
padding: 0.75rem 1rem;
border-radius: 4px;
background-color: $white;
a {
font-family: 'Roboto Condensed', sans-serif;
@@ -306,43 +414,219 @@
}
}
.boss-info, .collect-info {
width: 90%;
margin: 0 auto;
.boss-info {
text-align: left;
.boss-name {
font-size: 0.75rem;
font-weight: bold;
line-height: 1.33;
color: $gray-100;
margin-bottom: 0.25rem;
}
.boss-details {
margin-top: 0.5rem;
}
.hp-value {
font-size: 0.75rem;
line-height: 1.33;
color: $maroon-10;
display: flex;
}
.rage-value {
font-size: 0.75rem;
line-height: 1.33;
color: $orange-10;
display: flex;
height: 1rem;
.span {
align-self: center;
}
}
.pending-value {
font-size: 0.75rem;
line-height: 1.33;
text-align: right;
color: $yellow-5;
display: flex;
}
.health-icon {
width: 1rem;
height: 1rem;
object-fit: contain;
margin-right: 0.25rem;
}
.rage-icon {
width: 1rem;
height: 1rem;
object-fit: contain;
margin-right: 0.25rem;
::v-deep svg {
height: 1rem;
}
}
.sword-icon {
width: 1rem;
height: 1rem;
object-fit: contain;
margin-right: 0.25rem;
}
strong {
margin-left: 0.25rem;
}
}
}
.quest-invite {
background-color: #2995cd;
color: #fff;
padding: 1em;
background-color: $blue-10;
color: $white;
display: flex;
border-radius: 2px;
span {
margin-top: .3em;
font-size: 14px;
font-weight: bold;
.participate {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
margin-left: 1rem;
flex: 1;
}
.buttons {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
margin-right: 0.5rem;
}
.accept, .reject {
padding: .2em 1em;
font-size: 12px;
height: 24px;
font-size: 0.75rem;
font-weight: bold;
line-height: 1.33;
text-align: center;
color: $white;
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;
margin: 0 0.5rem 0 0;
}
.reject {
border-radius: 2px;
background-color: #f74e52;
}
}
.leave-quest-holder {
display: flex;
justify-content: center;
}
.leave-quest {
font-size: 0.875rem;
line-height: 1.71;
color: $maroon-50;
display: block;
margin-top: 1rem;
&:hover, &:focus {
color: $maroon-50;
text-decoration: underline;
}
&.disabled {
color: $gray-200;
cursor: default;
pointer-events: none;
}
}
.quest-item-row {
display: flex;
margin-bottom: 0.25rem;
.quest-item-icon {
margin-right: 0.813rem;
width: 3.5rem;
height: 3.5rem;
display: flex;
align-items: center;
justify-content: center;
align-self: center;
}
.quest-item-info {
flex: 1;
text-align: left;
.label {
font-size: 0.75rem;
line-height: 1.33;
color: $gray-100;
&.quest-label {
font-weight: bold;
margin-bottom: 0.25rem;
display: block;
}
}
.item-progress-row {
margin-top: 0.5rem;
display: flex;
> * {
flex: 1;
}
}
.item-progress:not(.no-items) {
color: $green-10;
}
.item-progress-label {
text-align: right;
color: $gray-100;
}
}
}
.item-progress-pending {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
margin-left: -1rem;
margin-right: -1rem;
margin-bottom: -1rem !important;
background-color: $gray-200;
.pending-amount {
font-size: 12px;
font-weight: bold;
line-height: 1.33;
text-align: center;
color: $white;
}
}
.not-participating {
opacity: 0.5;
}
.rage-bar-row {
margin-top: 0.875rem;
}
</style>
<script>
@@ -353,16 +637,24 @@ import percent from '@/../../common/script/libs/percent';
import sidebarSection from '../sidebarSection';
import questIcon from '@/assets/svg/quest.svg';
import swordIcon from '@/assets/svg/sword.svg';
import rageIcon from '@/assets/svg/rage.svg';
import healthNoPaddingIcon from '@/assets/svg/health_no_padding.svg';
import questActionsMixin from '@/components/groups/questActions.mixin';
export default {
components: {
sidebarSection,
},
mixins: [questActionsMixin],
props: ['group'],
data () {
return {
icons: Object.freeze({
questIcon,
healthNoPaddingIcon,
swordIcon,
rageIcon,
}),
};
},
@@ -372,24 +664,27 @@ export default {
if (!this.group.quest || !this.group.quest.members) return false;
return Boolean(this.group.quest.members[this.user._id]);
},
userIsQuestLeader () {
if (!this.group.quest) return false;
return this.group.quest.leader === 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);
},
pendingHpPercent () {
return percent(this.user.party.quest.progress.up, this.questData.boss.hp);
},
questData () {
if (!this.group.quest) return {};
return quests.quests[this.group.quest.key];
},
canEditQuest () {
if (!this.group.quest) return false;
const isQuestLeader = this.group.quest.leader === this.user._id;
const isPartyLeader = this.group.leader._id === this.user._id;
return isQuestLeader || isPartyLeader;
return this.userIsQuestLeader || isPartyLeader;
},
isMemberOfPendingQuest () {
const userid = this.user._id;
@@ -416,25 +711,32 @@ export default {
return count;
},
hasPendingQuestItems () {
return Boolean(this.user.party.quest?.progress?.collectedItems);
},
},
methods: {
openStartQuestModal () {
this.$root.$emit('bv::show::modal', 'start-quest-modal');
openSelectQuestModal () {
this.$root.$emit('bv::show::modal', 'quest-detail-modal');
},
openQuestDetails () {
this.$root.$emit('bv::show::modal', 'quest-details');
this.$root.$emit('bv::show::modal', 'quest-detail-modal', {
key: this.group.quest.key,
from: 'sidebar',
});
},
openParticipantList () {
if (this.onPendingQuest) {
this.$root.$emit('bv::show::modal', 'invitation-list');
} else {
this.$root.$emit('bv::show::modal', 'participant-list');
},
async questAbort () {
if (!window.confirm(this.$t('sureAbort'))) return; // eslint-disable-line no-alert
if (!window.confirm(this.$t('doubleSureAbort'))) return; // eslint-disable-line no-alert
const quest = await this.$store.dispatch('quests:sendAction', { groupId: this.group._id, action: 'quests/abort' });
this.group.quest = quest;
}
},
async questLeave () {
if (!window.confirm(this.$t('sureLeave'))) return; // eslint-disable-line no-alert
if (!window.confirm(this.$t(this.group.quest.active ? 'sureLeave' : 'sureLeaveInactive'))) {
return;
}
const quest = await this.$store.dispatch('quests:sendAction', { groupId: this.group._id, action: 'quests/leave' });
this.group.quest = quest;
},
@@ -447,6 +749,9 @@ export default {
const quest = await this.$store.dispatch('quests:sendAction', { groupId: partyId, action: 'quests/reject' });
this.user.party.quest = quest;
},
startQuest () {
this.questActionsConfirmQuest();
},
},
};
</script>

View File

@@ -0,0 +1,240 @@
<template>
<div class="sidebar px-4">
<div>
<div class="buttons-wrapper">
<div class="button-container button-with-menu-row">
<button
v-if="!isMember"
class="btn btn-success btn-success"
@click="$emit('join')"
>
<span v-once>{{ $t(isParty ? 'joinParty' : 'joinGuild') }}</span>
</button>
<button
v-if="isMember"
class="btn btn-primary inline"
@click="$emit('showInviteModal')"
>
<span v-once>{{ $t(isParty ? 'inviteToParty' : 'inviteToGuild') }}</span>
</button>
<b-dropdown
right="right"
toggle-class="with-icon"
class="ml-2"
:no-caret="true"
>
<template v-slot:button-content>
<span
class="svg-icon inline menuIcon"
v-html="icons.menuIcon"
v-once
>
</span>
</template>
<b-dropdown-item
v-if="isLeader && !group.purchased.active && group.privacy === 'private'"
class="selectListItem custom-hover--upgrade"
@click="$emit('upgradeGroup')"
>
<span class="with-icon">
<span
class="svg-icon icon-16 color"
v-html="icons.sparklesIcon"
v-once
></span>
<span v-once>
{{ $t('upgradeToGroup') }}
</span>
</span>
</b-dropdown-item>
<b-dropdown-item
v-if="!isMember"
class="selectListItem"
@click="$emit('showInviteModal')"
>
<span class="with-icon">
<span
class="svg-icon icon-16 color"
v-html="icons.usersIcon"
v-once
></span>
<span v-once>
{{ $t(isParty ? 'inviteToParty' : 'inviteToGuild') }}
</span>
</span>
</b-dropdown-item>
<b-dropdown-item
class="selectListItem"
@click="$emit('messageLeader')"
>
<span class="with-icon">
<span
class="svg-icon icon-16 color"
v-html="icons.messageIcon"
v-once
></span>
<span v-once>
{{ $t(isParty ? 'messagePartyLeader' : 'messageGuildLeader') }}
</span>
</span>
</b-dropdown-item>
<b-dropdown-item
v-if="isLeader || isAdmin"
class="selectListItem"
@click="$emit('updateGuild')"
>
<span class="with-icon">
<span
class="svg-icon icon-16 color"
v-html="icons.editIcon"
v-once
></span>
<span v-once>
{{ isParty ? $t('editParty') : $t('editGuild') }}
</span>
</span>
</b-dropdown-item>
<b-dropdown-item
v-if="isMember"
class="selectListItem custom-hover--leave"
@click="$emit('leave')"
>
<span class="with-icon">
<span
class="svg-icon icon-16 color"
v-html="icons.leaveIcon"
v-once
></span>
<span v-once>
{{ isParty ? $t('leaveParty') : $t('leaveGuild') }}
</span>
</span>
</b-dropdown-item>
</b-dropdown>
</div>
</div>
</div>
<div>
<quest-sidebar-section
v-if="isParty"
:group="group"
/>
<sidebar-section
v-if="!isParty"
:title="$t('guildSummary')"
>
<p v-markdown="group.summary"></p>
</sidebar-section>
<sidebar-section :title="$t('groupDescription')">
<p v-markdown="group.description"></p>
</sidebar-section>
<sidebar-section
:title="$t('challenges')"
>
<group-challenges :group="group" />
</sidebar-section>
</div>
</div>
</template>
<script>
import groupChallenges from '@/components/challenges/groupChallenges';
import questSidebarSection from '@/components/groups/questSidebarSection';
import sidebarSection from '@/components/sidebarSection';
import markdownDirective from '@/directives/markdown';
import menuIcon from '@/assets/svg/menu.svg';
import sparklesIcon from '@/assets/svg/sparklesIcon.svg';
import leaveIcon from '@/assets/svg/leave.svg';
import editIcon from '@/assets/svg/edit.svg';
import messageIcon from '@/assets/svg/message.svg';
import usersIcon from '@/assets/svg/users.svg';
export default {
components: {
groupChallenges,
questSidebarSection,
sidebarSection,
},
directives: {
markdown: markdownDirective,
},
props: ['isParty', 'isLeader', 'isAdmin', 'isMember', 'searchId', 'group'],
data () {
return {
icons: Object.freeze({
menuIcon,
sparklesIcon,
leaveIcon,
editIcon,
messageIcon,
usersIcon,
}),
};
},
computed: {
isGroup () {
return Boolean(this.group.purchased?.plan?.customerId);
},
},
};
</script>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@media (min-width: 1300px) {
.sidebar {
max-width: 430px !important;
}
}
.sidebar {
background-color: $gray-600;
padding-bottom: 2em;
padding-top: 1.5rem;
}
.button-container {
margin-bottom: 1.5em;
button {
width: 100%;
}
}
.button-with-menu-row {
display: flex;
}
.menuIcon {
width: 4px;
height: 1rem;
object-fit: contain;
}
.dropdown-link {
text-decoration: none;
}
.divider {
width: calc(100% + 3rem);
height: 0.063rem;
margin-top: 1.5rem;
margin-right: -1.5rem;
margin-left: -1.5rem;
margin-bottom: 0.688rem;
background-color: $gray-500;
}
.custom-hover--leave {
--hover-color: #{$maroon-50};
--hover-background: #ffb6b83F;
}
.custom-hover--upgrade {
--hover-color: #{$green-10};
--hover-background: #77f4c73F;
}
</style>

View File

@@ -1,242 +0,0 @@
<template>
<b-modal
id="start-quest-modal"
title="Empty"
size="md"
:hide-footer="true"
:hide-header="true"
>
<div class="left-panel content">
<h3 class="text-center">
Quests
</h3>
<div class="row">
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
<div
v-for="(value, key) in user.items.quests"
v-if="value > 0"
:key="key"
class="col-4 quest-col"
:class="{selected: key === selectedQuest}"
@click="selectQuest({key})"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<div class="quest-wrapper">
<b-popover
:target="`inventory_quest_scroll_${key}`"
placement="top"
triggers="hover"
>
<h4 class="popover-content-title">
{{ quests.quests[key].text() }}
</h4>
<questInfo :quest="quests.quests[key]" />
</b-popover>
<div
:id="`inventory_quest_scroll_${key}`"
class="quest"
:class="`inventory_quest_scroll_${key}`"
></div>
</div>
</div>
</div>
<div class="row">
<div class="col-10 offset-1 text-center">
<span
v-once
class="description"
>{{ $t('noQuestToStart') }}</span>
</div>
</div>
</div>
<div v-if="questData">
<questDialogContent :item="questData" />
</div>
<div class="text-center">
<button
class="btn btn-primary"
:disabled="!Boolean(selectedQuest) || loading"
@click="questInit()"
>
{{ $t('inviteToPartyOrQuest') }}
</button>
</div>
<div class="text-center">
<p>{{ $t('inviteInformation') }}</p>
</div>
<div
v-if="questData"
class="side-panel"
>
<questDialogDrops :item="questData" />
</div>
</b-modal>
</template>
<style lang='scss' scoped>
@import '~@/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 '@/libs/store';
import * as Analytics from '@/libs/analytics';
import * as 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 {
components: {
questDialogDrops,
questDialogContent,
QuestInfo,
},
props: ['group'],
data () {
return {
loading: false,
selectedQuest: {},
icons: Object.freeze({
copy: copyIcon,
greyBadge: greyBadgeIcon,
qrCode: qrCodeIcon,
facebook: facebookIcon,
twitter: twitterIcon,
starIcon,
goldIcon,
difficultyStarIcon,
}),
shareUserIdShown: false,
quests,
};
},
computed: {
...mapState({ user: 'user.data' }),
questData () {
return quests.quests[this.selectedQuest];
},
},
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);
},
beforeDestroy () {
this.$root.$off('selectQuest', this.selectQuest);
},
methods: {
selectQuest (quest) {
this.selectedQuest = quest.key;
},
async questInit () {
this.loading = true;
Analytics.updateUser({
partyID: this.group._id,
partySize: this.group.memberCount,
});
const 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>

View File

@@ -192,6 +192,8 @@ import svgUnEquipIcon from '@/assets/svg/unequip.svg';
import Avatar from '@/components/avatar';
import attributesGrid from '@/components/inventory/equipment/attributesGrid.vue';
import closeIcon from '@/components/shared/closeIcon';
// TODO @common/ path alias
import { getClassName } from '../../../../../common/script/libs/getClassName';
export default {
components: {
@@ -274,10 +276,7 @@ export default {
};
},
getClassName (classType) {
if (classType === 'wizard') {
return this.$t('mage');
}
return this.$t(classType);
return this.$t(getClassName(classType));
},
},
};

View File

@@ -293,7 +293,7 @@
</div>
</div>
</div>
<startQuestModal :group="user.party" />
<questDetailModal :group="user.party" />
<cards-modal :card-options="cardOptions" />
</div>
</template>
@@ -343,7 +343,7 @@ import FilterSidebar from '@/components/ui/filterSidebar';
import cardsModal from './cards-modal';
import HatchedPetDialog from '../stable/hatchedPetDialog';
import startQuestModal from '../../groups/startQuestModal';
import questDetailModal from '../../groups/questDetailModal';
import QuestInfo from '../../shops/quests/questInfo.vue';
import { mapState } from '@/libs/store';
@@ -384,7 +384,7 @@ export default {
ItemRows,
HatchedPetDialog,
CountBadge,
startQuestModal,
questDetailModal,
cardsModal,
QuestInfo,
FilterSidebar,
@@ -632,9 +632,9 @@ export default {
this.$root.$emit('selectMembersModal::showItem', item);
}
} else if (groupKey === 'quests') {
this.$root.$emit('bv::show::modal', 'start-quest-modal');
this.$root.$emit('selectQuest', item);
this.$root.$emit('bv::show::modal', 'quest-detail-modal', {
key: item.key,
});
}
},

View File

@@ -0,0 +1,34 @@
/* eslint-disable import/no-extraneous-dependencies */
import { storiesOf } from '@storybook/vue';
import MemberDetails from './memberDetails.vue';
import MemberDetailsNew from './memberDetailsNew.vue';
import { userStyles } from '../../config/storybook/mock.data';
storiesOf('Member Details', module)
.add('party header (old)', () => ({
components: { MemberDetails },
template: `
<div style="position: absolute; margin: 20px">
<member-details :member="user"></member-details>
</div>
`,
data () {
return {
user: userStyles,
};
},
}))
.add('quest participants (new)', () => ({
components: { MemberDetailsNew },
template: `
<div style="position: absolute; margin: 20px">
<member-details-new :member="user"></member-details-new>
</div>
`,
data () {
return {
user: userStyles,
};
},
}));

View File

@@ -0,0 +1,330 @@
<template>
<div
class="member-details-new"
:class="{ condensed, expanded }"
@click="showMemberModal(member)"
>
<div>
<avatar
:member="member"
:hide-class-badge="true"
@click.native="$emit('click')"
@mouseover.native="$emit('onHover')"
@mouseout.native="$emit('onHover')"
/>
</div>
<div
class="member-stats"
>
<div class="d-flex align-items-center profile-first-row">
<class-badge
v-if="classBadgePosition === 'next-to-name'"
:member-class="member.stats.class"
/>
<div class="d-flex flex-column profile-name-character">
<h3 class="character-name mt-75">
<user-link
:user-id="member._id"
:name="member.profile.name"
:backer="member.backer"
:contributor="member.contributor"
:smallerStyle="true"
/>
<inline-class-badge
class="inline-class-badge"
v-if="member.stats"
:member-class="member.stats.class"
/>
</h3>
<div class="small-text character-level">
<span
v-if="member.auth && member.auth.local && member.auth.local.username"
class="mr-1"
>@{{ member.auth.local.username }}</span>
<span
v-if="member.auth && member.auth.local && member.auth.local.username"
class="mr-1"
></span>
<span>{{ characterLevel }}</span>
</div>
</div>
</div>
<stats-bar
class="mt-3 stats-bar"
:icon="icons.health"
:value="member.stats.hp"
:max-value="MAX_HEALTH"
:tooltip="$t('health')"
progress-class="bg-health-new"
:condensed="condensed"
:show-icon="false"
:show-numbers="false"
/>
<stats-bar
class="mt-75 stats-bar"
:icon="icons.experience"
:value="member.stats.exp"
:max-value="toNextLevel"
:tooltip="$t('experience')"
progress-class="bg-experience-new"
:condensed="condensed"
:show-icon="false"
:show-numbers="false"
/>
<stats-bar
class="mt-75 stats-bar"
v-if="hasClass"
:icon="icons.mana"
:value="member.stats.mp"
:max-value="maxMP"
:tooltip="$t('mana')"
progress-class="bg-mana-new"
:condensed="condensed"
:show-icon="false"
:show-numbers="false"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.stats-bar {
margin-left: 0;
height: 12px;
border-radius: 2px;
::v-deep {
.bg-health-new {
background: $red-50;
}
.bg-experience-new {
background: $yellow-100;
}
.bg-mana-new {
background: $blue-100;
}
.progress-bar {
border-radius: 0;
}
.progress {
width: 100%;
--progress-background: #{$gray-500};
}
}
}
.inline-class-badge {
margin-left: 10px;
display: inline-block;
}
.member-details-new {
white-space: nowrap;
transition: all 0.15s ease-out;
display: flex;
flex-direction: row;
}
.member-stats {
flex: 1;
padding-left: 1rem;
padding-right: 24px;
opacity: 1;
transition: width 0.15s ease-out;
}
.member-details-new.condensed:not(.expanded) .member-stats {
opacity: 0;
display: none;
}
.small-text {
color: $header-color;
}
.profile-name-character {
}
.character-name {
margin-bottom: 1px;
color: $white;
height: 24px;
display: flex;
flex-direction: row;
align-items: center;
::v-deep {
.user-link {
display: flex;
align-items: center;
}
}
}
.character-level {
font-style: normal;
color: $gray-100;
}
.is-buffed {
width: 20px;
height: 20px;
background: $header-dark-background;
display: inline-block;
margin-left: 16px;
vertical-align: middle;
padding-top: 4px;
.svg-icon {
display: block;
width: 10px;
height: 12px;
margin: 0 auto;
}
}
.profile-first-row {
margin-bottom: .5em
}
// Condensed version
.member-details-new.condensed.expanded {
background: $header-dark-background;
box-shadow: 0 0 0px 8px $header-dark-background;
position: relative;
z-index: 8;
.is-buffed {
background-color: $purple-50;
}
.member-stats {
background: $header-dark-background;
position: absolute;
right: 100%;
height: calc(100% + 18px);
margin-top: -10px;
margin-right: 1px;
padding-top: 9px;
padding-bottom: 24px;
padding-right: 16px;
padding-bottom: 14px;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
z-index: 9;
}
}
</style>
<script>
import Avatar from './avatar';
import ClassBadge from './members/classBadge';
import { mapState } from '@/libs/store';
import StatsBar from './ui/statsbar';
import userLink from './userLink';
import { toNextLevel } from '@/../../common/script/statHelpers';
import statsComputed from '@/../../common/script/libs/statsComputed';
import percent from '@/../../common/script/libs/percent';
import buffIcon from '@/assets/svg/buff.svg';
import healthIcon from '@/assets/svg/health.svg';
import experienceIcon from '@/assets/svg/experience.svg';
import manaIcon from '@/assets/svg/mana.svg';
import InlineClassBadge from './members/inlineClassBadge';
import { getClassName } from '../../../common/script/libs/getClassName';
export default {
components: {
InlineClassBadge,
Avatar,
ClassBadge,
StatsBar,
userLink,
},
filters: {
statFloor (value) {
if (value < 1 && value > 0) {
return Math.ceil(value * 10) / 10;
}
return Math.floor(value);
},
},
props: {
member: {
type: Object,
required: true,
},
condensed: {
type: Boolean,
default: false,
},
expanded: {
type: Boolean,
default: false,
},
classBadgePosition: {
type: String,
default: 'under-avatar', // next-to-name or hidden
},
isHeader: {
type: Boolean,
default: false,
},
disableNameStyling: {
type: Boolean,
default: false,
},
},
data () {
return {
icons: Object.freeze({
buff: buffIcon,
health: healthIcon,
experience: experienceIcon,
mana: manaIcon,
}),
};
},
computed: {
...mapState({
MAX_HEALTH: 'constants.MAX_HEALTH',
}),
maxMP () {
return statsComputed(this.member).maxMP;
},
toNextLevel () { // Exp to next level
return toNextLevel(this.member.stats.lvl);
},
characterLevel () {
return `${this.$t('level')} ${this.member.stats.lvl} ${
this.member.stats.class ? this.getClassName(this.member.stats.class) : ''
}`;
},
isBuffed () {
return this.$store.getters['members:isBuffed'](this.member);
},
hasClass () {
return this.$store.getters['members:hasClass'](this.member);
},
},
methods: {
percent,
showMemberModal (member) {
this.$router.push({ name: 'userProfile', params: { userId: member._id } });
},
getClassName (classType) {
return this.$t(getClassName(classType));
},
},
};
</script>

View File

@@ -1,5 +1,6 @@
<template>
<div class="class-badge d-flex justify-content-center">
<div class="class-badge d-flex justify-content-center"
:style="{'--badge-size': badgeSize}">
<div
class="align-self-center svg-icon"
:aria-label="$t(memberClass)"
@@ -12,10 +13,10 @@
@import '~@/assets/scss/colors.scss';
.class-badge {
$badge-size: 32px;
--badge-size: 32px;
width: $badge-size;
height: $badge-size;
width: var(--badge-size);
height: var(--badge-size);
background: $white;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
border-radius: 100px;
@@ -23,13 +24,13 @@
&.under-avatar {
position: absolute;
left: calc(50% - (16px));
bottom: -($badge-size / 2);
bottom: -(var(--badge-size) / 2);
z-index: 1;
}
.svg-icon {
width: 19px;
height: 19px;
width: 20px;
height: 20px;
}
}
</style>
@@ -46,6 +47,10 @@ export default {
type: String,
required: true,
},
badgeSize: {
type: Number,
default: 32,
},
},
data () {
return {

View File

@@ -0,0 +1,44 @@
<template>
<span
class="svg-icon"
:aria-label="$t(memberClass)"
v-html="icons[memberClass]"
></span>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.svg-icon {
--badge-size: 16px;
width: var(--badge-size);
height: var(--badge-size);
}
</style>
<script>
import warriorIcon from '@/assets/svg/warrior.svg';
import rogueIcon from '@/assets/svg/rogue.svg';
import healerIcon from '@/assets/svg/healer.svg';
import wizardIcon from '@/assets/svg/wizard.svg';
export default {
props: {
memberClass: {
type: String,
required: true,
},
},
data () {
return {
icons: Object.freeze({
warrior: warriorIcon,
rogue: rogueIcon,
healer: healerIcon,
wizard: wizardIcon,
}),
};
},
};
</script>

View File

@@ -269,12 +269,12 @@ export default {
result = null;
}
}
if (!result || !result.data || !result.data.data) {
if (!result) {
this.userNotFound = true;
return;
}
this.userNotFound = false;
this.foundUser = result.data.data;
this.foundUser = result;
}, 500),
selectUser () {
this.$root.$emit('habitica::send-gems', this.foundUser);

View File

@@ -1,20 +1,20 @@
<template>
<div
class="toggle ml-auto"
class="toggle ml-auto section-button"
role="button"
:aria-expanded="visible"
tabindex="0"
@keyup.enter="$emit('click')"
@click="$emit('click')"
@keyup.enter="emitClick"
@click="emitClick"
>
<span
v-if="visible"
class="svg-icon"
class="svg-icon icon-16"
v-html="icons.upIcon"
></span>
<span
v-else
class="svg-icon"
class="svg-icon icon-16 down-icon color-stroke"
v-html="icons.downIcon"
></span>
</div>
@@ -28,13 +28,22 @@
background: transparent;
cursor: pointer;
&:focus {
// Fix keyboard inaccessible https://github.com/HabitRPG/habitica/pull/12656
outline: none;
border: $purple-400 solid 1px;
}
}
.svg-icon {
width: 16px;
display: flex;
::v-deep svg {
height: 100%;
}
}
.down-icon {
color: $gray-300;
}
</style>
@@ -56,5 +65,14 @@ export default {
},
};
},
methods: {
emitClick ($event) {
if ($event.stopPropagation) {
$event.stopPropagation();
}
this.$emit('click');
},
},
};
</script>

View File

@@ -0,0 +1,221 @@
<template>
<div>
<div
:id="itemId"
class="item-wrapper"
tabindex="0"
@click="click()"
@keypress.enter="click()"
>
<div
class="item"
:class="getItemClasses()"
>
<slot
name="badges"
:item="item"
:emptyItem="emptyItem"
></slot>
<div class="image">
<div
v-once
:class="item.class"
></div>
<slot
name="itemImage"
:item="item"
></slot>
</div>
<div
class="d-flex label-holder align-items-center justify-content-center"
:class="labelClass"
>
<slot
name="label"
:item="item"
></slot>
</div>
</div>
</div>
<b-popover
v-if="showPopover"
:target="itemId"
triggers="hover focus"
:placement="popoverPosition"
>
<slot
name="popoverContent"
:item="item"
>
</slot>
</b-popover>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.item-wrapper {
z-index: 10;
}
.item {
height: 7.5rem;
width: 94px;
border-radius: 4px;
background-color: $white;
box-shadow: 0 1px 3px 0 rgba($black, 0.12), 0 1px 2px 0 rgba($black, 0.24);
cursor: initial;
display: flex;
flex-direction: column;
&:hover {
border-color: transparent;
}
&.locked .price {
opacity: 0.5;
}
}
.image {
margin: 12px 13px;
flex: 1;
align-items: center;
justify-content: center;
display: flex;
}
.label-holder {
height: 28px;
font-weight: bold;
line-height: 1.33;
text-align: center;
background-color: $gray-700;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.label-holder.purple {
margin: -1px;
color: $purple-300;
background-color: rgba($purple-400, .1);
}
.label-holder.yellow {
color: $yellow-5;
}
span.svg-icon.inline.lock {
height: 12px;
width: 10px;
position: absolute;
right: 8px;
top: 8px;
margin-top: 0;
color: $gray-200;
}
span.badge.badge-round.badge-item.badge-clock {
height: 24px;
width: 24px;
background-color: $purple-300;
position: absolute;
left: -8px;
top: -12px;
margin-top: 0;
padding: 4px;
}
span.svg-icon.inline.clock {
height: 16px;
width: 16px;
}
.suggestedDot {
width: 6px;
height: 6px;
background-color: $purple-400;
border-radius: 4px;
position: absolute;
right: 8px;
top: 8px;
margin-top: 0;
}
.icon-48 {
width: 48px;
height: 48px;
}
.w-0 {
width: 0rem;
}
</style>
<script>
import { v4 as uuid } from 'uuid';
export default {
components: {
},
props: {
item: {
type: Object,
},
price: {
type: Number,
default: -1,
},
emptyItem: {
type: Boolean,
default: false,
},
highlightBorder: {
type: Boolean,
default: false,
},
popoverPosition: {
type: String,
default: 'bottom',
},
showPopover: {
type: Boolean,
default: true,
},
showEventBadge: {
type: Boolean,
default: true,
},
owned: {
type: Boolean,
default: false,
},
labelClass: {
type: String,
},
},
data () {
return Object.freeze({
itemId: uuid(),
});
},
computed: {
},
methods: {
click () {
this.$emit('click', {});
},
getItemClasses () {
return {
'item-empty': this.emptyItem,
'highlight-border': this.highlightBorder,
suggested: this.item.isSuggested,
locked: this.item.locked,
};
},
},
};
</script>

View File

@@ -89,6 +89,7 @@ import svgRogue from '@/assets/svg/rogue.svg';
import svgHealer from '@/assets/svg/healer.svg';
import pinUtils from '../../../mixins/pinUtils';
import { getClassName } from '../../../../../common/script/libs/getClassName';
const sortGearTypes = [
'sortByType', 'sortByPrice', 'sortByCon',
@@ -159,10 +160,7 @@ export default {
},
methods: {
getClassName (classType) {
if (classType === 'wizard') {
return this.$t('mage');
}
return this.$t(classType);
return this.$t(getClassName(classType));
},
gearSelected (item) {
this.$root.$emit('buyModal::showItem', item);

View File

@@ -13,14 +13,12 @@
:pinned="isPinned"
/>
</span>
<div class="close">
<span
class="svg-icon inline icon-10"
aria-hidden="true"
@click="hideDialog()"
v-html="icons.close"
></span>
<div class="dialog-close">
<close-icon @click="hideDialog()" />
</div>
<h2 class="text-center textCondensed">
{{$t('questDetailsTitle') }}
</h2>
<div
v-if="item != null"
class="content"
@@ -30,6 +28,7 @@
:item="item"
:abbreviated="true"
/>
<quest-rewards :quest="item" />
<div
v-if="!item.locked"
class="purchase-amount"
@@ -81,12 +80,6 @@
</button>
</div>
</div>
<div
v-if="item.drop"
class="right-sidebar"
>
<questDialogDrops :item="item" />
</div>
<countdown-banner
v-if="item.event"
:endDate="endDate"
@@ -113,9 +106,14 @@
#buy-quest-modal {
@include centeredModal();
h2 {
color: $purple-300;
margin-top: 1rem;
}
.modal-dialog {
margin-top: 8%;
width: 448px;
width: 448px !important;
}
.content {
@@ -134,6 +132,10 @@
padding-bottom: 0px;
}
.questInfo {
margin: 0 auto 10px auto;
}
.right-sidebar {
position: absolute;
right: -350px;
@@ -198,6 +200,11 @@
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
display: block;
padding: 1rem 1.5rem;
&> * {
margin: 0;
}
}
.notEnough {
@@ -235,6 +242,18 @@
}
}
}
@media only screen and (max-width: 1000px) {
.modal-dialog {
max-width: 80%;
width: 80% !important;
.modal-body {
flex-direction: column;
display: flex;
}
}
}
}
</style>
@@ -257,14 +276,16 @@ import numberInvalid from '@/mixins/numberInvalid';
import PinBadge from '@/components/ui/pinBadge';
import CountdownBanner from '../countdownBanner';
import questDialogDrops from './questDialogDrops';
import questDialogContent from './questDialogContent';
import QuestRewards from './questRewards';
import CloseIcon from '../../shared/closeIcon';
export default {
components: {
CloseIcon,
QuestRewards,
BalanceInfo,
PinBadge,
questDialogDrops,
questDialogContent,
CountdownBanner,
},

View File

@@ -219,39 +219,7 @@
@click="selectItem(item)"
>
<span slot="popoverContent">
<div class="questPopover">
<div></div>
<h4
v-if="item.locked"
class="popover-content-title"
>{{ `${$t('lockedItem')}` }}</h4>
<h4
v-else
class="popover-content-title"
>{{ item.text }}</h4>
<div
v-if="item.locked && item.key === 'lostMasterclasser1'"
class="popover-content-text"
>{{ `${$t('questUnlockLostMasterclasser')}` }}</div>
<div
v-if="item.locked && item.unlockCondition
&& item.unlockCondition.incentiveThreshold"
class="popover-content-text"
>{{ `${$t('loginIncentiveQuest', {
count: item.unlockCondition.incentiveThreshold})}` }}</div>
<div
v-if="item.locked && item.previous && isBuyingDependentOnPrevious(item)"
class="popover-content-text"
>{{ `${$t('unlockByQuesting', {title: item.previous})}` }}</div>
<div
v-if="item.lvl > user.stats.lvl"
class="popover-content-text"
>{{ `${$t('mustLvlQuest', {level: item.lvl})}` }}</div>
<questInfo
v-if="!item.locked"
:quest="item"
/>
</div>
<quest-popover :item="item" />
</span>
<template
slot="itemBadge"
@@ -456,10 +424,12 @@ import isPinned from '@/../../common/script/libs/isPinned';
import FilterSidebar from '@/components/ui/filterSidebar';
import FilterGroup from '@/components/ui/filterGroup';
import SelectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
import QuestPopover from './questPopover';
export default {
components: {
QuestPopover,
SelectTranslatedArray,
FilterGroup,
FilterSidebar,
@@ -597,11 +567,6 @@ export default {
this.$root.$emit('bv::show::modal', 'buy-quest-modal');
},
isBuyingDependentOnPrevious (item) {
const questsNotDependentToPrevious = ['moon2', 'moon3'];
if (item.key in questsNotDependentToPrevious) return false;
return true;
},
},
};
</script>

View File

@@ -0,0 +1,78 @@
/* eslint-disable import/no-extraneous-dependencies */
import { storiesOf } from '@storybook/vue';
import { withKnobs } from '@storybook/addon-knobs';
import { quests } from '@/../../common/script/content/quests';
import questRewards from './questRewards';
import itemWithLabel from '../itemWithLabel';
import questPopover from './questPopover';
const stories = storiesOf('Quests/Sub Components', module);
stories.addDecorator(withKnobs);
stories
.add('questRewads', () => ({
components: { questRewards },
data () {
return {
quest: quests.goldenknight2,
questWithDrop: quests.moon1,
};
},
template: `
<div>
<quest-rewards :quest="quest"></quest-rewards>
<quest-rewards :quest="questWithDrop"></quest-rewards>
</div>
`,
}))
.add('itemWithLabel', () => ({
components: { itemWithLabel },
data () {
return {
};
},
template: `
<div>
<item-with-label :item="{}">
<div slot="itemContent">
</div>
<div slot="itemImage">
Image
</div>
<div slot="label">
Label
</div>
</item-with-label>
<item-with-label :item="{}" label-class="purple">
<div slot="itemContent">
</div>
<div slot="itemImage">
Image
</div>
<div slot="label">
Label
</div>
</item-with-label>
</div>
`,
}))
.add('questPopover', () => ({
components: { questPopover },
data () {
return {
quest: quests.goldenknight2,
quest2: quests.moon1,
};
},
template: `
<div>
<quest-popover :item="quest"></quest-popover>
<quest-popover :item="quest2"></quest-popover>
</div>
`,
}));

View File

@@ -0,0 +1,56 @@
import groupBy from 'lodash/groupBy';
import { mapState } from '../../../libs/store';
export const QuestHelperMixin = {
computed: {
...mapState({
content: 'content',
}),
},
methods: {
getDropIcon (drop) {
switch (drop.type) {
case 'hatchingPotions':
return `Pet_HatchingPotion_${drop.key}`;
case 'food':
return `Pet_Food_${drop.key}`;
case 'eggs':
return `Pet_Egg_${drop.key}`;
case 'quests':
return `inventory_quest_scroll_${drop.key}`;
default:
return `shop_${drop.key}`;
}
},
getDropName (drop) {
return typeof drop.text === 'function' ? drop.text() : drop.text;
},
getDropsList (drops, ownerOnly) {
if (!drops) return [];
const dropList = drops.filter(drop => {
if (ownerOnly) {
return drop.onlyOwner;
}
return !drop.onlyOwner;
}).map(item => {
if (item.type === 'gear') {
const contentItem = this.content.gear.flat[item.key];
return contentItem;
}
return {
...item,
text: item.text(),
};
});
return Object.entries(groupBy(dropList, e => `${e.type}_${e.key}`))
.map(([, entries]) => ({
...entries[0],
amount: entries.length,
}));
},
},
};

View File

@@ -4,9 +4,18 @@
class="quest-image"
:class="'quest_' + item.key"
></div>
<h4 class="title">
<h3 class="text-center">
{{ itemText }}
</h4>
</h3>
<div
v-if="group && leader"
class="leader-label"
>
<span v-once>
{{ $t('questOwner') }}:
</span>
<user-label :user="leader" />
</div>
<div
class="text"
v-html="itemNotes"
@@ -23,6 +32,11 @@
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
h3 {
color: $gray-10;
margin-bottom: 0.25rem;
}
.quest-image {
margin: 0 auto;
margin-bottom: 1em;
@@ -30,29 +44,67 @@
}
.text {
margin-bottom: 8px;
overflow-y: scroll;
margin-bottom: 1rem;
overflow-y: auto;
text-overflow: ellipsis;
}
.leader-label {
font-size: 14px;
font-weight: bold;
line-height: 1.71;
color: $gray-50;
text-align: center;
margin-bottom: 0.5rem;
::v-deep .user-label {
font-size: 14px;
}
}
.questInfo {
width: 70%;
width: 160px;
margin: 0 auto;
margin-bottom: 10px;
display: flex;
justify-content: center;
}
</style>
<script>
import QuestInfo from './questInfo.vue';
import UserLabel from '../../userLabel';
export default {
components: {
UserLabel,
QuestInfo,
},
data () {
return {
leader: null,
};
},
props: {
item: {
type: Object,
},
group: {
type: Object,
},
},
async created () {
if (this.group && this.group.quest && this.group.quest.active) {
try {
const fetchMemberResult = await this.$store.dispatch('members:fetchMember', {
memberId: this.group.quest.leader,
});
this.leader = fetchMemberResult;
} catch {
this.leader = null;
}
}
},
computed: {
itemText () {

View File

@@ -1,159 +0,0 @@
<template>
<div class="questRewards">
<h3
v-once
class="text-center"
>
{{ $t('rewards') }}
</h3>
<div class="reward-item">
<span
class="svg-icon inline icon"
v-html="icons.experience"
></span>
<span class="reward-text">{{ $t('amountExperience', { amount: item.drop.exp }) }}</span>
</div>
<div
v-if="item.drop.gp != 0"
class="reward-item"
>
<span
class="svg-icon inline icon"
v-html="icons.gold"
></span>
<span class="reward-text">{{ $t('amountGold', { amount: item.drop.gp }) }}</span>
</div>
<div
v-for="drop in getDropsList(item.drop.items, false)"
:key="drop.key"
class="reward-item"
>
<span class="icon">
<div :class="getDropIcon(drop)"></div>
</span>
<span class="reward-text">{{ getDropName(drop) }}</span>
</div>
<div
v-if="item.drop.unlock"
class="reward-item text-center"
>
<span class="reward-text">{{ item.drop.unlock() }}</span>
</div>
<h3
v-if="getDropsList(item.drop.items, true).length > 0"
class="text-center"
>
{{ $t('questOwnerRewards') }}
</h3>
<div
v-for="drop in getDropsList(item.drop.items, true)"
:key="drop.key"
class="reward-item"
>
<span class="icon">
<div :class="getDropIcon(drop)"></div>
</span>
<span class="reward-text">{{ getDropName(drop) }}</span>
</div>
</div>
</template>
<style lang="scss" scoped>
.questRewards {
overflow-y: auto;
width: 364px;
z-index: -1;
height: 100%;
h3 {
margin-top: 24px;
margin-bottom: 16px;
margin-left: auto;
margin-right: auto;
}
.reward-item {
width: 306px;
height: 84px;
border-radius: 2px;
background-color: #ffffff;
margin: 0 auto;
margin-bottom: 1em;
padding: 1em;
display: flex;
flex-direction: row;
align-items: center;
.icon:not(.svg-icon) {
height: 68px;
width: 68px;
}
.svg-icon {
margin: 15px;
height: 38px;
width: 38px;
}
.reward-text {
font-weight: bold;
}
}
}
</style>
<script>
import svgGold from '@/assets/svg/gold.svg';
import svgExperience from '@/assets/svg/experience.svg';
export default {
components: {
},
mixins: [],
props: {
item: {
type: Object,
},
},
data () {
return {
icons: Object.freeze({
gold: svgGold,
experience: svgExperience,
}),
};
},
methods: {
getDropIcon (drop) {
switch (drop.type) {
case 'gear':
return `shop_${drop.key}`;
case 'hatchingPotions':
return `Pet_HatchingPotion_${drop.key}`;
case 'food':
return `Pet_Food_${drop.key}`;
case 'eggs':
return `Pet_Egg_${drop.key}`;
case 'quests':
return `inventory_quest_scroll_${drop.key}`;
default:
return '';
}
},
getDropName (drop) {
return drop.text();
},
getDropsList (drops, ownerOnly) {
if (!drops) return [];
return drops.filter(drop => {
if (ownerOnly) {
return drop.onlyOwner;
}
return !drop.onlyOwner;
});
},
},
};
</script>

View File

@@ -1,16 +1,18 @@
<template>
<div>
<div
class="d-flex flex-column justify-content-center"
class="row"
>
<div
v-if="quest.collect"
class="table-row m-auto"
class="table-row"
>
<dt>{{ $t('collect') + ':' }}</dt>
<dd>
<div
v-for="(collect, key) of quest.collect"
:key="key"
class="quest-item"
>
<span>{{ collect.count }} {{ getCollectText(collect) }}</span>
</div>
@@ -18,12 +20,12 @@
</div>
<div
v-if="quest.boss"
class="table-row m-auto"
class="table-row"
>
<dt>{{ $t('bossHP') + ':' }}</dt>
<dd>{{ quest.boss.hp }}</dd>
</div>
<div class="table-row m-auto">
<div class="table-row">
<dt>{{ $t('difficulty') + ':' }}</dt>
<dd>
<div
@@ -34,6 +36,7 @@
></div>
</dd>
</div>
</div>
<div
v-if="quest.event && !abbreviated"
class="m-auto"
@@ -49,37 +52,51 @@
.row {
display: table;
margin: 0;
width: 100%;
}
.table-row {
display: table-row;
margin-bottom: 4px;
font-size: 14px;
height: 1.5rem;
&:last-of-type {
dd {
padding-bottom: 0;
}
}
}
dd {
height: 24px;
padding-left: 1em;
padding-top: 3px;
padding-bottom: 3px;
text-align: right;
padding-bottom: 0.5rem;
.quest-item {
white-space: nowrap;
}
}
dt, dd {
display: table-cell;
vertical-align: middle;
}
dt, dd, dd > * {
text-align: left;
vertical-align: top;
height: 16px;
max-height: 16px;
}
dt {
font-size: 1.3em;
line-height: 1.2;
font-weight: bold;
font-stretch: normal;
font-style: normal;
line-height: 1.33;
letter-spacing: normal;
text-align: left;
color: $gray-50;
}
.svg-icon {
margin-right: 4px;
margin-left: 4px;
}
.small-version {
@@ -93,10 +110,11 @@ dt {
</style>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
.questPopover {
dt {
color: inherit;
font-size: 1em;
white-space: nowrap;
}
}
@@ -113,7 +131,7 @@ dt {
fill: #ffb445;
}
.star-empty {
fill: #686274;
fill: $gray-400;
}
</style>

View File

@@ -0,0 +1,70 @@
<template>
<div class="questPopover">
<h4
v-if="item.locked"
class="popover-content-title"
>
{{ $t('lockedItem') }}
</h4>
<h4
v-else
class="popover-content-title"
>
{{ item.text }}
</h4>
<div
v-if="item.locked && item.key === 'lostMasterclasser1'"
class="popover-content-text"
>
{{ $t('questUnlockLostMasterclasser') }}
</div>
<div
v-if="item.locked && item.unlockCondition
&& item.unlockCondition.incentiveThreshold"
class="popover-content-text"
>
{{ $t('loginIncentiveQuest', {
count: item.unlockCondition.incentiveThreshold}) }}
</div>
<div
v-if="item.locked && item.previous && isBuyingDependentOnPrevious(item)"
class="popover-content-text"
>
{{ $t('unlockByQuesting', {title: item.previous}) }}
</div>
<div
v-if="item.lvl > user.stats.lvl"
class="popover-content-text"
>
{{ $t('mustLvlQuest', {level: item.lvl}) }}
</div>
<questInfo
v-if="!item.locked"
:quest="item"
/>
</div>
</template>
<script>
import QuestInfo from './questInfo.vue';
import { mapState } from '@/libs/store';
export default {
components: {
QuestInfo,
},
props: ['item'],
computed: {
...mapState({
user: 'user.data',
}),
},
methods: {
isBuyingDependentOnPrevious (item) {
const questsNotDependentToPrevious = ['moon2', 'moon3'];
if (item.key in questsNotDependentToPrevious) return false;
return true;
},
},
};
</script>

View File

@@ -0,0 +1,213 @@
<template>
<div class="quest-rewards">
<div
class="header d-flex align-items-center"
@click="toggle"
>
<span class="d-flex justify-content-center">
<div
v-once
class="your-rewards d-flex align-items-center"
>
<span
class="sparkles"
v-html="icons.sparkles"
></span>
<span class="rewards-title">{{ $t('rewards') }}</span>
<span
class="sparkles mirror"
v-html="icons.sparkles"
></span>
</div>
</span>
<SectionButton
:visible="opened"
@click="toggle"
/>
</div>
<div
v-if="opened"
class="content ml-3 mr-3"
>
<item-with-label
v-for="drop in getDropsList(quest.drop.items, true)"
:key="drop.key"
:item="{}"
label-class="purple"
>
<div slot="itemImage">
<div :class="getDropIcon(drop)"></div>
</div>
<div slot="popoverContent">
<quest-popover :item="drop" />
</div>
<div slot="label">
{{ $t('ownerOnly') }}
</div>
</item-with-label>
<item-with-label
:item="{}"
label-class="yellow"
>
<div slot="itemImage">
<div
class="icon-48"
v-html="icons.expIcon"
></div>
</div>
<div slot="label">
{{ $t('amountExp', { amount: quest.drop.exp }) }}
</div>
</item-with-label>
<item-with-label
:item="{}"
label-class="yellow"
>
<div slot="itemImage">
<div
class="icon-48"
v-html="icons.goldIcon"
></div>
</div>
<div slot="label">
{{ $t('amountGold', { amount: quest.drop.gp }) }}
</div>
</item-with-label>
<item-with-label
v-for="drop in getDropsList(quest.drop.items, false)"
:key="drop.key"
:item="{}"
>
<countBadge
slot="badges"
:show="drop.amount !== 1"
:count="drop.amount"
/>
<div slot="itemImage">
<div :class="getDropIcon(drop)"></div>
</div>
<div
v-if="drop.klass"
slot="popoverContent"
>
<equipmentAttributesPopover
:item="drop"
/>
</div>
<div slot="label">
{{ $t('newItem') }}
</div>
</item-with-label>
</div>
</div>
</template>
<script>
import sparkles from '@/assets/svg/sparkles-left.svg';
import expIcon from '@/assets/svg/experience.svg';
import goldIcon from '@/assets/svg/gold.svg';
import SectionButton from '../../sectionButton';
import ItemWithLabel from '../itemWithLabel';
import { QuestHelperMixin } from './quest-helper.mixin';
import EquipmentAttributesPopover from '@/components/inventory/equipment/attributesPopover';
import QuestPopover from './questPopover';
import CountBadge from '../../ui/countBadge';
export default {
components: {
CountBadge,
QuestPopover,
ItemWithLabel,
SectionButton,
EquipmentAttributesPopover,
},
mixins: [QuestHelperMixin],
props: ['quest'],
data () {
return {
opened: true,
icons: Object.freeze({
sparkles,
expIcon,
goldIcon,
}),
};
},
computed: {
droppedItem () {
const item = this.quest.drop.items[0];
if (item) {
return item;
}
return null;
},
},
methods: {
toggle () {
this.opened = !this.opened;
},
},
};
</script>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
.quest-rewards {
margin-left: -1rem;
margin-right: -1rem;
background-color: $gray-700;
}
.header {
height: 3.5rem;
position: relative;
::v-deep {
.section-button {
position: absolute;
right: 1.5rem;
}
}
span {
flex: 1;
}
.mirror {
transform: scaleX(-1);
}
.your-rewards {
margin: 0 auto;
width: fit-content;
.sparkles {
width: 2rem;
}
.rewards-title {
font-weight: bold;
margin: 1rem;
color: $gray-50;
align-self: baseline; // center would move it to the top?!
}
}
}
.content{
display: flex;
flex-direction: row;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
padding-bottom: 1rem;
}
</style>

View File

@@ -371,6 +371,7 @@ import shops from '@/../../common/script/libs/shops';
import SelectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
import FilterSidebar from '@/components/ui/filterSidebar';
import FilterGroup from '@/components/ui/filterGroup';
import { getClassName } from '../../../../../common/script/libs/getClassName';
export default {
components: {
@@ -523,10 +524,7 @@ export default {
},
methods: {
getClassName (classType) {
if (classType === 'wizard') {
return this.$t('mage');
}
return this.$t(classType);
return this.$t(getClassName(classType));
},
seasonalItems (category, sortBy, searchBy, viewOptions, hidePinned) {
let result = _map(category.items, e => ({

View File

@@ -1,5 +1,5 @@
<template>
<div class="section">
<div class="section" :class="{'visible':visible}">
<div class="section-header d-flex align-items-center">
<h3
v-once
@@ -22,7 +22,7 @@
:target="tooltipId"
/>
</div>
<SidebarButton
<SectionButton
:visible="visible"
@click="toggle"
/>
@@ -39,18 +39,25 @@
<style lang="scss" scoped>
.section {
border-top: 1px solid #e1e0e3;
margin-top: 1em;
padding-top: 1em;
/* To have the divider full width */
margin-right: -1.5rem;
margin-left: -1.5rem;
/* change back the content padding*/
padding-left: 1.5rem;
padding-right: 1.5rem;
padding-top: 0.688em;
padding-bottom: 0.75em;
&.visible {
padding-bottom: 1rem;
}
}
.section:last-of-type {
border-bottom: 1px solid #e1e0e3;
margin-bottom: 1em;
padding-bottom: 1em;
}
.section-body {
margin-top: 1em;
}
.section-info {
@@ -60,15 +67,30 @@
.section-info .svg-icon {
width: 16px;
}
.section-body ::v-deep {
> *:empty {
display: none;
}
> :first-child:not(:empty) {
margin-top: 0.75rem;
}
:last-child {
margin-bottom: 0;
}
}
</style>
<script>
import { v4 as uuid } from 'uuid';
import SidebarButton from './sidebarButton';
import SectionButton from './sectionButton';
import informationIcon from '@/assets/svg/information.svg';
export default {
components: { SidebarButton },
components: { SectionButton },
props: {
title: {
required: true,

View File

@@ -4,18 +4,11 @@ import { withKnobs, number } from '@storybook/addon-knobs';
import MultiList from './multiList';
import SelectMulti from './selectMulti';
import getStore from '@/store';
const stories = storiesOf('Multiple Select List', module);
stories.addDecorator(withKnobs);
// Needed for SelectTag
const store = getStore();
store.state.user.data = {
tags: [],
};
const exampleTagList = [
1, 2, 3,
];
@@ -96,7 +89,6 @@ stories
Added event: {{ added }}
</div>
`,
store,
data () {
return {
tagList: exampleTagList,
@@ -119,7 +111,6 @@ stories
@changed="tagList = $event"></SelectMulti>
</div>
`,
store,
data () {
return {
tagList: [],

View File

@@ -12,7 +12,7 @@
right: -9px;
top: -12px;
border-radius: 100px;
color: $white;
color: $white !important; // I saw the default gray-100 color
background: $gray-200;
padding: 4.5px 8.5px;
min-width: 1.5rem;

View File

@@ -6,6 +6,7 @@
:class="{condensed}"
>
<div
v-if="showIcon"
class="svg-icon"
v-html="icon"
></div>
@@ -16,7 +17,9 @@
:style="{width: `${percent(value, maxValue)}%`}"
></div>
</div>
<span class="small-text">{{ value | statFloor }} / {{ maxValue }}</span>
<span class="small-text" v-if="showNumbers">
{{ value | statFloor }} / {{ maxValue }}
</span>
<b-tooltip
class="myClass"
:target="() => $refs.container"
@@ -55,11 +58,13 @@
}
.progress-container > .progress {
--progress-background: #{$header-dark-background};
min-width: 200px;
margin: 0px;
border-radius: 1px;
height: 12px;
background-color: $header-dark-background;
background-color: var(--progress-background);
@media (max-width: 992px) {
min-width: 160px;
}
@@ -130,6 +135,14 @@ export default {
type: Boolean,
default: false,
},
showIcon: {
type: Boolean,
default: true,
},
showNumbers: {
type: Boolean,
default: true,
},
},
data () {
return {

View File

@@ -7,6 +7,7 @@
>
{{ displayName }}
<div
v-if="hasTier"
class="svg-icon"
v-html="tierIcon()"
></div>
@@ -97,6 +98,9 @@ export default {
return false;
},
hasTier () {
return this.isNPC || this.level !== 0;
},
},
methods: {
tierIcon () {

View File

@@ -17,11 +17,7 @@
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
a.no-tier {
color: $gray-50;
}
a.leader { // this is the user name
.user-link { // this is the user name
font-family: 'Roboto Condensed', sans-serif;
font-weight: bold;
margin-bottom: 0;
@@ -29,10 +25,26 @@
display: inline-block;
font-size: 16px;
// currently used in the member-details-new.vue
&.smaller {
font-family: Roboto;
font-size: 14px;
font-weight: bold;
line-height: 1.71;
}
&.no-tier {
color: $gray-50;
}
.svg-icon {
width: 10px;
display: inline-block;
margin-left: .5em;
&:empty {
display: none;
}
}
}
</style>
@@ -56,7 +68,15 @@ import tierNPC from '@/assets/svg/tier-npc.svg';
export default {
mixins: [styleHelper],
props: ['user', 'userId', 'name', 'backer', 'contributor', 'hideTooltip'],
props: [
'user',
'userId',
'name',
'backer',
'contributor',
'hideTooltip',
'smallerStyle',
],
data () {
return {
icons: Object.freeze({
@@ -114,7 +134,7 @@ export default {
return this.hideTooltip ? '' : achievementsLib.getContribText(this.contributor, this.isNPC) || '';
},
levelStyle () {
return this.userLevelStyleFromLevel(this.level, this.isNPC);
return `${this.userLevelStyleFromLevel(this.level, this.isNPC)} ${this.smallerStyle ? 'smaller' : ''}`;
},
},
};

View File

@@ -858,7 +858,10 @@ export default {
const profileUserId = this.userId;
if (profileUserId && profileUserId !== this.userLoggedIn._id) {
const response = await this.$store.dispatch('members:fetchMember', { memberId: profileUserId });
const response = await this.$store.dispatch('members:fetchMember', {
memberId: profileUserId,
unpack: false,
});
if (response.response && response.response.status === 404) {
user = null;
this.$store.dispatch('snackbars:add', {

View File

@@ -1,4 +1,5 @@
import axios from 'axios';
// import omit from 'lodash/omit';
// import findIndex from 'lodash/findIndex';
@@ -25,16 +26,24 @@ export async function getGroupMembers (store, payload) {
return response.data.data;
}
function unpackResponse (response) {
return response && response.data
? response.data.data
: undefined;
}
export async function fetchMember (store, payload) {
const url = `${apiv4Prefix}/members/${payload.memberId}`;
const response = await axios.get(url);
return response;
return payload.unpack === false
? response
: unpackResponse(response);
}
export async function fetchMemberByUsername (store, payload) {
const url = `${apiv4Prefix}/members/username/${payload.username}`;
const response = await axios.get(url);
return response;
return unpackResponse(response);
}
export async function getGroupInvites (store, payload) {

View File

@@ -74,8 +74,12 @@
"applySortToHeader": "Apply Sort Options to Party Header",
"confirmGuild": "Create Guild for 4 Gems?",
"confirm": "Confirm",
"leaveGroup": "Leave Guild",
"leaveGuild": "Leave Guild",
"leaveParty": "Leave Party",
"editParty": "Edit Party",
"editGuild": "Edit Guild",
"joinParty": "Join Party",
"joinGuild": "Join Guild",
"send": "Send",
"pmsMarkedRead": "Your Private Messages have been marked as read",
"possessiveParty": "<%= name %>'s Party",
@@ -254,6 +258,7 @@
"inviteEmailUsernameInfo": "Invite users via a valid email or username. If an email isn't registered yet, we'll invite them to join.",
"emailOrUsernameInvite": "Email address or username",
"messageGuildLeader": "Message Guild Leader",
"messagePartyLeader": "Message Party Leader",
"donateGems": "Donate Gems",
"updateGuild": "Update Guild",
"viewMembers": "View Members",
@@ -301,22 +306,22 @@
"wantToJoinPartyDescription": "Give your username to a friend who already has a Party, or head to the <a href='/groups/guild/f2db2a7f-13c5-454d-b3ee-ea1f5089e601'>Party Wanted Guild</a> to meet potential comrades!",
"copy": "Copy",
"inviteToPartyOrQuest": "Invite Party to Quest",
"inviteInformation": "Clicking \"Invite\" will send an invitation to your Party members. When all members have accepted or denied, the Quest begins.",
"questOwnerRewards": "Quest Owner Rewards",
"updateParty": "Update Party",
"upgrade": "Upgrade",
"upgradeToGroup": "Upgrade to Group",
"selectPartyMember": "Select a Party Member",
"areYouSureDeleteMessage": "Are you sure you want to delete this message?",
"reverseChat": "Reverse Chat",
"invites": "Invites",
"details": "Details",
"viewDetails": "View Details",
"participantDesc": "Once all members have either accepted or declined, the Quest begins. Only those who clicked 'accept' will be able to participate in the Quest and receive the rewards.",
"groupGems": "Group Gems",
"groupGemsDesc": "Guild Gems can be spent to make Challenges! In the future, you will be able to add more Guild Gems.",
"groupTaskBoard": "Task Board",
"groupInformation": "Group Information",
"groupBilling": "Group Billing",
"wouldYouParticipate": "Would you like to participate?",
"invitedToThisQuest": "You were invited to this Quest!",
"managerAdded": "Manager added successfully",
"managerRemoved": "Manager removed successfully",
"leaderChanged": "Leader has been changed",

View File

@@ -39,6 +39,7 @@
"hidePinned": "Hide pinned",
"hideMissing": "Hide Missing",
"amountExperience": "<%= amount %> Experience",
"amountExp": "<%= amount %> Exp",
"amountGold": "<%= amount %> Gold",
"namedHatchingPotion": "<%= type %> Hatching Potion",
"buyGems": "Buy Gems",

View File

@@ -23,6 +23,7 @@
"rejected": "Rejected",
"pending": "Pending",
"questCollection": "+ <%= val %> quest item(s) found",
"questItemsPending": "<%= amount %> Items pending",
"bossDamage": "You damaged the boss!",
"begin": "Begin",
"bossHP": "Boss HP",
@@ -32,17 +33,17 @@
"collected": "Collected",
"abort": "Abort",
"leaveQuest": "Leave Quest",
"sureLeave": "Are you sure you want to leave the active quest? All your quest progress will be lost.",
"sureLeave": "Are you sure you want to leave the Quest? All your progress will be lost.",
"sureLeaveInactive": "Are you sure you want to leave the Quest? You won't be able to participate.",
"mustComplete": "You must first complete <%= quest %>.",
"mustLvlQuest": "You must be level <%= level %> to buy this quest!",
"unlockByQuesting": "To unlock this quest, complete <%= title %>.",
"questConfirm": "Are you sure? Only <%= questmembers %> of your <%= totalmembers %> party members have joined this quest! Quests start automatically when all players have joined or rejected the invitation.",
"sureCancel": "Are you sure you want to cancel this quest? All invitation acceptances will be lost. The quest owner will retain possession of the quest scroll.",
"sureAbort": "Are you sure you want to abort this mission? It will abort it for everyone in your party and all progress will be lost. The quest scroll will be returned to the quest owner.",
"doubleSureAbort": "Are you double sure? Make sure they won't hate you forever!",
"questConfirm": "Are you sure you want to start this Quest? Not all Party members have accepted the Quest invite. Quests start automatically after all members respond to the invite.",
"sureCancel": "Are you sure you want to cancel this Quest? Canceling the Quest will cancel all accepted and pending invitations. The Quest will be returned to the owner's inventory.",
"sureAbort": "Are you sure you want to cancel this Quest? All progress will be lost. The Quest will be returned to the owner's inventory.",
"bossRageTitle": "Rage",
"bossRageDescription": "When this bar fills, the boss will unleash a special attack!",
"startAQuest": "START A QUEST",
"selectQuest": "Select Quest",
"startQuest": "Start Quest",
"questInvitationDoesNotExist": "No quest invitation has been sent out yet.",
"questInviteNotFound": "No quest invitation found.",
@@ -54,21 +55,23 @@
"questAlreadyStarted": "The quest has already started.",
"questAlreadyStartedFriendly": "The quest has already started, but you can always catch the next one!",
"questAlreadyAccepted": "You already accepted the quest invitation.",
"noActiveQuestToLeave": "No active quest to leave",
"questLeaderCannotLeaveQuest": "Quest leader cannot leave quest",
"notPartOfQuest": "You are not part of the quest",
"youAreNotOnQuest": "You're not on a quest",
"yourPartyIsNotOnQuest": "Your Party is not on a Quest",
"noActiveQuestToAbort": "There is no active quest to abort.",
"onlyLeaderAbortQuest": "Only the group or quest leader can abort a quest.",
"questAlreadyRejected": "You already rejected the quest invitation.",
"cantCancelActiveQuest": "You can not cancel an active quest, use the abort functionality.",
"onlyLeaderCancelQuest": "Only the group or quest leader can cancel the quest.",
"questNotPending": "There is no quest to start.",
"membersParticipating": "<%= accepted %> / <%= invited %> Members participating",
"questOrGroupLeaderOnlyStartQuest": "Only the quest leader or group leader can force start the quest",
"loginIncentiveQuest": "To unlock this quest, check in to Habitica on <%= count %> different days!",
"loginReward": "<%= count %> Check-ins",
"questBundles": "Discounted Quest Bundles",
"noQuestToStart": "Cant find a quest to start? Try checking out the Quest Shop in the Market for new releases!",
"noQuestToStartTitle": "Cant find a Quest to start?",
"noQuestToStart": "Try checking out the <a href=\"<%= questShop %>\">Quest Shop</a> for new releases!",
"pendingDamage": "<%= damage %> pending damage",
"pendingDamageLabel": "pending damage",
"bossHealth": "<%= currentHealth %> / <%= maxHealth %> Health",
@@ -83,5 +86,12 @@
"chatItemQuestFinish": "All items found! Party has received their rewards.",
"chatQuestAborted": "<%= username %> aborted the party quest <%= questName %>.",
"chatQuestCancelled": "<%= username %> cancelled the party quest <%= questName %>.",
"tavernBossTired": "<%= bossName %> tries to unleash <%= rageName %> but is too tired."
"tavernBossTired": "<%= bossName %> tries to unleash <%= rageName %> but is too tired.",
"ownerOnly": "Owner only",
"newItem": "New Item",
"selectQuestModal": "Select a Quest",
"yourQuests": "Your Quests",
"backToSelection": "Back to Quest selection",
"cancelQuest": "Cancel Quest",
"questOwner": "Quest Owner"
}

View File

@@ -13,6 +13,7 @@ import * as contributorGear from './special-contributor';
import * as takeThisGear from './special-takeThis';
import * as wonderconGear from './special-wondercon';
import t from '../../../translation';
import { getClassName } from '../../../../libs/getClassName';
const CURRENT_EVENT = find(
EVENTS, event => moment().isBetween(event.start, event.end) && Boolean(event.season),
@@ -714,7 +715,7 @@ const armorStats = {
Object.keys(gearEvents).forEach(event => {
CLASSES.forEach(klass => {
const classNameString = klass === 'wizard' ? 'mage' : klass;
const classNameString = getClassName(klass);
const eventString = `${event}${upperFirst(classNameString)}`;
const textString = `armorSpecial${upperFirst(event)}${upperFirst(classNameString)}`;
defaults(armor[eventString], {
@@ -1761,7 +1762,7 @@ const headStats = {
Object.keys(gearEvents).forEach(event => {
CLASSES.forEach(klass => {
const classNameString = klass === 'wizard' ? 'mage' : klass;
const classNameString = getClassName(klass);
const eventString = `${event}${upperFirst(classNameString)}`;
const textString = `headSpecial${upperFirst(event)}${upperFirst(classNameString)}`;
defaults(head[eventString], {
@@ -3192,7 +3193,7 @@ const weaponCosts = {
Object.keys(gearEvents).forEach(event => {
CLASSES.forEach(klass => {
const classNameString = klass === 'wizard' ? 'mage' : klass;
const classNameString = getClassName(klass);
const eventString = `${event}${upperFirst(classNameString)}`;
const textString = `weaponSpecial${upperFirst(event)}${upperFirst(classNameString)}`;
defaults(weapon[eventString], {

View File

@@ -0,0 +1,3 @@
export function getClassName (klass) {
return klass === 'wizard' ? 'mage' : klass;
}

View File

@@ -12,8 +12,6 @@ export default function percent (x, y, dir) {
default:
roundFn = Math.round;
}
if (x === 0) {
x = 1; // eslint-disable-line no-param-reassign
}
return Math.max(0, roundFn((x / y) * 100));
}

View File

@@ -16,6 +16,7 @@ import seasonalShopConfig from './shops-seasonal.config';
import featuredItems from '../content/shop-featuredItems';
import getOfficialPinnedItems from './getOfficialPinnedItems';
import { getClassName } from './getClassName';
const shops = {};
@@ -89,11 +90,8 @@ shops.getMarketCategories = function getMarket (user, language) {
return categories;
};
function getClassName (classType, language) {
if (classType === 'wizard') {
return i18n.t('mage', language);
}
return i18n.t(classType, language);
function getTranslatedClassName (classType, language) {
return i18n.t(getClassName(classType), language);
}
// TODO Refactor the `.locked` logic
@@ -143,7 +141,7 @@ shops.getMarketGearCategories = function getMarketGear (user, language) {
for (const classType of content.classes) {
const category = {
identifier: classType,
text: getClassName(classType, language),
text: getTranslatedClassName(classType, language),
};
const result = filter(content.gear.flat, gearItem => {

View File

@@ -518,7 +518,6 @@ api.leaveQuest = {
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.type !== 'party') throw new NotAuthorized(res.t('guildQuestsNotSupported'));
if (!group.quest.active) throw new NotFound(res.t('noActiveQuestToLeave'));
if (group.quest.leader === user._id) throw new NotAuthorized(res.t('questLeaderCannotLeaveQuest'));
if (!group.quest.members[user._id]) throw new NotAuthorized(res.t('notPartOfQuest'));