mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
Client: Guilds Discovery (#8529)
* wip: add guilds discovery page * add public guilds page * fix and add tests for the groups utilities mixin
This commit is contained in:
61
test/client/unit/specs/mixins/groupsUtilities.spec.js
Normal file
61
test/client/unit/specs/mixins/groupsUtilities.spec.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import groupsUtilities from 'client/mixins/groupsUtilities';
|
||||
import { TAVERN_ID } from 'common/script/constants';
|
||||
import Vue from 'vue';
|
||||
|
||||
describe('Groups Utilities Mixin', () => {
|
||||
let instance, user;
|
||||
|
||||
before(() => {
|
||||
instance = new Vue({
|
||||
mixins: [groupsUtilities],
|
||||
});
|
||||
|
||||
user = {
|
||||
_id: '123',
|
||||
party: {
|
||||
_id: '456',
|
||||
},
|
||||
guilds: ['789'],
|
||||
};
|
||||
});
|
||||
|
||||
describe('isMemberOfGroup', () => {
|
||||
it('registers as a method', () => {
|
||||
expect(instance.isMemberOfGroup).to.be.a.function;
|
||||
});
|
||||
|
||||
it('returns true when the group is the Tavern', () => {
|
||||
expect(instance.isMemberOfGroup(user, {
|
||||
_id: TAVERN_ID,
|
||||
})).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns true when the group is the user\'s party', () => {
|
||||
expect(instance.isMemberOfGroup(user, {
|
||||
type: 'party',
|
||||
_id: user.party._id,
|
||||
})).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns false when the group is not the user\'s party', () => {
|
||||
expect(instance.isMemberOfGroup(user, {
|
||||
type: 'party',
|
||||
_id: 'not my party',
|
||||
})).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns true when the group is not a guild of which the user is a member', () => {
|
||||
expect(instance.isMemberOfGroup(user, {
|
||||
type: 'guild',
|
||||
_id: user.guilds[0],
|
||||
})).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns false when the group is not a guild of which the user is a member', () => {
|
||||
expect(instance.isMemberOfGroup(user, {
|
||||
type: 'guild',
|
||||
_id: 'not my guild',
|
||||
})).to.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
7
website/client/assets/less/forms.less
Normal file
7
website/client/assets/less/forms.less
Normal file
@@ -0,0 +1,7 @@
|
||||
.label-primary {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nested-field {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// CSS that doesn't belong to any specific Vue compoennt
|
||||
@import './utilities';
|
||||
@import './forms';
|
||||
@import './loading-screen';
|
||||
|
||||
body {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
// & { @import "~semantic-ui-less/definitions/elements/loader"; }
|
||||
// & { @import "~semantic-ui-less/definitions/elements/rail"; }
|
||||
// & { @import "~semantic-ui-less/definitions/elements/reveal"; }
|
||||
// & { @import "~semantic-ui-less/definitions/elements/segment"; }
|
||||
& { @import "~semantic-ui-less/definitions/elements/segment"; }
|
||||
// & { @import "~semantic-ui-less/definitions/elements/step"; }
|
||||
|
||||
/* Collections */
|
||||
|
||||
@@ -9,35 +9,35 @@
|
||||
.ui.form
|
||||
.field
|
||||
.ui.checkbox
|
||||
input(type='checkbox')
|
||||
input(type="checkbox")
|
||||
label.label-primary(v-once) {{ $t('pets') }}
|
||||
.field.nested-field
|
||||
.ui.checkbox
|
||||
input(type='checkbox')
|
||||
input(type="checkbox")
|
||||
label(v-once) {{ $t('hatchingPotions') }}
|
||||
.field.nested-field
|
||||
.ui.checkbox
|
||||
input(type='checkbox')
|
||||
input(type="checkbox")
|
||||
label(v-once) {{ $t('quest') }}
|
||||
.field.nested-field
|
||||
.ui.checkbox
|
||||
input(type='checkbox')
|
||||
input(type="checkbox")
|
||||
label(v-once) {{ $t('special') }}
|
||||
.field
|
||||
.ui.checkbox
|
||||
input(type='checkbox')
|
||||
input(type="checkbox")
|
||||
label.label-primary(v-once) {{ $t('mounts') }}
|
||||
.field.nested-field
|
||||
.ui.checkbox
|
||||
input(type='checkbox')
|
||||
input(type="checkbox")
|
||||
label(v-once) {{ $t('hatchingPotions') }}
|
||||
.field.nested-field
|
||||
.ui.checkbox
|
||||
input(type='checkbox')
|
||||
input(type="checkbox")
|
||||
label(v-once) {{ $t('quest') }}
|
||||
.field.nested-field
|
||||
.ui.checkbox
|
||||
input(type='checkbox')
|
||||
input(type="checkbox")
|
||||
label(v-once) {{ $t('special') }}
|
||||
|
||||
.thirteen.wide.column
|
||||
@@ -60,18 +60,9 @@
|
||||
h2 Mounts
|
||||
h2 Quest Mounts
|
||||
h2 Rare Mounts
|
||||
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.label-primary {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nested-field {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.inventory-item-container {
|
||||
padding: 20px;
|
||||
border: 1px solid;
|
||||
|
||||
47
website/client/components/social/guilds/discovery/index.vue
Normal file
47
website/client/components/social/guilds/discovery/index.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template lang="pug">
|
||||
.ui.grid
|
||||
.three.wide.column
|
||||
.ui.left.icon.input
|
||||
i.search.icon
|
||||
input(type="text", :placeholder="$t('search')")
|
||||
h3(v-once) {{ $t('filter') }}
|
||||
|
||||
.ui.form
|
||||
h4 Interests
|
||||
.field
|
||||
.ui.checkbox
|
||||
input(type="checkbox")
|
||||
label(v-once) Habitica Official
|
||||
.field
|
||||
.ui.checkbox
|
||||
input(type="checkbox")
|
||||
label(v-once) Nature
|
||||
.field
|
||||
.ui.checkbox
|
||||
input(type="checkbox")
|
||||
label(v-once) Animals
|
||||
|
||||
.thirteen.wide.column
|
||||
h2(v-once) {{ $t('publicGuilds') }}
|
||||
public-guild-item(v-for="guild in guilds", :key='guild._id', :guild="guild")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions } from '../../../../store';
|
||||
import PublicGuildItem from './publicGuildItem';
|
||||
|
||||
export default {
|
||||
components: { PublicGuildItem },
|
||||
computed: {
|
||||
...mapState(['guilds']),
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
fetchGuilds: 'guilds:fetchAll',
|
||||
}),
|
||||
},
|
||||
created () {
|
||||
if (!this.guilds) this.fetchGuilds();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template lang="pug">
|
||||
.ui.clearing.raised.segment
|
||||
.ui.right.floated.button(:class="[isMember ? 'red' : 'green']") {{ isMember ? $t('leave') : $t('join') }}
|
||||
.floated
|
||||
// TODO v-once?
|
||||
h3.ui.header {{ guild.name }}
|
||||
p {{ guild.description }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from '../../../../store';
|
||||
import groupUtilities from '../../../../mixins/groupsUtilities';
|
||||
|
||||
export default {
|
||||
mixins: [groupUtilities],
|
||||
props: ['guild'],
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
isMember () {
|
||||
return this.isMemberOfGroup(this.user, this.guild);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,9 +1,11 @@
|
||||
<template lang="pug">
|
||||
.row
|
||||
.sixteen.wide.column
|
||||
.ui.secondary.menu
|
||||
.ui.secondary.menu.center-content
|
||||
router-link.item(:to="{name: 'tavern'}")
|
||||
span(v-once) {{ $t('tavern') }}
|
||||
router-link.item(:to="{name: 'guilds'}")
|
||||
span(v-once) {{ $t('guilds') }}
|
||||
router-link.item(:to="{name: 'inbox'}")
|
||||
span(v-once) {{ $t('inbox') }}
|
||||
|
||||
|
||||
25
website/client/mixins/groupsUtilities.js
Normal file
25
website/client/mixins/groupsUtilities.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// TODO if we only have a single method here, move it to an utility
|
||||
// a full mixin is not needed
|
||||
|
||||
import { TAVERN_ID } from '../../common/script/constants';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
isMemberOfGroup (user, group) {
|
||||
if (group._id === TAVERN_ID) return true;
|
||||
|
||||
// If the group is a guild, just check for an intersection with the
|
||||
// current user's guilds, rather than checking the members of the group.
|
||||
if (group.type === 'guild') {
|
||||
return user.guilds.indexOf(group._id) !== -1;
|
||||
}
|
||||
|
||||
// Similarly, if we're dealing with the user's current party, return true.
|
||||
if (group.type === 'party') {
|
||||
return user.party._id === group._id;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -18,6 +18,7 @@ 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';
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
@@ -60,7 +61,17 @@ export default new VueRouter({
|
||||
},
|
||||
{ name: 'challenges', path: 'challenges', component: Page },
|
||||
{ name: 'party', path: 'party', component: Page },
|
||||
{ name: 'guilds', path: 'guilds', component: Page },
|
||||
{
|
||||
path: 'guilds',
|
||||
component: EmptyView,
|
||||
children: [
|
||||
{
|
||||
name: 'guilds',
|
||||
path: '',
|
||||
component: GuildsDiscoveryPage,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
6
website/client/store/actions/guilds.js
Normal file
6
website/client/store/actions/guilds.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export async function fetchAll (store) {
|
||||
let response = await axios.get('/api/v3/groups?type=publicGuilds');
|
||||
store.state.guilds = response.data.data;
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import { flattenAndNamespace } from '../helpers/internals';
|
||||
import * as tasks from './tasks';
|
||||
import * as user from './user';
|
||||
|
||||
// Actions should be named as 'actionName' and can be accessed as 'namespace.actionName'
|
||||
// Example: fetch in user.js -> 'user.fetch'
|
||||
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'
|
||||
|
||||
const actions = flattenAndNamespace({
|
||||
user,
|
||||
tasks,
|
||||
guilds,
|
||||
});
|
||||
|
||||
export default actions;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { flattenAndNamespace } from '../helpers/internals';
|
||||
import * as user from './user';
|
||||
|
||||
// Getters should be named as 'getterName' and can be accessed as 'namespace.getterName'
|
||||
// Example: gems in user.js -> 'user.gems'
|
||||
// Getters should be named as 'getterName' and can be accessed as 'namespace:getterName'
|
||||
// Example: gems in user.js -> 'user:gems'
|
||||
|
||||
const getters = flattenAndNamespace({
|
||||
user,
|
||||
|
||||
@@ -14,10 +14,10 @@ Example:
|
||||
|
||||
Result:
|
||||
getters
|
||||
user.gems
|
||||
user.tasks
|
||||
tasks.todos
|
||||
tasks.dailys
|
||||
user:gems
|
||||
user:tasks
|
||||
tasks:todos
|
||||
tasks:dailys
|
||||
*/
|
||||
export function flattenAndNamespace (namespaces) {
|
||||
let result = {};
|
||||
|
||||
@@ -5,6 +5,7 @@ const state = {
|
||||
title: 'Habitica',
|
||||
user: null,
|
||||
tasks: null, // user tasks
|
||||
guilds: null, // list of public guilds, not fetched initially
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user