diff --git a/test/client/unit/specs/components/categories/categoryTags.js b/test/client/unit/specs/components/categories/categoryTags.js new file mode 100644 index 0000000000..a0ea2e69b7 --- /dev/null +++ b/test/client/unit/specs/components/categories/categoryTags.js @@ -0,0 +1,65 @@ +import {shallow} from '@vue/test-utils'; + +import CategoryTags from 'client/components/categories/categoryTags.vue'; + +describe('Category Tags', () => { + let wrapper; + + beforeEach(function () { + wrapper = shallow(CategoryTags, { + propsData: { + categories: [], + }, + slots: { + default: '
This is a slot.
', + }, + mocks: { + $t: (string) => string, + }, + }); + }); + + it('displays a category', () => { + wrapper.setProps({ + categories: [ + { + name: 'test', + }, + ], + }); + expect(wrapper.contains('.category-label')).to.eq(true); + expect(wrapper.find('.category-label').text()).to.eq('test'); + }); + + it('displays a habitica official in purple', () => { + wrapper.setProps({ + categories: [ + { + name: 'habitica_official', + }, + ], + }); + expect(wrapper.contains('.category-label-purple')).to.eq(true); + expect(wrapper.find('.category-label').text()).to.eq('habitica_official'); + }); + + it('displays owner label', () => { + wrapper.setProps({ + owner: true, + }); + expect(wrapper.contains('.category-label-blue')).to.eq(true); + expect(wrapper.find('.category-label').text()).to.eq('owned'); + }); + + it('displays member label', () => { + wrapper.setProps({ + member: true, + }); + expect(wrapper.contains('.category-label-green')).to.eq(true); + expect(wrapper.find('.category-label').text()).to.eq('joined'); + }); + + it('displays additional content at the end', () => { + expect(wrapper.find('p').text()).to.eq('This is a slot.'); + }); +}); \ No newline at end of file diff --git a/test/client/unit/specs/components/sidebarSection.js b/test/client/unit/specs/components/sidebarSection.js new file mode 100644 index 0000000000..23b651b7fc --- /dev/null +++ b/test/client/unit/specs/components/sidebarSection.js @@ -0,0 +1,54 @@ +import {shallow} from '@vue/test-utils'; + +import SidebarSection from 'client/components/sidebarSection.vue'; + +describe('Sidebar Section', () => { + let wrapper; + + beforeEach(function () { + wrapper = shallow(SidebarSection, { + propsData: { + title: 'Hello World', + }, + slots: { + default: 'This is a test.
', + }, + }); + }); + + it('displays title', () => { + expect(wrapper.find('h3').text()).to.eq('Hello World'); + }); + + it('displays contents', () => { + expect(wrapper.find('.section-body').find('p').text()).to.eq('This is a test.'); + }); + + it('displays tooltip icon', () => { + expect(wrapper.contains('.section-info')).to.eq(false); + wrapper.setProps({tooltip: 'This is a test'}); + expect(wrapper.contains('.section-info')).to.eq(true); + }); + + it('hides contents', () => { + expect(wrapper.find('.section-body').element.style.display).to.not.eq('none'); + wrapper.find('.section-toggle').trigger('click'); + expect(wrapper.find('.section-body').element.style.display).to.eq('none'); + wrapper.find('.section-toggle').trigger('click'); + expect(wrapper.find('.section-body').element.style.display).to.not.eq('none'); + }); + + it('can hide contents by default', () => { + wrapper = shallow(SidebarSection, { + propsData: { + title: 'Hello World', + show: false, + }, + slots: { + default: 'This is a test.
', + }, + }); + + expect(wrapper.find('.section-body').element.style.display).to.eq('none'); + }); +}); \ No newline at end of file diff --git a/website/client/assets/scss/categories.scss b/website/client/assets/scss/categories.scss index 7317e9ee5e..3108537157 100644 --- a/website/client/assets/scss/categories.scss +++ b/website/client/assets/scss/categories.scss @@ -15,7 +15,7 @@ background-color: $gray-600; padding: .5em; display: inline-block; - margin-right: .5em; + margin: .25em; font-size: 12px; font-weight: 500; line-height: 1.33; @@ -23,6 +23,21 @@ color: $gray-300; } +.category-label-purple { + color: white; + background: $purple-300; +} + +.category-label-blue { + color: white; + background: $blue-50; +} + +.category-label-green { + color: white; + background: $green-50; +} + .category-select { border-radius: 2px; background-color: $white; diff --git a/website/client/components/categories/categoryTags.vue b/website/client/components/categories/categoryTags.vue new file mode 100644 index 0000000000..aaa0209db3 --- /dev/null +++ b/website/client/components/categories/categoryTags.vue @@ -0,0 +1,34 @@ + + .categories + span.category-label.category-label-blue(v-if='owner') + | {{ $t('owned') }} + span.category-label.category-label-green(v-if='member') + | {{ $t('joined') }} + span.category-label( + v-for='category in categories', + :class="{'category-label-purple':isOfficial(category)}" + ) + | {{ $t(category.name) }} + slot + + + \ No newline at end of file diff --git a/website/client/components/challenges/challengeDetail.vue b/website/client/components/challenges/challengeDetail.vue index 19ddd1f183..70b9ff2c60 100644 --- a/website/client/components/challenges/challengeDetail.vue +++ b/website/client/components/challenges/challengeDetail.vue @@ -6,11 +6,15 @@ challenge-member-progress-modal(:memberId='progressMemberId', :challengeId='challenge._id') .col-12.col-md-8.standard-page .row - .col-12.col-md-8 + .col-12.col-md-6 h1(v-markdown='challenge.name') div - strong(v-once) {{$t('createdBy')}}: - span(v-if='challenge.leader && challenge.leader.profile') {{challenge.leader.profile.name}} + span.mr-1.ml-0 + strong(v-once) {{ $t('createdBy') }}: + user-link.mx-1(:user="challenge.leader") + span.mr-1.ml-0(v-if="challenge.group && challenge.group.name !== 'Tavern'") + strong(v-once) {{ $t(challenge.group.type) }}: + group-link.mx-1(:group="challenge.group") // @TODO: make challenge.author a variable inside the createdBy string (helps with RTL languages) // @TODO: Implement in V2 strong.margin-left(v-once) .svg-icon.calendar-icon(v-html="icons.calendarIcon") @@ -19,7 +23,7 @@ // span {{challenge.endDate}} .tags span.tag(v-for='tag in challenge.tags') {{tag}} - .col-12.col-md-4 + .col-12.col-md-6.text-right .box(@click="showMemberModal()") .svg-icon.member-icon(v-html="icons.memberIcon") | {{challenge.memberCount}} @@ -29,10 +33,10 @@ | {{challenge.prize}} .details(v-once) {{$t('prize')}} .row.challenge-actions - .col-12.col-md-7.offset-md-5 - span.view-progress - strong {{ $t('viewProgressOf') }} + .col-12.col-md-6 + strong.view-progress {{ $t('viewProgressOf') }} member-search-dropdown(:text="$t('selectParticipant')", :members='members', :challengeId='challengeId', @member-selected='openMemberProgressModal') + .col-12.col-md-6.text-right span(v-if='isLeader || isAdmin') b-dropdown.create-dropdown(:text="$t('addTaskToChallenge')", :variant="'success'") b-dropdown-item(v-for="type in columns", :key="type", @click="createTask(type)") @@ -56,24 +60,23 @@ v-on:editTask="editTask", v-if='tasksByType[column].length > 0') .col-12.col-md-4.sidebar.standard-page - .acitons - div(v-if='canJoin') - button.btn.btn-success(v-once, @click='joinChallenge()') {{$t('joinChallenge')}} - div(v-if='isMember') - button.btn.btn-danger(v-once, @click='leaveChallenge()') {{$t('leaveChallenge')}} - div(v-if='isLeader || isAdmin') - button.btn.btn-secondary(v-once, @click='edit()') {{$t('editChallenge')}} - div(v-if='isLeader || isAdmin') - button.btn.btn-danger(v-once, @click='closeChallenge()') {{$t('endChallenge')}} - div(v-if='isLeader || isAdmin') - button.btn.btn-secondary(v-once, @click='exportChallengeCsv()') {{$t('exportChallengeCsv')}} - div(v-if='isLeader || isAdmin') - button.btn.btn-secondary(v-once, @click='cloneChallenge()') {{$t('clone')}} - .description-section - h2 {{$t('challengeSummary')}} - p(v-markdown='challenge.summary') - h2 {{$t('challengeDescription')}} - p(v-markdown='challenge.description') + .button-container(v-if='canJoin') + button.btn.btn-success(v-once, @click='joinChallenge()') {{$t('joinChallenge')}} + .button-container(v-if='isLeader || isAdmin') + button.btn.btn-primary(v-once, @click='edit()') {{$t('editChallenge')}} + .button-container(v-if='isLeader || isAdmin') + button.btn.btn-primary(v-once, @click='cloneChallenge()') {{$t('clone')}} + .button-container(v-if='isLeader || isAdmin') + button.btn.btn-primary(v-once, @click='exportChallengeCsv()') {{$t('exportChallengeCsv')}} + .button-container(v-if='isLeader || isAdmin') + button.btn.btn-danger(v-once, @click='closeChallenge()') {{$t('endChallenge')}} + div + sidebar-section(:title="$t('challengeSummary')") + p(v-markdown='challenge.summary') + sidebar-section(:title="$t('challengeDescription')") + p(v-markdown='challenge.description') + .text-center(v-if='isMember') + button.btn.btn-danger(v-once, @click='leaveChallenge()') {{$t('leaveChallenge')}} - - diff --git a/website/client/components/challenges/findChallenges.vue b/website/client/components/challenges/findChallenges.vue index 8bfe6c6e1b..6b3308adae 100644 --- a/website/client/components/challenges/findChallenges.vue +++ b/website/client/components/challenges/findChallenges.vue @@ -15,7 +15,7 @@ .svg-icon.positive-icon(v-html="icons.positiveIcon") span(v-once) {{$t('createChallenge')}} .row - .col-12.col-md-6(v-for='challenge in filteredChallenges', v-if='!memberOf(challenge)') + .col-12.col-md-6(v-for='challenge in filteredChallenges') challenge-item(:challenge='challenge') .row .col-12.text-center @@ -105,9 +105,6 @@ export default { }, }, methods: { - memberOf (challenge) { - return this.user.challenges.indexOf(challenge._id) !== -1; - }, updateSearch (eventData) { this.search = eventData.searchTerm; this.page = 0; diff --git a/website/client/components/challenges/groupChallenges.vue b/website/client/components/challenges/groupChallenges.vue index ceeeccf370..1415b921a9 100644 --- a/website/client/components/challenges/groupChallenges.vue +++ b/website/client/components/challenges/groupChallenges.vue @@ -6,131 +6,87 @@ div .svg-icon.challenge-icon(v-html="icons.challengeIcon") h4(v-once) {{ $t('haveNoChallenges') }} p(v-once) {{ $t('challengeDetails') }} - router-link.title(:to="{ name: 'challenge', params: { challengeId: challenge._id } }", v-for='challenge in challenges',:key='challenge._id') - .col-12.challenge-item - .row - .col-9 - router-link.title(:to="{ name: 'challenge', params: { challengeId: challenge._id } }") - strong(v-markdown='challenge.name') - p(v-markdown='challenge.summary || challenge.name') - div - .svg-icon.member-icon(v-html="icons.memberIcon") - .member-count {{challenge.memberCount}} - .col-3 - div - span.svg-icon.gem(v-html="icons.gemIcon") - span.prize {{challenge.prize}} - div.prize-title Prize - .col-12.text-center - button.btn.btn-secondary(@click='createChallenge()') {{ $t('createChallenge') }} + button.btn.btn-secondary(@click='createChallenge()') {{ $t('createChallenge') }} + template(v-else) + challenge-item(v-for='challenge in challenges',:challenge='challenge',:key='challenge._id',:fullLayout='false') + .col-12.text-center + button.btn.btn-secondary(@click='createChallenge()') {{ $t('createChallenge') }} - diff --git a/website/client/components/challenges/myChallenges.vue b/website/client/components/challenges/myChallenges.vue index c80d73763b..a512c355f4 100644 --- a/website/client/components/challenges/myChallenges.vue +++ b/website/client/components/challenges/myChallenges.vue @@ -63,8 +63,6 @@ \ No newline at end of file diff --git a/website/client/components/groups/group.vue b/website/client/components/groups/group.vue index 64e2bd3c62..3f61a5033f 100644 --- a/website/client/components/groups/group.vue +++ b/website/client/components/groups/group.vue @@ -9,8 +9,10 @@ .row .col-12.col-md-6.title-details h1 {{group.name}} - strong.float-left(v-once) {{$t('groupLeader')}} - span.leader.float-left(v-if='group.leader.profile', @click='showMemberProfile(group.leader)') : {{group.leader.profile.name}} + div + span.mr-1.ml-0 + strong(v-once) {{$t('groupLeader')}}: + user-link.mx-1(:user="group.leader") .col-12.col-md-6 .row.icon-row .col-4.offset-4(v-bind:class="{ 'offset-8': isParty }") @@ -61,47 +63,16 @@ // @TODO: V2 button.btn.btn-primary(v-once, v-if='!isLeader') {{$t('messageGuildLeader')}} // Suggest making the button visible to the leader too - useful for them to test how the feature works or to send a note to themself. -- Alys .button-container // @TODO: V2 button.btn.btn-primary(v-once, v-if='isMember && !isParty') {{$t('donateGems')}} // Suggest removing the isMember restriction - it's okay if non-members donate to a public guild. Also probably allow it for parties if parties can buy imagery. -- Alys - .section-header(v-if='isParty') - quest-sidebar-section(@toggle='toggleQuestSection', :show='sections.quest', :group='group') - .section-header(v-if='!isParty') - .row - .col-10 - h3(v-once) {{ $t('guildSummary') }} - .col-2 - .toggle-up(@click="sections.summary = !sections.summary", v-if="sections.summary") - .svg-icon(v-html="icons.upIcon") - .toggle-down(@click="sections.summary = !sections.summary", v-if="!sections.summary") - .svg-icon(v-html="icons.downIcon") - .section(v-if="sections.summary") + div + quest-sidebar-section(:group='group', v-if='isParty') + sidebar-section(:title="$t('guildSummary')", v-if='!isParty') p(v-markdown='group.summary') - .section-header - .row - .col-10 - h3 {{ $t('groupDescription') }} - .col-2 - .toggle-up(@click="sections.description = !sections.description", v-if="sections.description") - .svg-icon(v-html="icons.upIcon") - .toggle-down(@click="sections.description = !sections.description", v-if="!sections.description") - .svg-icon(v-html="icons.downIcon") - .section(v-if="sections.description") + sidebar-section(:title="$t('groupDescription')") p(v-markdown='group.description') - .section-header.challenge - .row - .col-10.information-header - h3(v-once) - | {{ $t('challenges') }} - #groupPrivateDescOrChallengeInfo.icon.tooltip-wrapper(:title="isParty ? $t('challengeDetails') : $t('privateDescription')") - .svg-icon(v-html='icons.information') - b-tooltip( - :title="isParty ? $t('challengeDetails') : $t('privateDescription')", - target="groupPrivateDescOrChallengeInfo", - ) - .col-2 - .toggle-up(@click="sections.challenges = !sections.challenges", v-if="sections.challenges") - .svg-icon(v-html="icons.upIcon") - .toggle-down(@click="sections.challenges = !sections.challenges", v-if="!sections.challenges") - .svg-icon(v-html="icons.downIcon") - .section(v-if="sections.challenges") + sidebar-section( + :title="$t('challenges')", + :tooltip="isParty ? $t('challengeDetails') : $t('privateDescription')" + ) group-challenges(:groupId='searchId') div.text-center button.btn.btn-danger(v-if='isMember', @click='clickLeave()') {{ isParty ? $t('leaveParty') : $t('leaveGroup') }} @@ -124,10 +95,6 @@ color: $purple-200; } - .leader:hover { - cursor: pointer; - } - .button-container { margin-bottom: 1em; @@ -270,29 +237,6 @@ margin-right: .3em; } - .information-header { - h3, .tooltip-wrapper { - display: inline-block; - } - - .tooltip-wrapper { - width: 15px; - margin-left: 1.2em; - } - } - - .section-header { - border-top: 1px solid #e1e0e3; - margin-top: 1em; - padding-top: 1em; - } - - .section-header.challenge { - border-bottom: 1px solid #e1e0e3; - margin-bottom: 1em; - padding-bottom: 1em; - } - .hr { width: 100%; height: 20px; @@ -334,6 +278,8 @@ import groupGemsModal from 'client/components/groups/groupGemsModal'; import questSidebarSection from 'client/components/groups/questSidebarSection'; import markdownDirective from 'client/directives/markdown'; import communityGuidelines from './communityGuidelines'; +import sidebarSection from '../sidebarSection'; +import userLink from '../userLink'; import deleteIcon from 'assets/svg/delete.svg'; import copyIcon from 'assets/svg/copy.svg'; @@ -342,10 +288,7 @@ import likedIcon from 'assets/svg/liked.svg'; import reportIcon from 'assets/svg/report.svg'; import gemIcon from 'assets/svg/gem.svg'; import questIcon from 'assets/svg/quest.svg'; -import informationIcon from 'assets/svg/information.svg'; import questBackground from 'assets/svg/quest-background-border.svg'; -import upIcon from 'assets/svg/up.svg'; -import downIcon from 'assets/svg/down.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'; @@ -365,6 +308,8 @@ export default { groupGemsModal, questSidebarSection, communityGuidelines, + sidebarSection, + userLink, }, directives: { markdown: markdownDirective, @@ -381,22 +326,13 @@ export default { gem: gemIcon, liked: likedIcon, questIcon, - information: informationIcon, questBackground, - upIcon, - downIcon, goldGuildBadgeIcon, silverGuildBadgeIcon, bronzeGuildBadgeIcon, }), members: [], selectedQuest: {}, - sections: { - quest: true, - summary: true, - description: true, - challenges: true, - }, chat: { submitDisable: false, submitTimeout: null, @@ -714,19 +650,9 @@ export default { } // $rootScope.$state.go('options.inventory.quests'); }, - async showMemberProfile (leader) { - let heroDetails = await this.$store.dispatch('members:fetchMember', { memberId: leader._id }); - this.$root.$emit('habitica:show-profile', { - user: heroDetails.data.data, - startingPage: 'profile', - }); - }, showGroupGems () { this.$root.$emit('bv::show::modal', 'group-gems-modal'); }, - toggleQuestSection () { - this.sections.quest = !this.sections.quest; - }, }, }; diff --git a/website/client/components/groups/publicGuildItem.vue b/website/client/components/groups/publicGuildItem.vue index f01ccded7d..a08548eb17 100644 --- a/website/client/components/groups/publicGuildItem.vue +++ b/website/client/components/groups/publicGuildItem.vue @@ -24,9 +24,7 @@ router-link.card-link(:to="{ name: 'guild', params: { groupId: guild._id } }") span.count {{ guild.balance * 4 }} div.guild-bank(v-if='displayGemBank', v-once) {{$t('guildBank')}} .row - .col-md-12 - .category-label(v-for="category in guild.categorySlugs") - | {{$t(category)}} + category-tags.col-md-12(:categories="guild.categories", :owner="isOwner", v-once) span.recommend-text(v-if='showSuggested(guild._id)') Suggested because you’re new to Habitica. @@ -128,6 +126,7 @@ router-link.card-link(:to="{ name: 'guild', params: { groupId: guild._id } }") \ No newline at end of file diff --git a/website/client/components/userLink.vue b/website/client/components/userLink.vue new file mode 100644 index 0000000000..c0e51fff43 --- /dev/null +++ b/website/client/components/userLink.vue @@ -0,0 +1,21 @@ + + b-link( + v-if='user && user.profile', + @click.prevent='showProfile(user)' + ) {{user.profile.name}} + + + \ No newline at end of file diff --git a/website/common/locales/en/challenge.json b/website/common/locales/en/challenge.json index a5ca557e4a..39d0342263 100644 --- a/website/common/locales/en/challenge.json +++ b/website/common/locales/en/challenge.json @@ -13,7 +13,8 @@ "challengeWinner": "Was the winner in the following challenges", "challenges": "Challenges", "challengesLink": "Challenges", - + "challengePrize": "Challenge Prize", + "endDate": "Ends", "noChallenges": "No challenges yet, visit", "toCreate": "to create one.", @@ -25,7 +26,9 @@ "filter": "Filter", "groups": "Groups", "noNone": "None", + "category": "Category", "membership": "Membership", + "ownership": "Ownership", "participating": "Participating", "notParticipating": "Not Participating", "either": "Either", diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json index 4eded55be6..59cc81f735 100644 --- a/website/common/locales/en/groups.json +++ b/website/common/locales/en/groups.json @@ -371,9 +371,11 @@ "recentActivity": "Recent Activity", "myGuilds": "My Guilds", "guildsDiscovery": "Discover Guilds", + "role": "Role", "guildOrPartyLeader": "Leader", "guildLeader": "Guild Leader", "member": "Member", + "guildSize": "Guild Size", "goldTier": "Gold Tier", "silverTier": "Silver Tier", "bronzeTier": "Bronze Tier", diff --git a/website/common/locales/en/npc.json b/website/common/locales/en/npc.json index 49b4a6b456..5c404f64d4 100644 --- a/website/common/locales/en/npc.json +++ b/website/common/locales/en/npc.json @@ -53,6 +53,7 @@ "featuredItems": "Featured Items!", "hideLocked": "Hide locked", "hidePinned": "Hide pinned", + "hideMissing": "Hide Missing", "amountExperience": "<%= amount %> Experience", "amountGold": "<%= amount %> Gold", "namedHatchingPotion": "<%= type %> Hatching Potion",