ffb92a5faa
* enable link to markdown info on group and challenge edit screen * allow admin (moderators and staff) to edit challenges * allow admin (moderators and staff) to edit guilds Also add some unrelated TODO comments. * allow any party member (not just leader) to start quest from party page * allow quest owner to cancel, begin, abort quest Previously only the party leader could see those buttons. The leader still can. This also hides those buttons from all other party members. * enable reverse chat in guilds and party * remove outdated videos from press kit * adjust various wordings
436 lines
13 KiB
Vue
436 lines
13 KiB
Vue
<template lang="pug">
|
|
b-modal#challenge-modal(:title="title", size='lg')
|
|
.form
|
|
.form-group
|
|
label
|
|
strong(v-once) {{$t('name')}} *
|
|
b-form-input(type="text", :placeholder="$t('challengeNamePlaceholder')", v-model="workingChallenge.name")
|
|
.form-group
|
|
label
|
|
strong(v-once) {{$t('shortName')}} *
|
|
b-form-input(type="text", :placeholder="$t('shortNamePlaceholder')", v-model="workingChallenge.shortName")
|
|
.form-group
|
|
label
|
|
strong(v-once) {{$t('challengeSummary')}} *
|
|
div.summary-count {{charactersRemaining}} {{ $t('charactersRemaining') }}
|
|
textarea.summary-textarea.form-control(:placeholder="$t('challengeSummaryPlaceholder')", v-model="workingChallenge.summary")
|
|
.form-group
|
|
label
|
|
strong(v-once) {{$t('challengeDescription')}} *
|
|
a.float-right(v-markdown='$t("markdownFormattingHelp")')
|
|
textarea.description-textarea.form-control(:placeholder="$t('challengeDescriptionPlaceholder')", v-model="workingChallenge.description")
|
|
.form-group(v-if='creating')
|
|
label
|
|
strong(v-once) {{$t('challengeGuild')}} *
|
|
select.form-control(v-model='workingChallenge.group')
|
|
option(v-for='group in groups', :value='group._id') {{group.name}}
|
|
.form-group(v-if='workingChallenge.categories')
|
|
label
|
|
strong(v-once) {{$t('categories')}} *
|
|
div.category-wrap(@click.prevent="toggleCategorySelect")
|
|
span.category-select(v-if='workingChallenge.categories.length === 0') {{$t('none')}}
|
|
.category-label(v-for='category in workingChallenge.categories') {{$t(categoriesHashByKey[category])}}
|
|
.category-box(v-if="showCategorySelect")
|
|
.form-check(
|
|
v-for="group in categoryOptions",
|
|
:key="group.key",
|
|
v-if='group.key !== "habitica_official" || user.contributor.admin'
|
|
)
|
|
label.custom-control.custom-checkbox
|
|
input.custom-control-input(type="checkbox",
|
|
:value='group.key',
|
|
v-model="workingChallenge.categories")
|
|
span.custom-control-indicator
|
|
span.custom-control-description(v-once) {{ $t(group.label) }}
|
|
button.btn.btn-primary(@click.prevent="toggleCategorySelect") {{$t('close')}}
|
|
// @TODO: Implement in V2 .form-group
|
|
label
|
|
strong(v-once) {{$t('endDate')}}
|
|
b-form-input.end-date-input
|
|
.form-group
|
|
label
|
|
strong(v-once) {{$t('prize')}}
|
|
input(type='number', :min='minPrize', :max='maxPrize', v-model="workingChallenge.prize")
|
|
.row.footer-wrap
|
|
.col-12.text-center.submit-button-wrapper
|
|
.alert.alert-warning(v-if='insufficientGemsForTavernChallenge')
|
|
You do not have enough gems to create a Tavern challenge
|
|
// @TODO if buy gems button is added, add analytics tracking to it
|
|
// see https://github.com/HabitRPG/habitica/blob/develop/website/views/options/social/challenges.jade#L134
|
|
button.btn.btn-primary(v-once, v-if='creating', @click='createChallenge()') {{$t('createChallengeAddTasks')}}
|
|
button.btn.btn-primary(v-once, v-if='!creating', @click='updateChallenge()') {{$t('updateChallenge')}}
|
|
.col-12.text-center
|
|
p(v-once) {{$t('challengeMinimum')}}
|
|
</template>
|
|
|
|
<style lang='scss'>
|
|
@import '~client/assets/scss/colors.scss';
|
|
|
|
#challenge-modal {
|
|
h5 {
|
|
color: $purple-200;
|
|
margin-bottom: 1.5em;
|
|
}
|
|
|
|
.modal-header {
|
|
border: none;
|
|
}
|
|
|
|
.modal-footer {
|
|
display: none;
|
|
}
|
|
|
|
.summary-count {
|
|
font-size: 12px;
|
|
line-height: 1.33;
|
|
margin-top: 1em;
|
|
color: $gray-200;
|
|
text-align: right;
|
|
}
|
|
|
|
.summary-textarea {
|
|
height: 90px;
|
|
}
|
|
|
|
.description-textarea {
|
|
height: 220px;
|
|
}
|
|
|
|
label {
|
|
margin-right: .5em;
|
|
}
|
|
|
|
.end-date-input {
|
|
width: 150px;
|
|
display: inline-block;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 2em;
|
|
}
|
|
|
|
.submit-button-wrapper {
|
|
margin-bottom: .5em;
|
|
}
|
|
|
|
.footer-wrap {
|
|
margin-top: 2em;
|
|
margin-bottom: 2em;
|
|
}
|
|
|
|
.category-wrap {
|
|
position: relative;
|
|
}
|
|
|
|
.category-box {
|
|
top: -120px !important;
|
|
z-index: 10;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
import clone from 'lodash/clone';
|
|
import bModal from 'bootstrap-vue/lib/components/modal';
|
|
import bDropdown from 'bootstrap-vue/lib/components/dropdown';
|
|
import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item';
|
|
import bFormInput from 'bootstrap-vue/lib/components/form-input';
|
|
|
|
import markdownDirective from 'client/directives/markdown';
|
|
|
|
import { TAVERN_ID, MIN_SHORTNAME_SIZE_FOR_CHALLENGES, MAX_SUMMARY_SIZE_FOR_CHALLENGES } from '../../../common/script/constants';
|
|
import { mapState } from 'client/libs/store';
|
|
|
|
export default {
|
|
props: ['groupId', 'cloning'],
|
|
components: {
|
|
bModal,
|
|
bDropdown,
|
|
bDropdownItem,
|
|
bFormInput,
|
|
},
|
|
directives: {
|
|
markdown: markdownDirective,
|
|
},
|
|
data () {
|
|
let categoryOptions = [
|
|
{
|
|
label: 'habitica_official',
|
|
key: 'habitica_official',
|
|
},
|
|
{
|
|
label: 'academics',
|
|
key: 'academics',
|
|
},
|
|
{
|
|
label: 'advocacy_causes',
|
|
key: 'advocacy_causes',
|
|
},
|
|
{
|
|
label: 'creativity',
|
|
key: 'creativity',
|
|
},
|
|
{
|
|
label: 'entertainment',
|
|
key: 'entertainment',
|
|
},
|
|
{
|
|
label: 'finance',
|
|
key: 'finance',
|
|
},
|
|
{
|
|
label: 'health_fitness',
|
|
key: 'health_fitness',
|
|
},
|
|
{
|
|
label: 'hobbies_occupations',
|
|
key: 'hobbies_occupations',
|
|
},
|
|
{
|
|
label: 'location_based',
|
|
key: 'location_based',
|
|
},
|
|
{
|
|
label: 'mental_health',
|
|
key: 'mental_health',
|
|
},
|
|
{
|
|
label: 'getting_organized',
|
|
key: 'getting_organized',
|
|
},
|
|
{
|
|
label: 'self_improvement',
|
|
key: 'self_improvement',
|
|
},
|
|
{
|
|
label: 'spirituality',
|
|
key: 'spirituality',
|
|
},
|
|
{
|
|
label: 'time_management',
|
|
key: 'time_management',
|
|
},
|
|
];
|
|
let hashedCategories = {};
|
|
categoryOptions.forEach((category) => {
|
|
hashedCategories[category.key] = category.label;
|
|
});
|
|
let categoriesHashByKey = hashedCategories;
|
|
|
|
return {
|
|
workingChallenge: {
|
|
name: '',
|
|
summary: '',
|
|
description: '',
|
|
categories: [],
|
|
group: '',
|
|
dailys: [],
|
|
habits: [],
|
|
leader: '',
|
|
members: [],
|
|
official: false,
|
|
prize: 1,
|
|
rewards: [],
|
|
shortName: '',
|
|
todos: [],
|
|
},
|
|
showCategorySelect: false,
|
|
categoryOptions,
|
|
categoriesHashByKey,
|
|
groups: [],
|
|
};
|
|
},
|
|
async mounted () {
|
|
this.groups = await this.$store.dispatch('guilds:getMyGuilds');
|
|
if (this.user.party._id) {
|
|
let party = await this.$store.dispatch('guilds:getGroup', {groupId: 'party'});
|
|
this.groups.push({
|
|
name: party.name,
|
|
_id: party._id,
|
|
privacy: 'private',
|
|
});
|
|
}
|
|
|
|
this.groups.push({
|
|
name: this.$t('publicChallengesTitle'),
|
|
_id: TAVERN_ID,
|
|
});
|
|
|
|
this.setUpWorkingChallenge();
|
|
},
|
|
watch: {
|
|
user () {
|
|
if (!this.challenge) this.workingChallenge.leader = this.user._id;
|
|
},
|
|
challenge () {
|
|
this.setUpWorkingChallenge();
|
|
},
|
|
cloning () {
|
|
this.setUpWorkingChallenge();
|
|
},
|
|
},
|
|
computed: {
|
|
...mapState({user: 'user.data'}),
|
|
creating () {
|
|
return !this.workingChallenge.id;
|
|
},
|
|
title () {
|
|
if (this.creating) {
|
|
return this.$t('createChallenge');
|
|
}
|
|
|
|
return this.$t('editingChallenge');
|
|
},
|
|
charactersRemaining () {
|
|
let currentLength = this.workingChallenge.summary ? this.workingChallenge.summary.length : 0;
|
|
return MAX_SUMMARY_SIZE_FOR_CHALLENGES - currentLength;
|
|
},
|
|
maxPrize () {
|
|
let userBalance = this.user.balance || 0;
|
|
userBalance = userBalance * 4;
|
|
|
|
let groupBalance = 0;
|
|
let group;
|
|
this.groups.forEach(item => {
|
|
if (item._id === this.workingChallenge.group) {
|
|
group = item;
|
|
return;
|
|
}
|
|
});
|
|
|
|
if (group && group.balance && group.leader === this.user._id) {
|
|
groupBalance = group.balance * 4;
|
|
}
|
|
|
|
return userBalance + groupBalance;
|
|
},
|
|
minPrize () {
|
|
if (this.workingChallenge.group === TAVERN_ID) return 1;
|
|
return 0;
|
|
},
|
|
insufficientGemsForTavernChallenge () {
|
|
let balance = this.user.balance || 0;
|
|
let isForTavern = this.workingChallenge.group === TAVERN_ID;
|
|
|
|
if (isForTavern) {
|
|
return balance <= 0;
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
challenge () {
|
|
return this.$store.state.challengeOptions.workingChallenge;
|
|
},
|
|
},
|
|
methods: {
|
|
setUpWorkingChallenge () {
|
|
this.resetWorkingChallenge();
|
|
|
|
if (!this.challenge) return;
|
|
|
|
this.workingChallenge = Object.assign({}, this.workingChallenge, this.challenge);
|
|
this.workingChallenge.categories = [];
|
|
|
|
if (this.challenge.categories) {
|
|
this.challenge.categories.forEach(category => {
|
|
this.workingChallenge.categories.push(category.slug);
|
|
});
|
|
}
|
|
|
|
if (this.cloning) {
|
|
this.$delete(this.workingChallenge, '_id');
|
|
this.$delete(this.workingChallenge, 'id');
|
|
}
|
|
},
|
|
resetWorkingChallenge () {
|
|
this.workingChallenge = {
|
|
name: '',
|
|
summary: '',
|
|
description: '',
|
|
categories: [],
|
|
group: '',
|
|
dailys: [],
|
|
habits: [],
|
|
leader: '',
|
|
members: [],
|
|
official: false,
|
|
prize: 1,
|
|
rewards: [],
|
|
shortName: '',
|
|
todos: [],
|
|
};
|
|
|
|
this.$store.state.workingChallenge = {};
|
|
},
|
|
async createChallenge () {
|
|
// @TODO: improve error handling, add it to updateChallenge, make errors translatable. Suggestion: `<% fieldName %> is required` where possible, where `fieldName` is inserted as the translatable string that's used for the field header.
|
|
let errors = [];
|
|
|
|
if (!this.workingChallenge.name) errors.push(this.$t('nameRequired'));
|
|
if (this.workingChallenge.shortName.length < MIN_SHORTNAME_SIZE_FOR_CHALLENGES) errors.push(this.$t('tagTooShort'));
|
|
if (!this.workingChallenge.summary) errors.push(this.$t('summaryRequired'));
|
|
if (this.workingChallenge.summary.length > MAX_SUMMARY_SIZE_FOR_CHALLENGES) errors.push(this.$t('summaryTooLong'));
|
|
if (!this.workingChallenge.description) errors.push(this.$t('descriptionRequired'));
|
|
if (!this.workingChallenge.group) errors.push(this.$t('locationRequired'));
|
|
if (!this.workingChallenge.categories || this.workingChallenge.categories.length === 0) errors.push(this.$t('categoiresRequired'));
|
|
|
|
if (errors.length > 0) {
|
|
alert(errors.join('\n'));
|
|
return;
|
|
}
|
|
|
|
this.workingChallenge.timestamp = new Date().getTime();
|
|
let categoryKeys = this.workingChallenge.categories;
|
|
let serverCategories = [];
|
|
categoryKeys.forEach(key => {
|
|
let catName = this.categoriesHashByKey[key];
|
|
serverCategories.push({
|
|
slug: key,
|
|
name: catName,
|
|
});
|
|
});
|
|
|
|
let challengeDetails = clone(this.workingChallenge);
|
|
challengeDetails.categories = serverCategories;
|
|
|
|
let challenge = await this.$store.dispatch('challenges:createChallenge', {challenge: challengeDetails});
|
|
// @TODO: When to remove from guild instead?
|
|
this.user.balance -= this.workingChallenge.prize / 4;
|
|
|
|
this.$emit('createChallenge', challenge);
|
|
this.resetWorkingChallenge();
|
|
|
|
if (this.cloning) this.$store.state.challengeOptions.cloning = true;
|
|
|
|
this.$root.$emit('hide::modal', 'challenge-modal');
|
|
this.$router.push(`/challenges/${challenge._id}`);
|
|
},
|
|
updateChallenge () {
|
|
let categoryKeys = this.workingChallenge.categories;
|
|
let serverCategories = [];
|
|
categoryKeys.forEach(key => {
|
|
let newKey = key.trim();
|
|
let catName = this.categoriesHashByKey[newKey];
|
|
serverCategories.push({
|
|
slug: newKey,
|
|
name: catName,
|
|
});
|
|
});
|
|
|
|
let challengeDetails = clone(this.workingChallenge);
|
|
challengeDetails.categories = serverCategories;
|
|
|
|
this.$emit('updatedChallenge', {
|
|
challenge: challengeDetails,
|
|
});
|
|
this.$store.dispatch('challenges:updateChallenge', {challenge: challengeDetails});
|
|
this.resetWorkingChallenge();
|
|
this.$root.$emit('hide::modal', 'challenge-modal');
|
|
},
|
|
toggleCategorySelect () {
|
|
this.showCategorySelect = !this.showCategorySelect;
|
|
},
|
|
},
|
|
};
|
|
</script>
|