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
|
// CSS that doesn't belong to any specific Vue compoennt
|
||||||
@import './utilities';
|
@import './utilities';
|
||||||
|
@import './forms';
|
||||||
@import './loading-screen';
|
@import './loading-screen';
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
// & { @import "~semantic-ui-less/definitions/elements/loader"; }
|
// & { @import "~semantic-ui-less/definitions/elements/loader"; }
|
||||||
// & { @import "~semantic-ui-less/definitions/elements/rail"; }
|
// & { @import "~semantic-ui-less/definitions/elements/rail"; }
|
||||||
// & { @import "~semantic-ui-less/definitions/elements/reveal"; }
|
// & { @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"; }
|
// & { @import "~semantic-ui-less/definitions/elements/step"; }
|
||||||
|
|
||||||
/* Collections */
|
/* Collections */
|
||||||
|
|||||||
@@ -9,35 +9,35 @@
|
|||||||
.ui.form
|
.ui.form
|
||||||
.field
|
.field
|
||||||
.ui.checkbox
|
.ui.checkbox
|
||||||
input(type='checkbox')
|
input(type="checkbox")
|
||||||
label.label-primary(v-once) {{ $t('pets') }}
|
label.label-primary(v-once) {{ $t('pets') }}
|
||||||
.field.nested-field
|
.field.nested-field
|
||||||
.ui.checkbox
|
.ui.checkbox
|
||||||
input(type='checkbox')
|
input(type="checkbox")
|
||||||
label(v-once) {{ $t('hatchingPotions') }}
|
label(v-once) {{ $t('hatchingPotions') }}
|
||||||
.field.nested-field
|
.field.nested-field
|
||||||
.ui.checkbox
|
.ui.checkbox
|
||||||
input(type='checkbox')
|
input(type="checkbox")
|
||||||
label(v-once) {{ $t('quest') }}
|
label(v-once) {{ $t('quest') }}
|
||||||
.field.nested-field
|
.field.nested-field
|
||||||
.ui.checkbox
|
.ui.checkbox
|
||||||
input(type='checkbox')
|
input(type="checkbox")
|
||||||
label(v-once) {{ $t('special') }}
|
label(v-once) {{ $t('special') }}
|
||||||
.field
|
.field
|
||||||
.ui.checkbox
|
.ui.checkbox
|
||||||
input(type='checkbox')
|
input(type="checkbox")
|
||||||
label.label-primary(v-once) {{ $t('mounts') }}
|
label.label-primary(v-once) {{ $t('mounts') }}
|
||||||
.field.nested-field
|
.field.nested-field
|
||||||
.ui.checkbox
|
.ui.checkbox
|
||||||
input(type='checkbox')
|
input(type="checkbox")
|
||||||
label(v-once) {{ $t('hatchingPotions') }}
|
label(v-once) {{ $t('hatchingPotions') }}
|
||||||
.field.nested-field
|
.field.nested-field
|
||||||
.ui.checkbox
|
.ui.checkbox
|
||||||
input(type='checkbox')
|
input(type="checkbox")
|
||||||
label(v-once) {{ $t('quest') }}
|
label(v-once) {{ $t('quest') }}
|
||||||
.field.nested-field
|
.field.nested-field
|
||||||
.ui.checkbox
|
.ui.checkbox
|
||||||
input(type='checkbox')
|
input(type="checkbox")
|
||||||
label(v-once) {{ $t('special') }}
|
label(v-once) {{ $t('special') }}
|
||||||
|
|
||||||
.thirteen.wide.column
|
.thirteen.wide.column
|
||||||
@@ -60,18 +60,9 @@
|
|||||||
h2 Mounts
|
h2 Mounts
|
||||||
h2 Quest Mounts
|
h2 Quest Mounts
|
||||||
h2 Rare Mounts
|
h2 Rare Mounts
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.label-primary {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nested-field {
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inventory-item-container {
|
.inventory-item-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border: 1px solid;
|
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">
|
<template lang="pug">
|
||||||
.row
|
.row
|
||||||
.sixteen.wide.column
|
.sixteen.wide.column
|
||||||
.ui.secondary.menu
|
.ui.secondary.menu.center-content
|
||||||
router-link.item(:to="{name: 'tavern'}")
|
router-link.item(:to="{name: 'tavern'}")
|
||||||
span(v-once) {{ $t('tavern') }}
|
span(v-once) {{ $t('tavern') }}
|
||||||
|
router-link.item(:to="{name: 'guilds'}")
|
||||||
|
span(v-once) {{ $t('guilds') }}
|
||||||
router-link.item(:to="{name: 'inbox'}")
|
router-link.item(:to="{name: 'inbox'}")
|
||||||
span(v-once) {{ $t('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 TavernPage from './components/social/tavern';
|
||||||
import InboxPage from './components/social/inbox/index';
|
import InboxPage from './components/social/inbox/index';
|
||||||
import InboxConversationPage from './components/social/inbox/conversationPage';
|
import InboxConversationPage from './components/social/inbox/conversationPage';
|
||||||
|
import GuildsDiscoveryPage from './components/social/guilds/discovery/index';
|
||||||
|
|
||||||
Vue.use(VueRouter);
|
Vue.use(VueRouter);
|
||||||
|
|
||||||
@@ -60,7 +61,17 @@ export default new VueRouter({
|
|||||||
},
|
},
|
||||||
{ name: 'challenges', path: 'challenges', component: Page },
|
{ name: 'challenges', path: 'challenges', component: Page },
|
||||||
{ name: 'party', path: 'party', 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 { 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'
|
import * as user from './user';
|
||||||
// Example: fetch in user.js -> 'user.fetch'
|
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({
|
const actions = flattenAndNamespace({
|
||||||
user,
|
user,
|
||||||
tasks,
|
tasks,
|
||||||
|
guilds,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default actions;
|
export default actions;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { flattenAndNamespace } from '../helpers/internals';
|
import { flattenAndNamespace } from '../helpers/internals';
|
||||||
import * as user from './user';
|
import * as user from './user';
|
||||||
|
|
||||||
// Getters should be named as 'getterName' and can be accessed as 'namespace.getterName'
|
// Getters should be named as 'getterName' and can be accessed as 'namespace:getterName'
|
||||||
// Example: gems in user.js -> 'user.gems'
|
// Example: gems in user.js -> 'user:gems'
|
||||||
|
|
||||||
const getters = flattenAndNamespace({
|
const getters = flattenAndNamespace({
|
||||||
user,
|
user,
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ Example:
|
|||||||
|
|
||||||
Result:
|
Result:
|
||||||
getters
|
getters
|
||||||
user.gems
|
user:gems
|
||||||
user.tasks
|
user:tasks
|
||||||
tasks.todos
|
tasks:todos
|
||||||
tasks.dailys
|
tasks:dailys
|
||||||
*/
|
*/
|
||||||
export function flattenAndNamespace (namespaces) {
|
export function flattenAndNamespace (namespaces) {
|
||||||
let result = {};
|
let result = {};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const state = {
|
|||||||
title: 'Habitica',
|
title: 'Habitica',
|
||||||
user: null,
|
user: null,
|
||||||
tasks: null, // user tasks
|
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
|
// 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?
|
// 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
|
// NOTE this takes about 10-15ms on a fast computer
|
||||||
|
|||||||
Reference in New Issue
Block a user