New client guilds (#8736)

* add colors palette

* add secondary menu component and style it

* add box shadow to secondary menu

* misc css, fixes for secondary menu

* client: add equipment page with grouping, css: add some styles

* add typography

* more equipment

* stable: fix linting

* equipment: add styles (lots of general styles too)

* remove duplicate google fonts loading

* add dropdowns

* design: white search input background, remove gray from items

* start adding drawer and selected indicator

* wip equipment

* fix equipment

* equipment: correctly bind new properties on items.gear.equipped

* equipment: fix vue binding. version 2

* equipment: fix vue binding. version 3

* back to first fix for equip op, fix for sourcemaps, send http request when an item is equipped, load bootstrap-vue components where needed

* checkboxes and radio buttons

* correctly renders selected items in first postion during the first render

* add search

* general changes, constants part of app state, add popovers

* add toggle switch, rename css

* correct offset

* upgrade deps

* upgrade deps

* drawer and lot of other work

* update equipping mechanism

* finish equipment

* fix compilation and upgrade deps

* use v-show in place of v-if to fix ui issues

* v-show -> v-if

* Start of guild syyles

* fix linting in test/client

* fix es6 compilation in test/client

* fix babel compilation for tests

* fix groupsUtilities mixin tests

* More designs

* Added public guild state

* Added my guilds store

* client: buttons

* client: buttons: fix colors

* Added join and leave

* Began adding new guild form

* Create form updates

* Added search to local data

* Added filtering

* Added initial code for group create

* Added more create checks

* Added more guild routes

* Added styles to guild page

* Added more chat styles

* Began porting over angular functions

* Moved over group service functions

* Added paging

* Updated sidebar

* Updated join/leave and minor text

* Added new sidebar functions

* Updated paging

* Added some form updates

* Added more translations and styles

* Updated shrinkwrap

* Removed features config

* Lint cleanup

* Added member modal

* Added more member actions

* Updated nav

* Fixed filter toggling

* Updated create guild

* Added no guild page

* Added sort select

* Added more styles

* Added update guild form

* Removed extra css and other minor changes

* Many css and syntax fixes

* Fixed color and merge conflic

* Removed paging from my guilds

* Removed extra strings

* Many requests updates

* Small style fixes
This commit is contained in:
Keith Holliday
2017-06-02 14:55:02 -06:00
committed by GitHub
parent b606dd1c40
commit 2e9bc2c31c
43 changed files with 1854 additions and 1096 deletions

969
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#A5A1AC" fill-rule="evenodd" d="M6 14h8V4H6v10zm-4-2V2h4a2 2 0 0 0-2 2v8H2zM14 2h-2a2 2 0 0 0-2-2H2a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 290 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="16" viewBox="0 0 14 16">
<path fill="#A5A1AC" fill-rule="evenodd" d="M3 14h8V4H3v10zM14 4h-1v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V4H0V2h4V1a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v1h4v2zm-6 8h1V6H8v6zm-3 0h1V6H5v6z"/>
</svg>

After

Width:  |  Height:  |  Size: 274 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#A5A1AC" fill-rule="evenodd" d="M11 10c0 1.654-1.346 3-3 3s-3-1.346-3-3h6zm2-3.5a1.5 1.5 0 1 1-3.001-.001A1.5 1.5 0 0 1 13 6.5zm-7 0a1.5 1.5 0 1 1-3.001-.001A1.5 1.5 0 0 1 6 6.5zM2 14h12V2H2v12zM14 0H2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 377 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#A5A1AC" fill-rule="evenodd" d="M11 10c0 1.654-1.346 3-3 3s-3-1.346-3-3h6zm2-3.5a1.5 1.5 0 1 1-3.001-.001A1.5 1.5 0 0 1 13 6.5zm-7 0a1.5 1.5 0 1 1-3.001-.001A1.5 1.5 0 0 1 6 6.5zM14 0H2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 361 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#A5A1AC" fill-rule="evenodd" d="M6 12h8V6H6v6zM4 9H2V3h7v1H6a2 2 0 0 0-2 2v3zm10-5h-3V3a2 2 0 0 0-2-2H2a1 1 0 0 0-2 0v15h2v-5h2v1a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 287 B

View File

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="78" height="76" viewBox="0 0 78 76">
<defs>
<path id="a" d="M35 0L0 15.235C0 45.705 4.255 62.941 35 76c30.745-13.059 35-30.294 35-60.765L35 0z"/>
<rect id="b" width="70" height="24" y="42" rx="12"/>
</defs>
<g fill="none" fill-rule="evenodd">
<g transform="translate(4)">
<use fill="#EA8C31" xlink:href="#a"/>
<path stroke="#B36213" stroke-width="10" d="M34.03 5.031l4.443 1.934 12.031 5.237 12.032 5.237 2.445 1.065c-.183 14.83-1.676 23.96-5.418 31.395C55.326 58.32 47.803 64.81 35 70.545 22.197 64.81 14.674 58.32 10.437 49.9c-3.742-7.435-5.235-16.565-5.418-31.395L35 5.454l-.97-.423z"/>
<path stroke="#D77A20" stroke-width="6" d="M34.421 3.02a506596810.815 506596810.815 0 0 0 16.882 7.348l12.03 5.237 3.66 1.593c-.111 15.9-1.621 25.609-5.643 33.6C56.795 59.848 48.69 66.734 35 72.733c-13.691-6-21.795-12.885-26.35-21.935-4.022-7.991-5.532-17.7-5.643-33.6L35 3.272l-.579-.252z"/>
</g>
<g transform="translate(4)">
<use fill="#FFF" xlink:href="#b"/>
<rect width="74" height="28" x="-2" y="40" stroke="#B36213" stroke-width="4" rx="14"/>
</g>
<path fill="#B36213" d="M31 30.667V34h16v-3.333C47 27.557 41.668 26 39 26c-2.666 0-8 1.556-8 4.667zM39.006 16A4.005 4.005 0 0 0 35 20c0 2.208 1.795 4 4.006 4A3.993 3.993 0 0 0 43 20c0-2.207-1.781-4-3.994-4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="78" height="76" viewBox="0 0 78 76">
<defs>
<path id="a" d="M35 0L0 15.235C0 45.705 4.255 62.941 35 76c30.745-13.059 35-30.294 35-60.765L35 0z"/>
<rect id="b" width="70" height="24" y="42" rx="12"/>
</defs>
<g fill="none" fill-rule="evenodd">
<g transform="translate(4)">
<use fill="#FFBC5A" xlink:href="#a"/>
<path stroke="#DF911E" stroke-width="10" d="M34.03 5.031l4.443 1.934 12.031 5.237 12.032 5.237 2.445 1.065c-.183 14.83-1.676 23.96-5.418 31.395C55.326 58.32 47.803 64.81 35 70.545 22.197 64.81 14.674 58.32 10.437 49.9c-3.742-7.435-5.235-16.565-5.418-31.395L35 5.454l-.97-.423z"/>
<path stroke="#FFA623" stroke-width="6" d="M34.421 3.02a506596810.815 506596810.815 0 0 0 16.882 7.348l12.03 5.237 3.66 1.593c-.111 15.9-1.621 25.609-5.643 33.6C56.795 59.848 48.69 66.734 35 72.733c-13.691-6-21.795-12.885-26.35-21.935-4.022-7.991-5.532-17.7-5.643-33.6L35 3.272l-.579-.252z"/>
</g>
<g transform="translate(4)">
<use fill="#FFF" xlink:href="#b"/>
<rect width="74" height="28" x="-2" y="40" stroke="#DF911E" stroke-width="4" rx="14"/>
</g>
<path fill="#DF911E" d="M31 30.667V34h16v-3.333C47 27.557 41.668 26 39 26c-2.666 0-8 1.556-8 4.667zM39.006 16A4.005 4.005 0 0 0 35 20c0 2.208 1.795 4 4.006 4A3.993 3.993 0 0 0 43 20c0-2.207-1.781-4-3.994-4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path fill="#24CC8F" d="M0 9l5-7h14l5 7-12 13z"/>
<path fill="#FFF" d="M7 8.8L6 4h6zM17 8.8L18 4h-6z" opacity=".25"/>
<path fill="#FFF" d="M7 8.8L12 4l5 4.8zM2.6 8.8L6 4l1 4.8z" opacity=".5"/>
<path fill="#1B996B" d="M21.4 8.8L18 4l-1 4.8zM2.6 8.8H7l5 10.3z" opacity=".35"/>
<path fill="#FFF" d="M21.4 8.8H17l-5 10.3z" opacity=".5"/>
<path fill="#FFF" d="M7 8.8h10l-5 10.3z" opacity=".25"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 579 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="76" height="88" viewBox="0 0 76 88">
<defs>
<path id="a" d="M38 0L0 17.64C0 52.924 4.62 72.88 38 88c33.38-15.12 38-35.077 38-70.36L38 0z"/>
</defs>
<g fill="none" fill-rule="evenodd">
<g>
<use fill="#C3C0C7" xlink:href="#a"/>
<path stroke="#F9F9F9" stroke-width="10" d="M36.975 5.037l4.857 2.255 13.063 6.064 13.062 6.064 3.028 1.405c-.168 17.685-1.8 28.545-5.99 37.422-4.69 9.939-12.968 17.548-26.995 24.24-14.027-6.692-22.306-14.301-26.996-24.24-4.189-8.877-5.821-19.737-5.99-37.422L38 5.513l-1.025-.476z"/>
<path stroke="#C3C0C7" stroke-width="6" d="M37.39 3.025a191742500.733 191742500.733 0 0 1 18.347 8.517l13.062 6.064 4.196 1.947c-.102 18.718-1.747 30.131-6.19 39.548-5 10.593-13.865 18.622-28.805 25.597-14.94-6.975-23.805-15.004-28.805-25.597-4.443-9.417-6.088-20.83-6.19-39.548L38 3.308l-.61-.283z"/>
</g>
<path fill="#F9F9F9" d="M26 51.001V56h24v-4.999C50 46.334 42.002 44 38 44c-3.998 0-12 2.334-12 7.001zM38.009 29A6.008 6.008 0 0 0 32 35.001C32 38.312 34.692 41 38.009 41A5.99 5.99 0 0 0 44 35.001 5.991 5.991 0 0 0 38.009 29"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#A5A1AC" fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM7 7h2v5H7V7zm0-3h2v2H7V4z"/>
</svg>

After

Width:  |  Height:  |  Size: 238 B

View File

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="78" height="76" viewBox="0 0 78 76">
<defs>
<path id="a" d="M35 0L0 15.235C0 45.705 4.255 62.941 35 76c30.745-13.059 35-30.294 35-60.765L35 0z"/>
<rect id="b" width="70" height="24" y="42" rx="12"/>
</defs>
<g fill="none" fill-rule="evenodd">
<g transform="translate(4)">
<use fill="#E1E0E3" xlink:href="#a"/>
<path stroke="#A5A1AC" stroke-width="10" d="M34.03 5.031l4.443 1.934 12.031 5.237 12.032 5.237 2.445 1.065c-.183 14.83-1.676 23.96-5.418 31.395C55.326 58.32 47.803 64.81 35 70.545 22.197 64.81 14.674 58.32 10.437 49.9c-3.742-7.435-5.235-16.565-5.418-31.395L35 5.454l-.97-.423z"/>
<path stroke="#C3C0C7" stroke-width="6" d="M34.421 3.02a506596810.815 506596810.815 0 0 0 16.882 7.348l12.03 5.237 3.66 1.593c-.111 15.9-1.621 25.609-5.643 33.6C56.795 59.848 48.69 66.734 35 72.733c-13.691-6-21.795-12.885-26.35-21.935-4.022-7.991-5.532-17.7-5.643-33.6L35 3.272l-.579-.252z"/>
</g>
<g transform="translate(4)">
<use fill="#FFF" xlink:href="#b"/>
<rect width="74" height="28" x="-2" y="40" stroke="#A5A1AC" stroke-width="4" rx="14"/>
</g>
<path fill="#A5A1AC" d="M31 30.667V34h16v-3.333C47 27.557 41.668 26 39 26c-2.666 0-8 1.556-8 4.667zM39.006 16A4.005 4.005 0 0 0 35 20c0 2.208 1.795 4 4.006 4A3.993 3.993 0 0 0 43 20c0-2.207-1.781-4-3.994-4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -19,4 +19,3 @@
@import './dropdown';
@import './popover';
@import './item';

View File

@@ -15,14 +15,18 @@ nav.navbar.navbar-inverse.fixed-top.navbar-toggleable-sm
router-link.dropdown-item(:to="{name: 'stable'}") {{ $t('stable') }}
router-link.nav-item(tag="li", :to="{name: 'market'}", exact)
a.nav-link(v-once) {{ $t('market') }}
router-link.nav-item.dropdown(tag="li", :to="{name: 'tavern'}", :class="{'active': $route.path.startsWith('/guilds')}")
a.nav-link(v-once) {{ $t('guilds') }}
.dropdown-menu
router-link.dropdown-item(:to="{name: 'tavern'}") {{ $t('tavern') }}
router-link.dropdown-item(:to="{name: 'myGuilds'}") {{ $t('myGuilds') }}
router-link.dropdown-item(:to="{name: 'guildsDiscovery'}") {{ $t('guildsDiscovery') }}
router-link.nav-item.dropdown(tag="li", :to="{name: 'tavern'}", :class="{'active': $route.path.startsWith('/social')}")
a.nav-link(v-once) {{ $t('social') }}
.dropdown-menu
router-link.dropdown-item(:to="{name: 'tavern'}") {{ $t('tavern') }}
router-link.dropdown-item(:to="{name: 'inbox'}") {{ $t('inbox') }}
router-link.dropdown-item(:to="{name: 'challenges'}") {{ $t('challenges') }}
router-link.dropdown-item(:to="{name: 'party'}") {{ $t('party') }}
router-link.dropdown-item(:to="{name: 'guildsDiscovery'}") {{ $t('guilds') }}
router-link.nav-item.dropdown(tag="li", to="/help", :class="{'active': $route.path.startsWith('/help')}")
a.nav-link(v-once) {{ $t('help') }}
.dropdown-menu

View File

@@ -0,0 +1,104 @@
<template lang="pug">
.row
sidebar(@search="updateSearch", @filter="updateFilters")
.col-10.standard-page
.clearfix
h1.page-header.float-left(v-once) {{ $t('publicGuilds') }}
.float-right
span.dropdown-label {{ $t('sortBy') }}
b-dropdown(:text="$t('sort')", right=true)
b-dropdown-item(v-for='sortOption in sortOptions', @click='sort(sortOption.value)') {{sortOption.text}}
.col-md-12
public-guild-item(v-for="guild in filteredGuilds", :key='guild._id', :guild="guild", :display-leave='true')
mugen-scroll(
:handler="fetchGuilds",
:should-handle="!loading && !this.hasLoadedAllGuilds",
:handle-on-mount="false",
v-show="loading",
)
span(v-once) {{ $t('loading') }}
</template>
<style>
.sort-select {
margin: 2em;
}
</style>
<script>
import MugenScroll from 'vue-mugen-scroll';
import PublicGuildItem from './publicGuildItem';
import Sidebar from './sidebar';
import groupUtilities from 'client/mixins/groupsUtilities';
import bFormSelect from 'bootstrap-vue/lib/components/form-select';
import bDropdown from 'bootstrap-vue/lib/components/dropdown';
import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item';
export default {
mixins: [groupUtilities],
components: { PublicGuildItem, MugenScroll, Sidebar, bFormSelect, bDropdown, bDropdownItem },
data () {
return {
loading: false,
hasLoadedAllGuilds: false,
lastPageLoaded: 0,
search: '',
filters: {},
sort: 'none',
sortOptions: [
{
text: this.$t('none'),
value: 'none',
},
{
text: this.$t('memberCount'),
value: 'member_count',
},
{
text: this.$t('recentActivity'),
value: 'recent_activity',
},
],
guilds: [],
};
},
created () {
if (!this.$store.state.publicGuilds) this.fetchGuilds();
},
computed: {
filteredGuilds () {
let search = this.search;
let filters = this.filters;
let user = this.$store.state.user.data;
let filterGuild = this.filterGuild;
// @TODO: Move this to the server
return this.guilds.filter((guild) => {
return filterGuild(guild, filters, search, user);
});
},
},
methods: {
updateSearch (eventData) {
this.search = eventData.searchTerm;
},
updateFilters (eventData) {
this.filters = eventData;
},
async fetchGuilds () {
// We have the data cached
if (this.lastPageLoaded === 0 && this.guilds.length > 0) return;
this.loading = true;
let guilds = await this.$store.dispatch('guilds:getPublicGuilds', {page: this.lastPageLoaded});
if (guilds.length === 0) this.hasLoadedAllGuilds = true;
this.guilds.push(...guilds);
this.lastPageLoaded++;
this.loading = false;
},
},
};
</script>

View File

@@ -0,0 +1,340 @@
<template lang="pug">
b-modal#guild-form(:title="title", :hide-footer="true")
form(@submit.stop.prevent="submit")
.form-group
label
strong(v-once) {{$t('name')}}*
b-form-input(type="text", placeholder="$t('newGuildPlaceHolder')", v-model="newGuild.name")
.form-group(v-if='newGuild.id')
label
strong(v-once) {{$t('guildLeader')}}*
b-form-select(v-model="newGuild.newLeader" :options="members")
.form-group
label
strong(v-once) {{$t('privacySettings')}}*
br
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", v-model="newGuild.onlyLeaderCreatesChallenges")
span.custom-control-indicator
span.custom-control-description(v-once) {{ $t('onlyLeaderCreatesChallenges') }}
b-tooltip.icon(:content="$t('privateDescription')")
img(src='~assets/guilds/information.svg')
br
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", v-model="newGuild.guildLeaderCantBeMessaged")
span.custom-control-indicator
span.custom-control-description(v-once) {{ $t('guildLeaderCantBeMessaged') }}
br
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", v-model="newGuild.privateGuild")
span.custom-control-indicator
span.custom-control-description(v-once) {{ $t('privateGuild') }}
b-tooltip.icon(:content="$t('privateDescription')")
img(src='~assets/guilds/information.svg')
br
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", v-model="newGuild.allowGuildInvationsFromNonMembers")
span.custom-control-indicator
span.custom-control-description(v-once) {{ $t('allowGuildInvationsFromNonMembers') }}
.form-group
label
strong(v-once) {{$t('description')}}*
div.description-count {{charactersRemaining}} {{ $t('charactersRemaining') }}
b-form-input(type="text", textarea :placeholder="$t('guildDescriptionPlaceHolder')", v-model="newGuild.description")
.form-group(v-if='newGuild.id')
label
strong(v-once) {{$t('guildInformation')}}*
b-form-input(type="text", textarea :placeholder="$t('guildInformationPlaceHolder')", v-model="newGuild.guildInformation")
.form-group(style='position: relative;')
label
strong(v-once) {{$t('categories')}}*
div.category-wrap(@click.prevent="toggleCategorySelect")
span.category-select(v-if='newGuild.categories.length === 0') {{$t('none')}}
.category-label(v-for='category in newGuild.categories') {{$t(categoriesHashByKey[category])}}
.category-box(v-if="showCategorySelect")
.form-check(
v-for="group in categoryOptions",
:key="group.key",
)
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", :value='group.key' v-model="newGuild.categories")
span.custom-control-indicator
span.custom-control-description(v-once) {{ $t(group.label) }}
button.btn.btn-primary(@click.prevent="toggleCategorySelect") {{$t('close')}}
.form-group.text-center
div.item-with-icon
img(src="~assets/guilds/green-gem.svg")
span.count 4
button.btn.btn-primary.btn-md(v-if='!newGuild.id', :disabled='!newGuild.name || !newGuild.description') {{ $t('createGuild') }}
button.btn.btn-primary.btn-md(v-if='newGuild.id', :disabled='!newGuild.name || !newGuild.description') {{ $t('updateGuild') }}
.gem-description(v-once) {{ $t('guildGemCostInfo') }}
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
textarea {
height: 150px;
}
.description-count, .gem-description {
font-size: 12px;
line-height: 1.33;
text-align: center;
color: $gray-200;
}
.description-count {
text-align: right;
}
.gem-description {
margin-top: 1em;
}
.category-box {
padding: 1em;
max-width: 400px;
position: absolute;
top: -480px;
padding: 2em;
border-radius: 2px;
background-color: $white;
box-shadow: 0 2px 2px 0 rgba($black, 0.15), 0 1px 4px 0 rgba($black, 0.1);
}
.category-label {
min-width: 100px;
border-radius: 100px;
background-color: $gray-600;
padding: .5em;
display: inline-block;
margin-right: .5em;
font-size: 12px;
font-weight: 500;
line-height: 1.33;
text-align: center;
color: $gray-300;
}
.item-with-icon {
display: inline-block;
img {
height: 20px;
margin-right: .5em;
}
.count {
font-size: 14px;
font-weight: bold;
margin-right: 1em;
color: $green-10;
}
}
.description-count {
margin-top: 1em;
}
.category-select {
border-radius: 2px;
background-color: $white;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
padding: 1em;
}
.category-select:hover {
cursor: pointer;
}
.category-wrap {
margin-top: .5em;
}
.icon {
margin-left: .5em;
display: inline-block;
}
</style>
<script>
import bModal from 'bootstrap-vue/lib/components/modal';
import bBtn from 'bootstrap-vue/lib/components/button';
import bFormInput from 'bootstrap-vue/lib/components/form-input';
import bFormCheckbox from 'bootstrap-vue/lib/components/form-checkbox';
import bFormSelect from 'bootstrap-vue/lib/components/form-select';
import bTooltip from 'bootstrap-vue/lib/components/tooltip';
export default {
components: {
bModal,
bBtn,
bFormInput,
bFormCheckbox,
bFormSelect,
bTooltip,
},
data () {
let data = {
newGuild: {
id: '',
name: '',
type: 'guild',
privacy: 'private',
description: '',
guildInformation: '',
categories: [],
onlyLeaderCreatesChallenges: true,
guildLeaderCantBeMessaged: true,
privateGuild: true,
allowGuildInvationsFromNonMembers: true,
newLeader: '',
},
categoryOptions: [
{
label: 'animals',
key: 'animals',
},
{
label: 'artDesign',
key: 'art_design',
},
{
label: 'booksWriting',
key: 'books_writing',
},
{
label: 'comicsHobbies',
key: 'comics_hobbies',
},
{
label: 'diyCrafts',
key: 'diy_crafts',
},
{
label: 'education',
key: 'education',
},
{
label: 'foodCooking',
key: 'food_cooking',
},
{
label: 'healthFitness',
key: 'health_fitness',
},
{
label: 'music',
key: 'music',
},
{
label: 'relationship',
key: 'relationship',
},
{
label: 'scienceTech',
key: 'science_tech ',
},
],
showCategorySelect: false,
members: ['one', 'two'],
};
let hashedCategories = {};
data.categoryOptions.forEach((category) => {
hashedCategories[category.key] = category.label;
});
data.categoriesHashByKey = hashedCategories;
return data;
},
mounted () {
this.$root.$on('shown::modal', () => {
let editingGroup = this.$store.state.editingGroup;
if (!editingGroup) return;
this.newGuild.name = editingGroup.name;
this.newGuild.type = editingGroup.type;
this.newGuild.privacy = editingGroup.privacy;
if (editingGroup.description) this.newGuild.description = editingGroup.description;
this.newGuild.id = editingGroup._id;
});
},
computed: {
charactersRemaining () {
return 500 - this.newGuild.description.length;
},
title () {
if (!this.newGuild.id) return this.$t('createGuild');
return this.$t('updateGuild');
},
},
methods: {
toggleCategorySelect () {
this.showCategorySelect = !this.showCategorySelect;
},
async submit () {
if (this.$store.state.user.data.balance < 1 && !this.newGuild.id) {
// @TODO: Add proper notifications
alert('Not enough gems');
return;
// @TODO return $rootScope.openModal('buyGems', {track:"Gems > Create Group"});
}
if (!this.newGuild.name || !this.newGuild.description) {
// @TODO: Add proper notifications
alert('Enter a name and description');
return;
}
if (this.newGuild.description.length > 500) {
// @TODO: Add proper notifications
alert('Description is too long');
return;
}
// @TODO: Add proper notifications
if (!confirm(this.$t('confirmGuild'))) return;
if (!this.newGuild.privateGuild) {
this.newGuild.privacy = 'public';
}
if (!this.newGuild.onlyLeaderCreatesChallenges) {
this.newGuild.leaderOnly = {
challenges: true,
};
}
if (this.newGuild.id) {
await this.$store.dispatch('guilds:update', {group: this.newGuild});
} else {
await this.$store.dispatch('guilds:create', {group: this.newGuild});
}
this.$store.state.editingGroup = {};
this.newGuild = {
name: '',
type: 'guild',
privacy: 'private',
description: '',
categories: [],
onlyLeaderCreatesChallenges: true,
guildLeaderCantBeMessaged: true,
privateGuild: true,
allowGuildInvationsFromNonMembers: true,
};
},
},
};
</script>

View File

@@ -0,0 +1,317 @@
<template lang="pug">
// TODO this is necessary until we have a way to wait for data to be loaded from the server
.row(v-if="guild")
.clearfix.col-8
.row
.col-6
.float-left
h2 {{guild.name}}
strong.float-left(v-once) {{$t('groupLeader')}}
span.float-left : {{guild.leader.profile.name}}
.col-6
.float-right
.row.icon-row
.col-6
img.icon.shield(src="~assets/guilds/gold-guild-badge.svg")
span.number {{guild.memberCount}}
div(v-once) {{ $t('guildMembers') }}
.col-6
.item-with-icon
img.icon.gem(src="~assets/header/png/gem@3x.png")
span.number {{guild.memberCount}}
div(v-once) {{ $t('guildBank') }}
.row.chat-row
.col-12
h3(v-once) {{ $t('chat') }}
textarea(placeholder="$('chatPlaceHolder')")
button.btn.btn-secondary.send-chat.float-right(v-once) {{ $t('send') }}
.hr
.hr-middle(v-once) {{ $t('today') }}
.row
.col-md-2
img.icon(src="~assets/chat/like.svg")
.col-md-10
.card(v-for="msg in guild.chat", :key="msg.id")
.card-block
h3.leader Character name
span 2 hours ago
.clearfix
strong.float-left {{msg.user}}
.float-right {{msg.timestamp}}
.text {{msg.text}}
hr
span.action(v-once)
img.icon(src="~assets/chat/like.svg")
| {{$t('like')}}
span.action(v-once)
img.icon(src="~assets/chat/copy.svg")
| {{$t('copyAsTodo')}}
span.action(v-once)
img.icon(src="~assets/chat/report.svg")
| {{$t('report')}}
span.action(v-once)
img.icon(src="~assets/chat/delete.svg")
| {{$t('delete')}}
span.action.float-right
img.icon(src="~assets/chat/liked.svg")
| +3
.col-md-4.sidebar
.guild-background.row
.col-6
p Image here
.col-6
members-modal(:group='guild')
br
button.btn.btn-primary(v-once) {{$t('joinGuild')}}
br
button.btn.float-left(:class="[isMember ? 'btn-danger' : 'btn-success']") {{ isMember ? $t('leave') : $t('join') }}
br
button.btn.btn-primary(v-once) {{$t('inviteToGuild')}}
br
button.btn.btn-primary(v-once) {{$t('messageGuildLeader')}}
br
button.btn.btn-primary(v-once) {{$t('donateGems')}}
br
button.btn.btn-primary(b-btn, @click="updateGuild", v-once) {{ $t('updateGuild') }}
div
h3(v-once) {{ $t('description') }}
p(v-once) {{ guild.description }}
p Life hacks are tricks, shortcuts, or methods that help increase productivity, efficiency, health, and so on. Generally, they get you to a better state of life. Life hacking is the process of utilizing and implementing these secrets. And, in this guild, we want to help everyone discover these improved ways of doing things.
div
h3(v-once) {{$t('guildInformation')}}
h4 Welcome
p Below are some resources that some members might find useful. Consider checking them out before posting any questions, as they just might help answer some of them! Feel free to share your life hacks in the guild chat, or ask any questions that you might have. Please peruse at your leisure, and remember: this guild is meant to help guide you in the right direction. Only you will know what works best for you.
div
h3 Challenges
.card
h4 Challenge
.row
.col-8
p Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla scelerisque ultrices libero.
.col-4
.row
.col-md-12
span Tag
span 100
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.sidebar {
background-color: $gray-600;
}
.card {
margin: 2em 0;
padding: 1em;
h3.leader {
color: $purple-200;
}
.text {
font-size: 16px;
line-height: 1.43;
color: $gray-50;
}
}
.guild-background {
background-image: linear-gradient(to bottom, rgba($gray-600, 0), $gray-600);
height: 300px;
}
textarea {
height: 150px;
width: 100%;
border-radius: 2px;
background-color: $white;
border: solid 1px $gray-400;
font-size: 16px;
font-style: italic;
line-height: 1.43;
color: $gray-300;
padding: .5em;
}
.icon.shield, .icon.gem {
width: 40px;
margin-right: 1em;
}
.icon-row {
width: 200px;
margin-top: 3em;
margin-right: 3em;
.number {
font-size: 22px;
font-weight: bold;
}
}
.chat-row {
margin-top: 2em;
.send-chat {
margin-top: -3.5em;
z-index: 10;
position: relative;
margin-right: 1em;
}
}
.hr {
width: 100%;
height: 20px;
border-bottom: 1px solid $gray-500;
text-align: center;
margin: 2em 0;
}
.hr-middle {
font-size: 16px;
font-weight: bold;
font-family: 'Roboto Condensed';
line-height: 1.5;
text-align: center;
color: $gray-200;
background-color: $gray-700;
padding: .2em;
margin-top: .2em;
display: inline-block;
width: 100px;
}
span.action {
font-size: 14px;
line-height: 1.33;
color: $gray-200;
font-weight: 500;
margin-right: 1em;
}
span.action .icon {
margin-right: .3em;
}
</style>
<script>
import groupUtilities from 'client/mixins/groupsUtilities';
import { mapState } from 'client/libs/store';
import membersModal from './membersModal';
export default {
mixins: [groupUtilities],
props: ['guildId'],
components: {
membersModal,
},
data () {
return {
guild: null,
};
},
computed: {
...mapState({user: 'user.data'}),
isMember () {
return this.isMemberOfGroup(this.user, this.guild);
},
isMemberOfPendingQuest () {
let userid = this.user._id;
let group = this.guild;
if (!group.quest || !group.quest.members) return false;
if (group.quest.active) return false; // quest is started, not pending
return userid in group.quest.members && group.quest.members[userid] !== false;
},
isMemberOfRunningQuest () {
let userid = this.user._id;
let group = this.guild;
if (!group.quest || !group.quest.members) return false;
if (!group.quest.active) return false; // quest is pending, not started
return group.quest.members[userid];
},
memberProfileName (memberId) {
let foundMember = find(this.group.members, function findMember (member) {
return member._id === memberId;
});
return foundMember.profile.name;
},
isManager (memberId, group) {
return Boolean(group.managers[memberId]);
},
userCanApprove (userId, group) {
if (!group) return false;
let leader = group.leader._id === userId;
let userIsManager = Boolean(group.managers[userId]);
return leader || userIsManager;
},
},
created () {
this.fetchGuild();
},
watch: {
// call again the method if the route changes (when this route is already active)
$route: 'fetchGuild',
},
methods: {
updateGuild () {
this.$store.state.editingGroup = this.guild;
this.$root.$emit('show::modal', 'guild-form');
},
async fetchGuild () {
this.guild = await this.$store.dispatch('guilds:getGroup', {groupId: this.guildId});
this.guild.chat = [
{
text: '@CharacterName Vestibulum ultricies, lorem non bibendum consequat, nisl lacus semper nulla, hendrerit dignissim ipsum erat eu odio. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla at aliquet urna. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla non est ut nisl interdum tincidunt in eu dui. Proin condimentum a.',
},
];
},
editGroup () {
// @TODO: Open up model
},
save () {
let newLeader = this.group._newLeader && this.group._newLeader._id;
if (newLeader) {
this.group.leader = newLeader;
}
// Groups.Group.update(group);
},
deleteAllMessages () {
if (confirm(this.$t('confirmDeleteAllMessages'))) {
// User.clearPMs();
}
},
// inviteOrStartParty (group) {
// Analytics.track({'hitType':'event','eventCategory':'button','eventAction':'click','eventLabel':'Invite Friends'});
// var sendInviteText = window.env.t('sendInvitations');
// if (group.type !== 'party' && group.type !== 'guild') {
// $location.path("/options/groups/party");
// return console.log('Invalid group type.')
// }
//
// if (group.purchased && group.purchased.plan && group.purchased.plan.customerId) sendInviteText += window.env.t('groupAdditionalUserCost');
//
// group.sendInviteText = sendInviteText;
//
// $rootScope.openModal('invite-' + group.type, {
// controller:'InviteToGroupCtrl',
// resolve: {
// injectedGroup: function() {
// return group;
// },
// },
// });
// },
},
};
</script>

View File

@@ -0,0 +1,26 @@
<template lang="pug">
.row
secondary-menu.col-12
router-link.nav-link(:to="{name: 'tavern'}", exact, :class="{'active': $route.name === 'tavern'}") {{ $t('tavern') }}
router-link.nav-link(:to="{name: 'myGuilds'}", :class="{'active': $route.name === 'myGuilds'}") {{ $t('myGuilds') }}
router-link.nav-link(:to="{name: 'guildsDiscovery'}", :class="{'active': $route.name === 'guildsDiscovery'}") {{ $t('guildsDiscovery') }}
.col-12
router-view
group-form-modal
</template>
<!-- .col-md-2
button.btn.btn-primary(b-btn, @click="$root.$emit('show::modal','guild-form')") {{ $t('createGuild') }} -->
<script>
import groupFormModal from './groupFormModal';
import SecondaryMenu from 'client/components/secondaryMenu';
export default {
components: {
groupFormModal,
SecondaryMenu,
},
};
</script>

View File

@@ -0,0 +1,107 @@
<template lang="pug">
div
button.btn.btn-primary(b-btn, @click="$root.$emit('show::modal','members-modal')") {{ $t('viewMembers') }}
b-modal#members-modal(:title="$t('createGuild')")
ul(v-for='member in members', :key='member')
li(@click='clickMember') {{member}}
button(@click='removeMember(member)', v-once) {{$t('remove')}}
button(@click='quickReply(member)', v-once) {{$t('message')}}
button(@click='addManager(member)', v-once) {{$t('addManager')}}
button(@click='removeManager(member)', v-once) {{$t('addManager')}}
b-modal#remove-member(:title="$t('confirmRemoveMember')")
button(@click='confirmRemoveMember(member)', v-once) {{$t('remove')}}
b-modal#private-message(:title="$t('confirmRemoveMember')")
button(@click='confirmRemoveMember(member)', v-once) {{$t('remove')}}
</template>
<script>
import bModal from 'bootstrap-vue/lib/components/modal';
export default {
props: ['group'],
components: {
bModal,
},
data () {
return {
members: ['one', 'two'],
memberToRemove: '',
};
},
methods: {
getMembers () {
// We should get members here via store if they are not loaded
},
clickMember (uid, forceShow) {
let user = this.$store.state.user.data;
if (user._id === uid && !forceShow) {
if (this.$route.name === 'tasks') {
this.$route.router.go('options.profile.avatar');
return;
}
this.$route.router.go('tasks');
return;
}
// $root.$emit('show::modal','members-modal')
// We need the member information up top here, but then we pass it down to the modal controller
// down below. Better way of handling this?
// Members.selectMember(uid)
// .then(function () {
// $rootScope.openModal('member', {controller: 'MemberModalCtrl', windowClass: 'profile-modal', size: 'lg'});
// });
},
removeMember (member) {
this.memberToRemove = member;
this.$root.$emit('show::modal', 'remove-member');
},
confirmRemoveMember (confirmation) {
if (!confirmation) {
this.memberToRemove = '';
return;
}
// Groups.Group.removeMember(
// $scope.removeMemberData.group._id,
// $scope.removeMemberData.member._id,
// $scope.removeMemberData.message
// ).then(function (response) {
// if($scope.removeMemberData.isMember){
// _.pull($scope.removeMemberData.group.members, $scope.removeMemberData.member);
// }else{
// _.pull($scope.removeMemberData.group.invites, $scope.removeMemberData.member);
// }
//
// $scope.removeMemberData = undefined;
// });
},
quickReply (uid) {
this.memberToReply = uid;
this.$root.$emit('show::modal', 'private-message');
// Members.selectMember(uid)
// .then(function (response) {
// $rootScope.openModal('private-message', {controller: 'MemberModalCtrl'});
// });
},
addManager () {
// Groups.Group.addManager(this.group._id, this.group._newManager)
// .then(function (response) {
// this.group._newManager = '';
// this.group.managers = response.data.data.managers;
// });
},
removeManager (memberId) {
this.memberToReply = memberId;
// Groups.Group.removeManager(this.group._id, memberId)
// .then(function (response) {
// this.group._newManager = '';
// this.group.managers = response.data.data.managers;
// });
},
},
};
</script>

View File

@@ -0,0 +1,117 @@
<template lang="pug">
.row
sidebar(v-on:search="updateSearch", v-on:filter="updateFilters")
.col-10.no-guilds.standard-page(v-if='filteredGuilds.length === 0')
.no-guilds-wrapper
img(src='~assets/guilds/grey-badge.svg')
h2 {{$t('noGuildsTitle')}}
p {{$t('noGuildsParagraph1')}}
p {{$t('noGuildsParagraph2')}}
span(v-if='loading') {{ $t('loading') }}
.col-10.standard-page(v-if='filteredGuilds.length > 0')
.row
.col-md-12
h1.page-header.float-left(v-once) {{ $t('myGuilds') }}
.float-right
span.dropdown-label {{ $t('sortBy') }}
b-dropdown(:text="$t('sort')", right=true)
b-dropdown-item(v-for='sortOption in sortOptions', @click='sort(sortOption.value)') {{sortOption.text}}
.row
.col-md-12
public-guild-item(v-for="guild in filteredGuilds", :key='guild._id', :guild="guild", :display-gem-bank='true')
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.sort-select {
margin: 2em;
}
.no-guilds {
text-align: center;
color: $gray-200;
margin-top: 15em;
p {
font-size: 14px;
line-height: 1.43;
}
.no-guilds-wrapper {
width: 400px;
margin: 0 auto;
}
}
</style>
<script>
import { mapState } from 'client/libs/store';
import groupUtilities from 'client/mixins/groupsUtilities';
import MugenScroll from 'vue-mugen-scroll';
import bFormSelect from 'bootstrap-vue/lib/components/form-select';
import bDropdown from 'bootstrap-vue/lib/components/dropdown';
import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item';
import PublicGuildItem from './publicGuildItem';
import Sidebar from './sidebar';
export default {
mixins: [groupUtilities],
components: { PublicGuildItem, MugenScroll, Sidebar, bFormSelect, bDropdown, bDropdownItem },
data () {
return {
loading: false,
search: '',
filters: {},
sort: 'none',
sortOptions: [
{
text: this.$t('none'),
value: 'none',
},
{
text: this.$t('memberCount'),
value: 'member_count',
},
{
text: this.$t('recentActivity'),
value: 'recent_activity',
},
],
};
},
created () {
this.fetchGuilds();
},
computed: {
...mapState({
guilds: 'myGuilds',
}),
filteredGuilds () {
let search = this.search;
let filters = this.filters;
let user = this.$store.state.user.data;
let filterGuild = this.filterGuild;
return this.guilds.filter((guild) => {
return filterGuild(guild, filters, search, user);
});
},
},
methods: {
updateSearch (eventData) {
this.search = eventData.searchTerm;
},
updateFilters (eventData) {
this.filters = eventData;
},
async fetchGuilds () {
this.loading = true;
await this.$store.dispatch('guilds:getMyGuilds');
this.loading = false;
},
},
};
</script>

View File

@@ -0,0 +1,144 @@
<template lang="pug">
.card
.card-block
.row
.col-md-2
img.icon.shield(src="~assets/guilds/gold-guild-badge.svg")
.member-count {{guild.memberCount}}
.col-md-10
.row
.col-md-8
router-link(:to="{ name: 'guild', params: { guildId: guild._id } }")
h3 {{ guild.name }}
p {{ guild.description }}
.col-md-2.cta-container
button.btn.btn-danger(v-if='isMember && displayLeave' @click='leave()', v-once) {{ $t('leave') }}
button.btn.btn-success(v-if='!isMember' @click='join()', v-once) {{ $t('join') }}
div.item-with-icon(v-if='displayGemBank')
img(src="~assets/guilds/green-gem.svg")
span.count {{ guild.balance }}
div.guild-bank(v-if='displayGemBank', v-once) {{$t('guildBank')}}
.row
.col-md-12
.category-label(v-for="category in guild.categories")
| {{category}}
span.recommend-text Suggested because youre new to Habitica.
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.card {
height: 260px;
border-radius: 4px;
background-color: $white;
box-shadow: 0 2px 2px 0 rgba($black, 0.15), 0 1px 4px 0 rgba($black, 0.1);
margin-bottom: 1rem;
.category-label {
min-width: 100px;
border-radius: 100px;
background-color: $gray-600;
padding: .5em;
display: inline-block;
margin-right: .5em;
font-size: 12px;
font-weight: 500;
line-height: 1.33;
text-align: center;
color: $gray-300;
}
.recommend-text {
font-size: 12px;
font-style: italic;
line-height: 2;
color: $gray-300;
}
.cta-container {
margin: 0 auto;
margin-top: 4em;
}
.shield {
width: 70px;
height: 76px;
margin: auto;
margin: 4em auto;
display: block;
background-size: cover;
width: 100%;
height: 100px;
}
.item-with-icon {
img {
height: 37px;
}
.count {
font-size: 20px;
height: 37px;
width: 37px;
margin-left: .2em;
}
}
.guild-bank {
font-size: 12px;
line-height: 1.33;
color: $gray-300;
}
.member-count {
position: relative;
top: -3.6em;
left: -.1em;
font-size: 28px;
font-weight: bold;
font-family: 'Roboto Condensed';
line-height: 1.2;
text-align: center;
color: #b36213;
}
}
</style>
<script>
import { mapState } from 'client/libs/store';
import groupUtilities from 'client/mixins/groupsUtilities';
import findIndex from 'lodash/findIndex';
export default {
mixins: [groupUtilities],
props: ['guild', 'displayLeave', 'displayGemBank'],
computed: {
...mapState({user: 'user.data'}),
isMember () {
return this.isMemberOfGroup(this.user, this.guild);
},
},
methods: {
async join () {
// @TODO: This needs to be in the notifications where users will now accept invites
if (this.guild.cancelledPlan && !confirm(window.env.t('aboutToJoinCancelledGroupPlan'))) {
return;
}
await this.$store.dispatch('guilds:join', {guildId: this.guild._id, type: 'myGuilds'});
},
async leave () {
// @TODO: ask about challenges when we add challenges
await this.$store.dispatch('guilds:leave', {guildId: this.guild._id, type: 'myGuilds'});
},
async reject (invitationToReject) {
// @TODO: This needs to be in the notifications where users will now accept invites
let index = findIndex(this.user.invitations.guilds, function findInviteIndex (invite) {
return invite.id === invitationToReject.id;
});
this.user.invitations.guilds = this.user.invitations.guilds.splice(0, index);
await this.$store.dispatch('guilds:rejectInvite', {guildId: invitationToReject.id});
},
},
};
</script>

View File

@@ -0,0 +1,152 @@
<template lang="pug">
.col-2.standard-sidebar
.form-group
input.form-control.search(type="text", :placeholder="$t('search')", v-model='searchTerm')
form
h3(v-once) {{ $t('filter') }}
.form-group
h5 Category
.form-check(
v-for="group in categoryOptions",
:key="group.key",
)
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", :value='group.key' v-model="categoryFilters")
span.custom-control-indicator
span.custom-control-description(v-once) {{ $t(group.label) }}
.form-group
h5 Role
.form-check(
v-for="group in roleOptions",
:key="group.key",
)
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", :value='group.key' v-model="roleFilters")
span.custom-control-indicator
span.custom-control-description(v-once) {{ $t(group.label) }}
.form-group
h5 Guild Size
.form-check(
v-for="group in guildSizeOptions",
:key="group.key",
)
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", :value='group.key' v-model="guildSizeFilters")
span.custom-control-indicator
span.custom-control-description(v-once) {{ $t(group.label) }}
</template>
<script>
import throttle from 'lodash/throttle';
export default {
data () {
return {
categoryFilters: [],
categoryOptions: [
{
label: 'habiticaOfficial',
key: 'official',
},
{
label: 'animals',
key: 'animals',
},
{
label: 'artDesign',
key: 'art_design',
},
{
label: 'booksWriting',
key: 'books_writing',
},
{
label: 'comicsHobbies',
key: 'comics_hobbies',
},
{
label: 'diyCrafts',
key: 'diy_crafts',
},
{
label: 'education',
key: 'education',
},
{
label: 'foodCooking',
key: 'food_cooking',
},
{
label: 'healthFitness',
key: 'health_fitness',
},
{
label: 'music',
key: 'music',
},
{
label: 'relationship',
key: 'relationship',
},
{
label: 'scienceTech',
key: 'science_tech ',
},
],
roleFilters: [],
roleOptions: [
{
label: 'guildLeader',
key: 'guild_leader',
},
{
label: 'member',
key: 'member',
},
],
guildSizeFilters: [],
guildSizeOptions: [
{
label: 'goldTier',
key: 'gold_tier',
},
{
label: 'silverTier',
key: 'silver_tier',
},
{
label: 'bronzeTier',
key: 'bronze_tier',
},
],
searchTerm: '',
};
},
watch: {
categoryFilters: function categoryFilters () {
this.emitFilters();
},
roleFilters: function roleFilters () {
this.emitFilters();
},
guildSizeFilters: function guildSizeFilters () {
this.emitFilters();
},
searchTerm: throttle(function searchTerm (newSearch) {
this.$emit('search', {
searchTerm: newSearch,
});
}, 250),
},
methods: {
emitFilters () {
this.$emit('filter', {
categories: this.categoryFilters,
roles: this.roleFilters,
guildSize: this.guildSizeFilters,
});
},
},
};
</script>

View File

@@ -1,8 +1,6 @@
<template lang="pug">
.row
h1.page-header.col-12 Tavern
// TODO Example code based on Semantic UI .ui.grid
.four.wide.column
.col-md-4
h2.ui.dividing.header SideMenu
.ui.card
@@ -153,7 +151,7 @@
ul
li Challenge 1
.twelve.wide.column
.col-md-8
h2.ui.dividing.header Tavern Chat
.ui.comments
@@ -172,7 +170,6 @@
span.date {{message.date}}
.text
| {{message.message}}
</template>
<script>

View File

@@ -1,79 +0,0 @@
<template lang="pug">
.row
.col-2.standard-sidebar
.form-group
input.form-control(type="text", :placeholder="$t('search')")
form
h3(v-once) {{ $t('filter') }}
.form-group
h5 Interests
.form-check
.label.form-check-label
input.form-check-input(type="checkbox")
span Habitica Official
.form-check
.label.form-check-label
input.form-check-input(type="checkbox")
span Nature
.form-check
.label.form-check-label
input.form-check-input(type="checkbox")
span Animals
.col-10.standard-page
h1.page-header(v-once) {{ $t('publicGuilds') }}
public-guild-item(v-for="guild in guilds", :key='guild._id', :guild="guild")
mugen-scroll(
:handler="fetchGuilds",
:should-handle="loading === false && hasLoadedAllGuilds === false",
:handle-on-mount="false",
v-show="hasLoadedAllGuilds === false",
)
span loading...
</template>
<script>
import axios from 'axios';
import MugenScroll from 'vue-mugen-scroll';
import PublicGuildItem from './publicGuildItem';
import { mapState } from 'client/libs/store';
export default {
components: { PublicGuildItem, MugenScroll },
data () {
return {
loading: false,
hasLoadedAllGuilds: false,
lastPageLoaded: 0,
guilds: [],
};
},
computed: {
...mapState({
GUILDS_PER_PAGE: 'constants.GUILDS_PER_PAGE',
}),
},
created () {
this.fetchGuilds();
},
methods: {
async fetchGuilds () {
this.loading = true;
let response = await axios.get('/api/v3/groups', {
params: {
type: 'publicGuilds',
paginate: true,
page: this.lastPageLoaded,
},
});
let guilds = response.data.data;
this.guilds.push(...guilds);
if (guilds.length < this.GUILDS_PER_PAGE) this.hasLoadedAllGuilds = true;
this.lastPageLoaded++;
this.loading = false;
},
},
};
</script>

View File

@@ -1,31 +0,0 @@
<template lang="pug">
.card
.card-block
.clearfix
router-link.float-left(:to="{ name: 'guild', params: { guildId: guild._id } }")
h3 {{ guild.name }}
button.btn.float-right(:class="[isMember ? 'btn-danger' : 'btn-success']") {{ isMember ? $t('leave') : $t('join') }}
p {{ guild.description }}
</template>
<style lang="scss" scoped>
.card {
margin-bottom: 1rem;
}
</style>
<script>
import { mapState } from 'client/libs/store';
import groupUtilities from 'client/mixins/groupsUtilities';
export default {
mixins: [groupUtilities],
props: ['guild'],
computed: {
...mapState({user: 'user.data'}),
isMember () {
return this.isMemberOfGroup(this.user, this.guild);
},
},
};
</script>

View File

@@ -1,68 +0,0 @@
<template lang="pug">
// TODO this is necessary until we have a way to wait for data to be loaded from the server
.row(v-if="guild")
.clearfix.col-12
.float-left
h1.page-header {{guild.name}}
strong.float-left {{$t('groupLeader')}}
span.float-left : {{guild.leader.profile.name}}
.float-right
.clearfix
h3.float-left
span.badge.badge-default {{guild.memberCount}}
| {{$t('members')}}
button.btn.float-left(:class="[isMember ? 'btn-danger' : 'btn-success']") {{ isMember ? $t('leave') : $t('join') }}
.col-5
h4(v-once) {{ $t('description') }}
p {{ guild.description }}
.col-7
h4(v-once) {{ $t('chat') }}
.card(v-for="msg in guild.chat", :key="msg.id")
.card-block
.clearfix
strong.float-left {{msg.user}}
.float-right {{msg.timestamp}}
.text {{msg.text}}
</template>
<style lang="scss" scoped>
.card {
margin-bottom: 1rem;
}
</style>
<script>
import axios from 'axios';
import groupUtilities from 'client/mixins/groupsUtilities';
import { mapState } from 'client/libs/store';
export default {
mixins: [groupUtilities],
props: ['guildId'],
data () {
return {
guild: null,
};
},
computed: {
...mapState({user: 'user.data'}),
isMember () {
return this.isMemberOfGroup(this.user, this.guild);
},
},
created () {
this.fetchGuild();
},
watch: {
// call again the method if the route changes (when this route is already active)
$route: 'fetchGuild',
},
methods: {
fetchGuild () {
axios.get(`/api/v3/groups/${this.guildId}`).then(response => {
this.guild = response.data.data;
});
},
},
};
</script>

View File

@@ -1,3 +1,5 @@
import intersection from 'lodash/intersection';
export default {
methods: {
isMemberOfGroup (user, group) {
@@ -16,5 +18,36 @@ export default {
return false;
},
isLeaderOfGroup (user, group) {
return user._id === group.leader._id;
},
filterGuild (group, filters, search, user) {
let passedSearch = true;
let hasCategories = true;
let isMember = true;
let isLeader = true;
if (search) {
passedSearch = group.name.toLowerCase().indexOf(search.toLowerCase()) >= 0;
}
if (filters.categories && filters.categories.length > 0) {
let intersectingCats = intersection(filters.categories, group.categories);
hasCategories = intersectingCats.length > 0;
}
let filteringRole = filters.roles && filters.roles.length > 0;
if (filteringRole && filters.roles.indexOf('member')) {
isMember = this.isMemberOfGroup(user, group);
}
if (filteringRole && filters.roles.indexOf('guild_leader')) {
isLeader = this.isLeaderOfGroup(user, group);
}
// @TODO: Tier filters
return passedSearch && hasCategories && isMember && isLeader;
},
},
};

View File

@@ -17,11 +17,15 @@ import StablePage from './components/inventory/stable';
// Social
import SocialContainer from './components/social/index';
import TavernPage from './components/social/tavern';
import InboxPage from './components/social/inbox/index';
import InboxConversationPage from './components/social/inbox/conversationPage';
import GuildsDiscoveryPage from './components/social/guilds/discovery/index';
import GuildPage from './components/social/guilds/guild';
// Guilds
import GuildIndex from './components/guilds/index';
import TavernPage from './components/guilds/tavern';
import MyGuilds from './components/guilds/myGuilds';
import GuildsDiscoveryPage from './components/guilds/discovery';
import GuildPage from './components/guilds/guild';
Vue.use(VueRouter);
@@ -46,11 +50,33 @@ export default new VueRouter({
],
},
{ name: 'market', path: '/market', component: Page },
{
path: '/guilds',
component: GuildIndex,
children: [
{ name: 'tavern', path: 'tavern', component: TavernPage },
{
name: 'myGuilds',
path: 'myGuilds',
component: MyGuilds,
},
{
name: 'guildsDiscovery',
path: 'discovery',
component: GuildsDiscoveryPage,
},
{
name: 'guild',
path: 'guild/:guildId',
component: GuildPage,
props: true,
},
],
},
{
path: '/social',
component: SocialContainer,
children: [
{ name: 'tavern', path: 'tavern', component: TavernPage },
{
path: 'inbox',
component: EmptyView,
@@ -69,23 +95,6 @@ export default new VueRouter({
},
{ name: 'challenges', path: 'challenges', component: Page },
{ name: 'party', path: 'party', component: Page },
{
path: 'guilds',
component: EmptyView,
children: [
{
name: 'guildsDiscovery',
path: 'discovery',
component: GuildsDiscoveryPage,
},
{
name: 'guild',
path: 'guild/:guildId',
component: GuildPage,
props: true,
},
],
},
],
},
{

View File

@@ -0,0 +1,162 @@
import axios from 'axios';
import omit from 'lodash/omit';
import findIndex from 'lodash/findIndex';
export async function getPublicGuilds (store, payload) {
let response = await axios.get('/api/v3/groups', {
params: {
type: 'publicGuilds',
paginate: true,
page: payload.page,
},
});
return response.data.data;
}
export async function getMyGuilds (store) {
let response = await axios.get('/api/v3/groups', {
params: {
type: 'privateGuilds',
},
});
let guilds = response.data.data;
store.state.myGuilds = guilds;
return response.data.data;
}
export async function getGroup (store, payload) {
let response = await axios.get(`/api/v3/groups/${payload.groupId}`);
// @TODO: should we store the active group for modifying?
// let guilds = response.data.data;
// store.state.myGuilds = guilds;
// @TODO: Populate wiht members, challenges, and invites
return response.data.data;
}
export async function join (store, payload) {
let response = await axios.post(`/api/v3/groups/${payload.guildId}/join`);
// @TODO: abstract for parties as well
store.state.user.data.guilds.push(payload.guildId);
if (payload.type === 'myGuilds') {
store.state.myGuilds.push(response.data.data);
}
return response.data.data;
}
export async function leave (store, payload) {
// @TODO: is the dafault for keepChallenges 'remain-in-challenges'
let data = {
keep: payload.keep,
keepChallenges: payload.keepChallenges,
};
let response = await axios.post(`/api/v3/groups/${payload.guildId}/leave`, data);
// @TODO: update for party
let index = store.state.user.data.guilds.indexOf(payload.guildId);
store.state.user.data.guilds.splice(index, 1);
if (payload.type === 'myGuilds') {
let guildIndex = findIndex(store.state.myGuilds, (guild) => {
return guild._id === payload.guildId;
});
store.state.myGuilds.splice(guildIndex, 1);
}
return response.data.data;
}
export async function create (store, payload) {
let response = await axios.post('/api/v3/groups/', payload.group);
let newGroup = response.data.data;
// @TODO: Add party
if (newGroup.privacy === 'public') {
store.state.publicGuilds.push(newGroup);
} else if (newGroup.privacy === 'private') {
store.state.myGuilds.push(newGroup);
}
return newGroup;
}
export async function update (store, payload) {
// Remove populated fields
let groupDetailsToSend = omit(payload.group, ['chat', 'challenges', 'members', 'invites']);
if (groupDetailsToSend.leader && groupDetailsToSend.leader._id) groupDetailsToSend.leader = groupDetailsToSend.leader._id;
let response = await axios.put(`/api/v3/groups/${payload.group.id}`, {
data: groupDetailsToSend,
});
let updatedGroup = response.data.data;
// @TODO: Replace old group
// store.state.publicGuilds.push(newGroup);
return updatedGroup;
}
export async function rejectInvite (store, payload) {
let response = await axios.post(`/api/v3/groups/${payload.groupId}/reject-invite`);
// @TODO: refresh or add guild
return response;
}
export async function removeMember (store, payload) {
let response = await axios.post(`/api/v3/groups/${payload.groupId}/removeMember/${payload.memberId}`, {
message: payload.message,
});
// @TODO: find guild and remove member
return response;
}
export async function invite (store, payload) {
let response = await axios.post(`/api/v3/groups/${payload.groupId}/invite`, {
uuids: payload.invitationDetails.uuids,
emails: payload.invitationDetails.emails,
});
// @TODO: find guild and add invites
return response;
}
export async function inviteToQuest (store, payload) {
let response = await axios.post(`/api/v3/groups/${payload.groupId}/quests/invite/${payload.key}`);
// @TODO: Any updates?
return response;
}
export async function addManager (store, payload) {
let response = await axios.post(`/api/v3/groups/${payload.groupId}/add-manager/`, {
managerId: payload.memberId,
});
// @TODO: Add managers to group or does the component handle?
return response;
}
export async function removeManager (store, payload) {
let response = await axios.post(`/api/v3/groups/${payload.groupId}/remove-manager/`, {
managerId: payload.memberId,
});
// @TODO: Add managers to group or does the component handle?
return response;
}

View File

@@ -3,6 +3,7 @@ import { flattenAndNamespace } from 'client/libs/store/helpers/internals';
import * as common from './common';
import * as user from './user';
import * as tasks from './tasks';
import * as guilds from './guilds';
// Actions should be named as 'actionName' and can be accessed as 'namespace:actionName'
// Example: fetch in user.js -> 'user:fetch'
@@ -11,6 +12,7 @@ const actions = flattenAndNamespace({
common,
user,
tasks,
guilds,
});
export default actions;

View File

@@ -17,6 +17,9 @@ export default function () {
title: 'Habitica',
user: asyncResourceFactory(),
tasks: asyncResourceFactory(), // user tasks
publicGuilds: [],
myGuilds: [],
editingGroup: {},
// content data, frozen to prevent Vue from modifying it since it's static and never changes
// TODO apply freezing to the entire codebase (the server) and not only to the client side?
// NOTE this takes about 10-15ms on a fast computer

View File

@@ -170,6 +170,5 @@
"hideQuickAllocation": "Hide stat allocation",
"quickAllocationLevelPopover": "Each level earns you one point to assign to an attribute of your choice. You can do so manually, or let the game decide for you using one of the Automatic Allocation options found in User -> Stats.",
"invalidAttribute": "\"<%= attr %>\" is not a valid attribute.",
"notEnoughAttrPoints": "You don't have enough attribute points.",
"gearNotOwned": "You do not own this item."
"notEnoughAttrPoints": "You don't have enough attribute points."
}

View File

@@ -1,13 +1,64 @@
{
"costumePopoverText": "Select \"Use Costume\" to equip items to your avatar without affecting the stats from your Battle Gear! This means that you can dress up your avatar in whatever outfit you like while still having your best Battle Gear equipped.",
"autoEquipPopoverText": "Select this option to automatically equip gear as soon as you purchase it.",
"showAllGearItems": "Show All <%= items %> <%= type %> Gear Items",
"showLessGearItems": "Show Less <%= type %> Gear Items",
"noGearItemsOfType": "You don't own any pieces of <%= type %>.",
"costumeDisabled": "You have disabled your costume.",
"newGuildPlaceHolder": "Enter your name",
"guildMembers": "Guild Members",
"guildBank": "Guild Bank",
"chatPlaceHolder": "Type your message to Guild members here",
"today": "Today",
"like": "Like",
"copyAsTodo": "Copy as To-Do",
"report": "Report",
"joinGuild": "Join Guild",
"inviteToGuild": "Invite to Guild",
"messageGuildLeader": "Message Guild Leader",
"donateGems": "Donate Gems",
"guildInformation": "Guild Information",
"updateGuild": "Update Guild",
"viewMembers": "View Members",
"items": "Items",
"sortBy": "Sort By",
"groupBy2": "Group By",
"quantity": "Quantity",
"AZ": "A-Z"
"AZ": "A-Z",
"sort": "Sort",
"memberCount": "Member Count",
"recentActivity": "Recent Activity",
"gearNotOwned": "You do not own this item.",
"showAllGearItems": "Show All <%= items %> <%= type %> Gear Items",
"showLessGearItems": "Show Less <%= type %> Gear Items",
"noGearItemsOfType": "You don't own any pieces of <%= type %>.",
"myGuilds": "My Guilds",
"guildsDiscovery": "Discover Guilds",
"habiticaOfficial": "Habitica Official",
"animals": "Animals",
"artDesign": "Art & Design",
"booksWriting": "Books & Writing",
"comicsHobbies": "Comics & Hobbies",
"diyCrafts": "DIY & Crafts",
"education": "Education",
"foodCooking": "Food & Cooking",
"healthFitness": "Health & Fitness",
"music": "Music",
"relationship": "Relationships",
"scienceTech": "Science & Technology",
"guildLeader": "Guild Leader",
"member": "Member",
"goldTier": "Gold Tier",
"silverTier": "Silver Tier",
"bronzeTier": "Bronze Tier",
"privacySettings": "Privacy Settings",
"onlyLeaderCreatesChallenges": "Only the Guild Leader can create Guild Challenges",
"guildLeaderCantBeMessaged": "Guild Leader can not be messaged directly",
"privateGuild": "Private Guild",
"allowGuildInvationsFromNonMembers": "Allow Guild invitations from non-members",
"charactersRemaining": "characters remaining",
"guildDescriptionPlaceHolder": "Write a short description advertising your Guild to other Habiticans. What is the main purpose of your Guild and why should people join it? Try to include useful keywords in the description so that Habiticans can easily find it when they search!",
"guildGemCostInfo": "A Gem cost promotes high quality Guilds and is transferred into your Guild's bank.",
"categories": "Categories",
"noGuildsTitle": "You arent a member of any Guilds.",
"noGuildsParagraph1": "Guilds are social groups created by other players that can offer you support, accountability, and encouraging chat.",
"noGuildsParagraph2": "Click the Discover tab to see recommended Guilds based on your interests, browse Habiticas public Guilds, or create your own Guild.",
"privateDescription": "A private Guild will not be displayed in Habiticas Guild directory. New members can be added by invitation only."
}