[WIP] New client challenges (#8842)

* Added initial challenge pages

* Added challenge item and find guilds page

* Added challenge detail

* Added challenge modals

* Ported over challenge service code

* Ported over challenge ctrl code

* Added styles and column

* Minor modal updates

* Removed duplicate keys

* Fixed casing
This commit is contained in:
Keith Holliday
2017-07-20 14:52:46 -06:00
committed by GitHub
parent d677f5cfc7
commit 0a59b8e85b
16 changed files with 1312 additions and 20 deletions

View File

@@ -0,0 +1,115 @@
<svg xmlns="http://www.w3.org/2000/svg" width="319" height="86" viewBox="0 0 319 86">
<g fill="none" fill-rule="evenodd">
<path fill="#BDA8FF" d="M32.772 28.281l-5.886 1.607 4.35 4.364 4.351 4.365 1.536-5.972 1.536-5.971zM50.432 23.46l-5.886 1.608-5.887 1.606 4.35 4.365 4.351 4.364 1.536-5.97z"/>
<path fill="#E7E0FF" d="M50.432 23.46l-1.536 5.972-1.536 5.971 5.887-1.607 5.886-1.606-4.35-4.365z"/>
<path fill="#CDBEFF" d="M37.123 32.646l-1.536 5.97 5.887-1.606 5.886-1.606-4.35-4.365-4.351-4.365z"/>
<path fill="#C8B6FF" d="M25.35 35.859l-1.536 5.971 5.886-1.607 5.887-1.606-4.35-4.365-4.352-4.364z"/>
<path fill="#CDBEFF" d="M23.814 41.83l22.32 12.609-10.547-15.822z"/>
<path fill="#BDA8FF" d="M47.374 35.4l-1.24 19.038 13.013-22.251z"/>
<path fill="#E7E0FF" d="M35.587 38.617l10.547 15.822 1.226-19.036z"/>
<g>
<path fill="#CDC2FF" d="M40.85 1.097L39.314.75l.462 1.522.463 1.521 1.073-1.174 1.074-1.175z"/>
<path fill="#CDC2FF" d="M45.458 2.139l-1.536-.348-1.536-.347.463 1.522.462 1.522 1.074-1.175z"/>
<path fill="#EDE9FF" d="M45.458 2.139l-1.073 1.174-1.074 1.175 1.536.347 1.537.347-.463-1.522z"/>
<path fill="#D9D1FF" d="M41.312 2.619L40.24 3.793l1.536.347 1.536.348-.462-1.522-.463-1.522z"/>
<path fill="#D5CCFF" d="M38.24 1.924L37.167 3.1l1.536.347 1.536.347-.463-1.521L39.314.75z"/>
<path fill="#D9D1FF" d="M37.167 3.099l3.559 5.577-.487-4.883z"/>
<path fill="#CDC2FF" d="M43.315 4.488l-2.59 4.188 5.662-3.493z"/>
<path fill="#EDE9FF" d="M40.239 3.793l.486 4.883 2.586-4.188z"/>
</g>
<g>
<path fill="#9A62FF" d="M32.47 28.036l-5.994 1.638 4.443 4.493 4.442 4.493 1.551-6.13 1.552-6.132zM50.452 23.124l-5.994 1.637-5.994 1.637 4.443 4.495 4.442 4.493 1.551-6.131z"/>
<path fill="#DBC6FF" d="M50.452 23.124L48.9 29.255l-1.551 6.131 5.994-1.638 5.994-1.636-4.443-4.495z"/>
<path fill="#B389FF" d="M36.912 32.53l-1.55 6.13 5.993-1.637 5.993-1.637-4.442-4.494-4.442-4.493z"/>
<path fill="#AA7BFF" d="M24.925 35.804l-1.552 6.131 5.994-1.637 5.994-1.637-4.442-4.494-4.443-4.494z"/>
<path fill="#B389FF" d="M23.373 41.935l22.77 13.004L35.36 38.66z"/>
<path fill="#9A62FF" d="M47.363 35.382l-1.22 19.557L59.35 32.108z"/>
<path fill="#DBC6FF" d="M35.361 38.66L46.142 54.94l1.207-19.553z"/>
</g>
<g>
<path fill="#9A62FF" d="M67.006 79.1l-1.25-.575.104 1.416.104 1.416 1.146-.84 1.147-.841z"/>
<path fill="#9A62FF" d="M70.758 80.826l-1.25-.575-1.251-.575.104 1.416.104 1.415 1.146-.84z"/>
<path fill="#DBC6FF" d="M70.758 80.826l-1.147.84-1.146.841 1.25.575 1.25.576-.103-1.416z"/>
<path fill="#B389FF" d="M67.11 80.516l-1.146.841 1.25.575 1.25.575-.103-1.416-.104-1.415z"/>
<path fill="#AA7BFF" d="M64.609 79.366l-1.147.84 1.25.576 1.251.575-.103-1.416-.104-1.416z"/>
<path fill="#B389FF" d="M63.462 80.207l1.979 5.518.522-4.368z"/>
<path fill="#9A62FF" d="M68.468 82.509l-3.027 3.216 5.528-2.066z"/>
<path fill="#DBC6FF" d="M65.963 81.357l-.522 4.368 3.024-3.218z"/>
</g>
<g>
<path fill="#BDA8FF" d="M89.04 45.436l-3.876-2.038.147 4.42.147 4.419 3.728-2.382 3.729-2.382z"/>
<path fill="#BDA8FF" d="M100.666 51.548l-3.876-2.037-3.875-2.038.147 4.42.147 4.42 3.728-2.383z"/>
<path fill="#E7E0FF" d="M100.666 51.548l-3.729 2.382-3.728 2.382 3.876 2.037 3.875 2.038-.147-4.42z"/>
<path fill="#CDBEFF" d="M89.186 49.855l-3.728 2.382 3.876 2.037 3.875 2.038-.147-4.42-.147-4.419z"/>
<path fill="#C8B6FF" d="M81.436 45.78l-3.729 2.382 3.876 2.037 3.875 2.038-.147-4.42-.147-4.42z"/>
<path fill="#CDBEFF" d="M77.707 48.162l5.544 17.54 2.207-13.465z"/>
<path fill="#BDA8FF" d="M93.218 56.317L83.251 65.7l17.718-5.309z"/>
<path fill="#E7E0FF" d="M85.458 52.237L83.25 65.701l9.96-9.389z"/>
</g>
<g>
<path fill="#6133B4" d="M3.54 59.3L.42 60.913l3.015 2.026 3.015 2.026.104-3.637.105-3.637zM12.899 54.469l-3.12 1.61-3.12 1.611 3.015 2.027 3.014 2.026.105-3.637z"/>
<path fill="#C6B6E4" d="M12.899 54.469l-.106 3.637-.105 3.637 3.12-1.611 3.12-1.61-3.015-2.027z"/>
<path fill="#8966C7" d="M6.554 61.327l-.104 3.637 3.119-1.611 3.12-1.61-3.015-2.027-3.015-2.026z"/>
<path fill="#7A54C0" d="M.316 64.548L.21 68.185l3.12-1.61 3.12-1.611-3.015-2.026L.42 60.912z"/>
<path fill="#8966C7" d="M.21 68.185l14.217 4.727-7.978-7.948z"/>
<path fill="#6133B4" d="M12.696 61.739l1.73 11.173 4.509-14.394z"/>
<path fill="#C6B6E4" d="M6.45 64.964l7.977 7.948-1.738-11.17z"/>
</g>
<g>
<path fill="#CDC2FF" d="M314 60.13l-.957.627 1.006.49 1.006.49-.048-1.117-.05-1.117zM316.873 58.249l-.957.627-.958.627 1.006.49 1.007.49-.05-1.117z"/>
<path fill="#EDE9FF" d="M316.873 58.249l.049 1.117.049 1.117.957-.627.958-.627-1.006-.49z"/>
<path fill="#D9D1FF" d="M315.007 60.62l.048 1.117.958-.627.958-.627-1.007-.49-1.006-.49z"/>
<path fill="#D5CCFF" d="M313.091 61.874l.049 1.117.958-.627.957-.627-1.006-.49-1.007-.49z"/>
<path fill="#D9D1FF" d="M313.14 62.99l4.637.834-2.722-2.087z"/>
<path fill="#CDC2FF" d="M316.973 60.481l.804 3.343 1.111-4.597z"/>
<path fill="#EDE9FF" d="M315.055 61.737l2.722 2.087-.806-3.341z"/>
<g>
<path fill="#CDC2FF" d="M219.597 71.759l-1.136.247.79.905.79.904.345-1.152.346-1.152zM223.002 71.016l-1.135.247-1.135.248.79.904.79.905.345-1.152z"/>
<path fill="#EDE9FF" d="M223.002 71.016l-.345 1.152-.346 1.152 1.136-.248 1.135-.248-.79-.904z"/>
<path fill="#D9D1FF" d="M220.386 72.663l-.345 1.152 1.135-.248 1.135-.247-.79-.905-.79-.904z"/>
<path fill="#D5CCFF" d="M218.116 73.158l-.346 1.152 1.136-.247 1.135-.248-.79-.904-.79-.905z"/>
<path fill="#D9D1FF" d="M217.77 74.31l4.14 2.723-1.87-3.218z"/>
<path fill="#CDC2FF" d="M222.314 73.32l-.405 3.713 2.676-4.21z"/>
<path fill="#EDE9FF" d="M220.04 73.815l1.87 3.218.401-3.713z"/>
</g>
<g>
<path fill="#9A62FF" d="M236.787 31.737l-5.608 3.681 6.05 3.112 6.048 3.113-.441-6.793-.44-6.794z"/>
<path fill="#9A62FF" d="M253.614 20.695l-5.609 3.681-5.609 3.68 6.05 3.114 6.048 3.113-.44-6.794z"/>
<path fill="#DBC6FF" d="M253.614 20.695l.44 6.794.44 6.794 5.608-3.681 5.61-3.68-6.05-3.114z"/>
<path fill="#B389FF" d="M242.836 34.85l.44 6.793 5.61-3.68 5.608-3.68-6.05-3.114-6.048-3.112z"/>
<path fill="#AA7BFF" d="M231.62 42.211l.44 6.794 5.608-3.681 5.609-3.68-6.05-3.114-6.049-3.112z"/>
<path fill="#B389FF" d="M232.06 49.005l27.678 5.691-16.462-13.052z"/>
<path fill="#9A62FF" d="M254.507 34.274l5.23 20.422 5.987-27.783z"/>
<path fill="#DBC6FF" d="M243.277 41.643l16.46 13.053-5.243-20.414z"/>
</g>
<g>
<path fill="#6133B4" d="M276.012 56.987l-5.04-.954 1.671 4.94 1.67 4.938 3.37-3.985 3.369-3.985z"/>
<path fill="#6133B4" d="M291.131 59.85l-5.04-.954-5.039-.955 1.67 4.94 1.67 4.939 3.37-3.985z"/>
<path fill="#C6B6E4" d="M291.131 59.85l-3.37 3.985-3.368 3.985 5.04.954 5.039.954-1.67-4.94z"/>
<path fill="#8966C7" d="M277.682 61.926l-3.368 3.985 5.04.954 5.038.955-1.67-4.94-1.67-4.939z"/>
<path fill="#7A54C0" d="M267.603 60.018l-3.369 3.984 5.04.955 5.04.954-1.67-4.94-1.672-4.938z"/>
<path fill="#8966C7" d="M264.234 64.002l12.19 17.88-2.11-15.971z"/>
<path fill="#6133B4" d="M284.404 67.822l-7.98 14.059 18.06-12.15z"/>
<path fill="#C6B6E4" d="M274.314 65.911l2.11 15.97 7.969-14.061z"/>
</g>
<g>
<path fill="#6133B4" d="M276.468 8.001l-1.219-.23.401 1.211.4 1.211.819-.98.818-.981zM280.126 8.692l-1.22-.23-1.218-.23.4 1.211.4 1.211.819-.98z"/>
<path fill="#C6B6E4" d="M280.126 8.692l-.82.981-.818.981 1.22.23 1.218.231-.4-1.211z"/>
<path fill="#8966C7" d="M276.869 9.213l-.819.98 1.22.23 1.218.231-.4-1.211-.4-1.211z"/>
<path fill="#7A54C0" d="M274.431 8.752l-.819.98 1.22.231 1.218.23-.4-1.21-.4-1.212z"/>
<path fill="#8966C7" d="M273.612 9.733l2.937 4.38-.499-3.92z"/>
<path fill="#6133B4" d="M278.491 10.655l-1.942 3.458 4.38-2.997z"/>
<path fill="#C6B6E4" d="M276.05 10.193l.499 3.92 1.94-3.459z"/>
</g>
<g>
<path fill="#BDA8FF" d="M298.634 21.087l-2.861-1.025.518 3.051.517 3.05 2.343-2.025 2.344-2.025zM307.216 24.164l-2.86-1.026-2.861-1.025.517 3.05.518 3.052 2.343-2.026z"/>
<path fill="#E7E0FF" d="M307.216 24.164l-2.343 2.025-2.343 2.026 2.86 1.025 2.861 1.026-.517-3.052z"/>
<path fill="#CDBEFF" d="M299.151 24.138l-2.343 2.026 2.861 1.025 2.86 1.026-.517-3.051-.517-3.051z"/>
<path fill="#C8B6FF" d="M293.43 22.088l-2.344 2.025 2.861 1.025 2.861 1.026-.517-3.051-.518-3.051z"/>
<path fill="#CDBEFF" d="M291.086 24.113l5.47 11.612.252-9.561z"/>
<path fill="#BDA8FF" d="M302.536 28.217l-5.98 7.508 11.702-5.457z"/>
<path fill="#E7E0FF" d="M296.808 26.164l-.252 9.56 5.974-7.51z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="16" viewBox="0 0 20 16">
<path fill="#A5A1AC" fill-rule="evenodd" d="M14 14a5.968 5.968 0 0 0-1.537-4H14c2.206 0 4 1.794 4 4h-4zM2 14c0-2.206 1.794-4 4-4h2c2.206 0 4 1.794 4 4H2zM7 2a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm4.315.519a3 3 0 1 1 0 4.962A4.94 4.94 0 0 0 12 5a4.94 4.94 0 0 0-.685-2.481zm5.193 6.036A4.983 4.983 0 0 0 18 5c0-2.757-2.243-5-5-5-1.13 0-2.162.391-3 1.026A4.948 4.948 0 0 0 7 0C4.243 0 2 2.243 2 5c0 1.39.573 2.648 1.492 3.555A5.994 5.994 0 0 0 0 14a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2 5.994 5.994 0 0 0-3.492-5.445z"/>
</svg>

After

Width:  |  Height:  |  Size: 598 B

View File

@@ -24,7 +24,7 @@ div
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(tag="li", :to="{name: 'challenges'}", exact)
router-link.nav-item(tag="li", :to="{name: 'myChallenges'}", exact)
a.nav-link(v-once) {{ $t('challenges') }}
router-link.nav-item.dropdown(tag="li", to="/help", :class="{'active': $route.path.startsWith('/help')}")
a.nav-link(v-once) {{ $t('help') }}

View File

@@ -0,0 +1,199 @@
<template lang="pug">
.row
challenge-modal(:challenge='challenge')
close-challenge-modal
.col-8.standard-page
.row
.col-8
h1 {{challenge.name}}
div
strong(v-once) {{$t('createdBy')}}
span {{challenge.author}}
strong.margin-left(v-once)
.svg-icon.calendar-icon(v-html="icons.calendarIcon")
| {{$t('endDate')}}
span {{challenge.endDate}}
.tags
span.tag(v-for='tag in challenge.tags') {{tag}}
.col-4
.box
.svg-icon.member-icon(v-html="icons.memberIcon")
| {{challenge.memberCount}}
.details(v-once) {{$t('participants')}}
.box
.svg-icon.gem-icon(v-html="icons.gemIcon")
| {{challenge.prize}}
.details(v-once) {{$t('prize')}}
.row
task-column.col-6(v-for="column in columns", :type="column", :key="column")
.col-4.sidebar.standard-page
.acitons
div(v-if='!isMember && !isLeader')
button.btn.btn-success(v-once) {{$t('joinChallenge')}}
div(v-if='isMember')
button.btn.btn-danger(v-once) {{$t('leaveChallenge')}}
div(v-if='isLeader')
button.btn.btn-success(v-once) {{$t('addTask')}}
div(v-if='isLeader')
button.btn.btn-secondary(v-once, @click='edit()') {{$t('editChallenge')}}
div(v-if='isLeader')
button.btn.btn-danger(v-once, @click='closeChallenge()') {{$t('endChallenge')}}
.description-section
h2(v-once) {{$t('challengeDescription')}}
p {{challenge.description}}
</template>
<style lang='scss' scoped>
@import '~client/assets/scss/colors.scss';
h1 {
color: $purple-200;
}
.margin-left {
margin-left: 1em;
}
span {
margin-left: .5em;
}
.calendar-icon {
width: 12px;
display: inline-block;
margin-right: .2em;
}
.tags {
margin-top: 1em;
}
.tag {
border-radius: 30px;
background-color: $gray-600;
padding: .5em;
}
.sidebar {
background-color: $gray-600;
}
.box {
display: inline-block;
padding: 1em;
border-radius: 2px;
background-color: $white;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
margin-left: 1em;
width: 120px;
height: 76px;
text-align: center;
font-size: 20px;
vertical-align: bottom;
.svg-icon {
width: 30px;
display: inline-block;
margin-right: .2em;
vertical-align: bottom;
}
.details {
font-size: 12px;
margin-top: 0.4em;
color: $gray-200;
}
}
.acitons {
width: 100%;
div, button {
width: 60%;
margin: 0 auto;
text-align: center;
}
}
.description-section {
margin-top: 2em;
}
</style>
<script>
import { mapState } from 'client/libs/store';
import closeChallengeModal from './closeChallengeModal';
import Column from '../tasks/column';
import challengeModal from './challengeModal';
import gemIcon from 'assets/svg/gem.svg';
import memberIcon from 'assets/svg/member-icon.svg';
import calendarIcon from 'assets/svg/calendar.svg';
export default {
props: ['challengeId'],
components: {
closeChallengeModal,
challengeModal,
TaskColumn: Column,
},
data () {
return {
columns: ['habit', 'daily', 'todo', 'reward'],
icons: Object.freeze({
gemIcon,
memberIcon,
calendarIcon,
}),
challenge: {
// _id: 1,
// title: 'I am the Night! (Official TAKE THIS Challenge June 2017)',
// memberCount: 5261,
// endDate: '2017-04-04',
// tags: ['Habitica Official', 'Tag'],
// prize: 10,
// description: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium.',
// counts: {
// habit: 0,
// dailies: 2,
// todos: 2,
// rewards: 0,
// },
// author: 'SabreCat',
},
};
},
computed: {
...mapState({user: 'user.data'}),
isMember () {
return this.user.challenges.indexOf(this.challenge._id) !== -1;
},
isLeader () {
return true;
},
},
mounted () {
this.getChallenge();
},
methods: {
async getChallenge () {
this.challenge = await this.$store.dispatch('challenges:getChallenge', {challengeId: this.challengeId});
},
async joinChallenge () {
// this.challenge = this.$store.dispatch('challenges:joinChallenge', {challengeId: this.challengeId});
},
async leaveChallenge () {
// this.challenge = this.$store.dispatch('challenges:leaveChallenge', {challengeId: this.challengeId});
},
closeChallenge () {
this.$root.$emit('show::modal', 'close-challenge-modal');
},
edit () {
// @TODO: set working challenge
this.$root.$emit('show::modal', 'challenge-modal');
},
// @TODO: view members
},
};
</script>

View File

@@ -0,0 +1,178 @@
<template lang="pug">
.card
router-link(:to="{ name: 'challenge', params: { challengeId: challenge._id } }")
h3 {{challenge.name}}
.row
.col-6
div.details
span
.svg-icon.member-icon(v-html="icons.memberIcon")
span {{challenge.memberCount}}
span
.svg-icon.calendar-icon(v-html="icons.calendarIcon")
span
strong End Date:
span {{challenge.endDate}}
div.tags
span.tag(v-for='tag in challenge.tags') {{tag}}
.col-6.prize-section
div
span.svg-icon.gem(v-html="icons.gemIcon")
span.prize {{challenge.prize}}
div Challenge Prize
.row.description
.col-12
| {{challenge.description}}
.container.well-wrapper(v-if='challenge.counts')
.well.row
.col-3
.count-details
.svg-icon.habit-icon(v-html="icons.habitIcon")
span.count {{challenge.counts.habit}}
div {{$t('habit')}}
.col-3
.count-details
.svg-icon.daily-icon(v-html="icons.dailyIcon")
span.count {{challenge.counts.dailies}}
div {{$t('daily')}}
.col-3
.count-details
.svg-icon.todo-icon(v-html="icons.todoIcon")
span.count {{challenge.counts.todos}}
div {{$t('todo')}}
.col-3
.count-details
.svg-icon.reward-icon(v-html="icons.rewardIcon")
span.count {{challenge.counts.rewards}}
div {{$t('reward')}}
</template>
<style lang='scss' scoped>
@import '~client/assets/scss/colors.scss';
.card {
background-color: $white;
box-shadow: 0 2px 2px 0 $gray-600, 0 1px 4px 0 $gray-600;
padding: 1em;
height: 372px;
.gem {
width: 32px;
}
.member-icon {
width: 20px;
}
.calendar-icon {
width: 14px;
}
span {
display: inline-block;
font-size: 14px;
color: $gray-200;
margin-right: 1em;
vertical-align: bottom;
}
.details {
margin: 1em 0;
}
.tags {
margin-bottom: 1em;
}
.tag {
border-radius: 30px;
background-color: $gray-600;
padding: .5em;
}
.prize {
color: $gray-100;
font-size: 24px;
}
.prize-section {
text-align: right;
padding-right: 2em;
padding-top: 1em;
}
.description {
color: $gray-200;
margin-top: 2em;
margin-bottom: 2em;
}
.well-wrapper {
padding: .8em;
}
.well {
background-color: $gray-700;
text-align: center;
padding: 2em;
border-radius: 4px;
.svg-icon {
display: inline-block;
margin-left: .5em;
}
.habit-icon {
width: 30px;
}
.todo-icon {
width: 20px;
}
.daily-icon {
width: 24px;
}
.reward-icon {
width: 26px;
}
.count-details {
padding-left: 1em;
}
.count {
font-size: 20px;
margin-left: .5em;
}
}
}
</style>
<script>
import gemIcon from 'assets/svg/gem.svg';
import memberIcon from 'assets/svg/member-icon.svg';
import calendarIcon from 'assets/svg/calendar.svg';
import habitIcon from 'assets/svg/habit.svg';
import todoIcon from 'assets/svg/todo.svg';
import dailyIcon from 'assets/svg/daily.svg';
import rewardIcon from 'assets/svg/reward.svg';
export default {
props: ['challenge'],
data () {
return {
icons: Object.freeze({
gemIcon,
memberIcon,
calendarIcon,
habitIcon,
todoIcon,
dailyIcon,
rewardIcon,
}),
};
},
};
</script>

View File

@@ -0,0 +1,160 @@
<template lang="pug">
div
b-modal#challenge-modal(:title="$t('createChallenge')", size='lg')
form(@submit.stop.prevent="submit")
.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('description')}}*
div.description-count.float-right {{charactersRemaining}} {{ $t('charactersRemaining') }}
b-form-input.description-textarea(type="text", textarea, :placeholder="$t('challengeDescriptionPlaceHolder')", v-model="workingChallenge.description")
.form-group
label
strong(v-once) {{$t('guildInformation')}}*
a.float-right {{ $t('markdownFormattingHelp') }}
b-form-input.information-textarea(type="text", textarea, :placeholder="$t('challengeInformationPlaceHolder')", v-model="workingChallenge.information")
.form-group(v-if='creating')
label
strong(v-once) {{$t('where')}}
b-dropdown(:text="$t('sort')", right=true)
b-dropdown-item(@click='sort(option.value)')
.form-group
label
strong(v-once) {{$t('categories')}}*
b-dropdown(:text="$t('sort')", right=true)
b-dropdown-item(@click='sort(option.value)')
| Member
.form-group
label
strong(v-once) {{$t('endDate')}}
b-form-input.end-date-input
.form-group
label
strong(v-once) {{$t('prize')}}
b-dropdown(:text="$t('sort')", right=true)
b-dropdown-item(@click='sort(option.value)')
| Member
.row.footer-wrap
.col-12.text-center.submit-button-wrapper
button.btn.btn-primary(v-once) {{$t('createChallenge')}}
.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;
}
.description-textarea {
height: 90px;
}
.information-textarea {
height: 220px;
}
label {
width: 130px;
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;
}
}
</style>
<script>
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';
export default {
props: ['challenge'],
components: {
bModal,
bDropdown,
bDropdownItem,
bFormInput,
},
data () {
return {
creating: true,
charactersRemaining: 250,
workingChallenge: {
name: '',
description: '',
information: '',
},
};
},
mounted () {
if (this.challenge) {
this.workingChallenge = this.challenge;
this.creating = false;
}
},
computed: {
maxPrize () {
// var groupBalance = 0;
// var group = _.find($scope.groups, { _id: gid });
//
// if (group && group.balance && group.leader === User.user._id) {
// groupBalance = group.balance * 4;
// }
//
// return groupBalance;
// return userBalance + availableGroupBalance;
},
insufficientGemsForTavernChallenge () {
// var balance = User.user.balance || 0;
// var isForTavern = $scope.newChallenge.group == TAVERN_ID;
//
// if (isForTavern) {
// return balance <= 0;
// } else {
// return false;
// }
},
},
methods: {
createChallenge () {
// this.$store.dispatch('challenges:createChallenge', {challenge: this.workingChallenge});
},
updateChallenge () {
// this.$store.dispatch('challenges:updateChallenge', {challenge: this.workingChallenge});
},
},
};
</script>

View File

@@ -0,0 +1,104 @@
<template lang="pug">
div
b-modal#close-challenge-modal(:title="$t('createGuild')", size='md')
.header-wrap(slot="modal-header")
h2.text-center(v-once) {{$t('endChallenge')}}
.row.text-center
.col-12
.support-habitica
// @TODO: Add challenge achievement badge here
.col-12
strong(v-once) {{$t('selectChallengeWinnersDescription')}}
.col-12
b-dropdown(:text="$t('sort')", right=true)
b-dropdown-item(@click='sort(option.value)')
| Member
.col-12
button.btn.btn-primary(v-once) {{$t('awardWinners')}}
.col-12
hr
.or {{$t('or')}}
.col-12
strong(v-once) {{$t('doYouWantedToDeleteChallenge')}}
.col-12
button.btn.btn-danger(v-once) {{$t('deleteChallenge')}}
.footer-wrap(slot="modal-footer")
</template>
<style lang='scss'>
@import '~client/assets/scss/colors.scss';
#close-challenge-modal {
h2 {
color: $purple-200
}
#close-challenge-modal_modal_body {
padding-bottom: 2em;
}
.header-wrap {
width: 100%;
padding-top: 2em;
}
.support-habitica {
background-image: url('~client/assets/svg/for-css/support-habitica-gems.svg');
width: 325px;
height: 89px;
margin: 0 auto;
}
.modal-footer, .modal-header {
border: none !important;
}
.footer-wrap {
display: none;
}
.col-12 {
margin-top: 2em;
}
.or {
margin-top: -2em;
background: $white;
width: 40px;
margin-right: auto;
margin-left: auto;
font-weight: bold;
}
}
</style>
<script>
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';
export default {
props: ['challenge'],
components: {
bModal,
bDropdown,
bDropdownItem,
},
data () {
return {
};
},
methods: {
closeChallenge () {
// this.challenge = this.$store.dispatch('challenges:selectChallengeWinner', {
// challengeId: this.challengeId,
// winnerId: this.winnerId,
// });
},
deleteChallenge () {
// this.challenge = this.$store.dispatch('challenges:deleteChallenge', {challengeId: this.challengeId});
},
},
};
</script>

View File

@@ -0,0 +1,89 @@
<template lang="pug">
.row
challenge-modal
sidebar(v-on:search="updateSearch", v-on:filter="updateFilters")
.col-10.standard-page
.row.header-row
.col-md-8.text-left
h1(v-once) {{$t('findChallenges')}}
.col-md-4
span.dropdown-label {{ $t('sortBy') }}
b-dropdown(:text="$t('sort')", right=true)
b-dropdown-item(v-for='sortOption in sortOptions', :key="sortOption.value", @click='sort(sortOption.value)') {{sortOption.text}}
button.btn.btn-secondary.create-challenge-button
.svg-icon.positive-icon(v-html="icons.positiveIcon")
span(v-once, @click='createChallenge()') {{$t('createChallenge')}}
.row
.col-6(v-for='challenge in challenges')
challenge-item(:challenge='challenge')
</template>
<style lang='scss' scoped>
@import '~client/assets/scss/colors.scss';
.header-row {
h1 {
color: $purple-200;
}
.create-challenge-button {
margin-left: 1em;
}
.positive-icon {
color: $green-10;
width: 10px;
display: inline-block;
margin-right: .5em;
}
}
</style>
<script>
import bDropdown from 'bootstrap-vue/lib/components/dropdown';
import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item';
import Sidebar from './sidebar';
import ChallengeItem from './challengeItem';
import challengeModal from './challengeModal';
import positiveIcon from 'assets/svg/positive.svg';
export default {
components: {
Sidebar,
ChallengeItem,
challengeModal,
bDropdown,
bDropdownItem,
},
data () {
return {
icons: Object.freeze({
positiveIcon,
}),
challenges: [],
sortOptions: [],
};
},
mounted () {
this.loadchallanges();
// @TODO: do we need to load groups for filters still?
},
methods: {
updateSearch () {
},
updateFilters () {
},
createChallenge () {
this.$root.$emit('show::modal', 'challenge-modal');
},
async loadchallanges () {
this.challenges = await this.$store.dispatch('challenges:getUserChallenges');
},
},
};
</script>

View File

@@ -0,0 +1,19 @@
<template lang="pug">
.row
secondary-menu.col-12
router-link.nav-link(:to="{name: 'myChallenges'}", :class="{'active': $route.name === 'myChallenges'}") {{ $t('myChallenges') }}
router-link.nav-link(:to="{name: 'findChallenges'}", :class="{'active': $route.name === 'findChallenges'}") {{ $t('findChallenges') }}
.col-12
router-view
</template>
<script>
import SecondaryMenu from 'client/components/secondaryMenu';
export default {
components: {
SecondaryMenu,
},
};
</script>

View File

@@ -0,0 +1,142 @@
<template lang="pug">
.row
challenge-modal
sidebar(v-on:search="updateSearch", v-on:filter="updateFilters")
.col-10.standard-page
.row.header-row
.col-md-8.text-left
h1(v-once) {{$t('myChallenges')}}
.col-md-4
span.dropdown-label {{ $t('sortBy') }}
b-dropdown(:text="$t('sort')", right=true)
b-dropdown-item(v-for='sortOption in sortOptions', :key="sortOption.value", @click='sort(sortOption.value)') {{sortOption.text}}
button.btn.btn-secondary.create-challenge-button
.svg-icon.positive-icon(v-html="icons.positiveIcon")
span(v-once, @click='createChallenge()') {{$t('createChallenge')}}
.row
.no-challenges.text-center.col-md-6.offset-3(v-if='challenges.length === 0')
.svg-icon(v-html="icons.challengeIcon")
h2(v-once) {{$t('noChallengeTitle')}}
p(v-once) {{$t('challengeDescription1')}}
p(v-once) {{$t('challengeDescription2')}}
.row
.col-6(v-for='challenge in challenges')
challenge-item(:challenge='challenge')
</template>
<style lang='scss' scoped>
@import '~client/assets/scss/colors.scss';
.header-row {
h1 {
color: $purple-200;
}
.create-challenge-button {
margin-left: 1em;
}
.positive-icon {
color: $green-10;
width: 10px;
display: inline-block;
margin-right: .5em;
}
}
.no-challenges {
color: $gray-300;
margin-top: 10em;
h2 {
color: $gray-300;
}
.svg-icon {
width: 88.7px;
margin: 1em auto;
}
}
</style>
<script>
import bDropdown from 'bootstrap-vue/lib/components/dropdown';
import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item';
import Sidebar from './sidebar';
import ChallengeItem from './challengeItem';
import challengeModal from './challengeModal';
import challengeIcon from 'assets/svg/challenge.svg';
import positiveIcon from 'assets/svg/positive.svg';
export default {
components: {
Sidebar,
ChallengeItem,
challengeModal,
bDropdown,
bDropdownItem,
},
data () {
return {
icons: Object.freeze({
challengeIcon,
positiveIcon,
}),
challenges: [
// {
// _id: 1,
// title: 'I am the Night! (Official TAKE THIS Challenge June 2017)',
// memberCount: 5261,
// endDate: '2017-04-04',
// tags: ['Habitica Official', 'Tag'],
// prize: 10,
// description: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium.',
// counts: {
// habit: 0,
// dailies: 2,
// todos: 2,
// rewards: 0,
// },
// },
// {
// _id: 2,
// title: '30-Day Money Cleanse 💰',
// memberCount: 112,
// endDate: '2017-04-05',
// tags: ['Owned', 'Tag'],
// prize: 10,
// description: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.',
// counts: {
// habit: 0,
// dailies: 2,
// todos: 30,
// rewards: 0,
// },
// },
],
sortOptions: [],
};
},
mounted () {
this.loadchallanges();
},
methods: {
updateSearch () {
},
updateFilters () {
},
createChallenge () {
this.$root.$emit('show::modal', 'challenge-modal');
},
async loadchallanges () {
this.challenges = await this.$store.dispatch('challenges:getUserChallenges');
},
},
};
</script>

View File

@@ -0,0 +1,156 @@
<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 Membership
.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: 'participating',
key: 'participating',
},
{
label: 'not_participating',
key: 'not_participating',
},
{
label: 'either',
key: 'either',
},
],
guildSizeFilters: [],
guildSizeOptions: [
{
label: 'owned',
key: 'owned',
},
{
label: 'not_owned',
key: 'not_owned',
},
{
label: 'either',
key: 'either',
},
],
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

@@ -24,26 +24,26 @@
</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;
@import '~client/assets/scss/colors.scss';
.sort-select {
margin: 2em;
}
.no-guilds-wrapper {
width: 400px;
margin: 0 auto;
.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>

View File

@@ -72,6 +72,12 @@ const MyGuilds = () => import(/* webpackChunkName: "guilds" */ './components/gui
const GuildsDiscoveryPage = () => import(/* webpackChunkName: "guilds" */ './components/guilds/discovery');
const GuildPage = () => import(/* webpackChunkName: "guilds" */ './components/guilds/guild');
// Challenges
const ChallengeIndex = () => import(/* webpackChunkName: "challenges" */ './components/challenges/index');
const MyChallenges = () => import(/* webpackChunkName: "challenges" */ './components/challenges/myChallenges');
const FindChallenges = () => import(/* webpackChunkName: "challenges" */ './components/challenges/findChallenges');
const ChallengeDetail = () => import(/* webpackChunkName: "challenges" */ './components/challenges/challengeDetail');
Vue.use(VueRouter);
const router = new VueRouter({
@@ -124,7 +130,29 @@ const router = new VueRouter({
},
],
},
{ name: 'challenges', path: 'challenges', component: Page },
{
name: 'challenges',
path: '/challenges',
component: ChallengeIndex,
children: [
{
name: 'myChallenges',
path: 'myChallenges',
component: MyChallenges,
},
{
name: 'findChallenges',
path: 'findChallenges',
component: FindChallenges,
},
{
name: 'challenge',
path: 'challenges/:challengeId',
component: ChallengeDetail,
props: true,
},
],
},
{
path: '/user',
component: ParentPage,

View File

@@ -0,0 +1,75 @@
import axios from 'axios';
import omit from 'lodash/omit';
export async function createChallenge (store, payload) {
let response = await axios.post('/api/v3/challenges', {
data: payload.challenge,
});
return response.data.data;
}
export async function joinChallenge (store, payload) {
let response = await axios.post(`/api/v3/challenges/${payload.challengeId}/join`);
return response.data.data;
}
export async function leaveChallenge (store, payload) {
let response = await axios.post(`/api/v3/challenges/${payload.challengeId}/join`, {
data: {
keep: payload.keep,
},
});
return response.data.data;
}
export async function getUserChallenges () {
let response = await axios.get('/api/v3/challenges/user');
return response.data.data;
}
export async function getGroupChallenges (store, payload) {
let response = await axios.get(`/api/v3/challenges/groups/${payload.groupId}`);
return response.data.data;
}
export async function getChallenge (store, payload) {
let response = await axios.get(`/api/v3/challenges/${payload.challengeId}`);
return response.data.data;
}
export async function exportChallengeCsv (store, payload) {
let response = await axios.get(`/api/v3/challenges/challenges/${payload.challengeId}/export/csv`);
return response.data.data;
}
export async function updateChallenge (store, payload) {
let challengeDataToSend = omit(payload.challenge, ['tasks', 'habits', 'todos', 'rewards', 'group']);
if (challengeDataToSend.leader && challengeDataToSend.leader._id) challengeDataToSend.leader = challengeDataToSend.leader._id;
let response = await axios.post(`/api/v3/challenges/${payload.challenge._id}`, {
data: challengeDataToSend,
});
return response.data.data;
}
export async function deleteChallenge (store, payload) {
let response = await axios.delete(`/api/v3/challenges/challenges/${payload.challengeId}`);
return response.data.data;
}
export async function selectChallengeWinner (store, payload) {
let response = await axios.post(`/api/v3/challenges/challenges/${payload.challengeId}/selectWinner/${payload.winnerId}`);
return response.data.data;
}

View File

@@ -8,6 +8,7 @@ import * as party from './party';
import * as members from './members';
import * as auth from './auth';
import * as quests from './quests';
import * as challenges from './challenges';
// Actions should be named as 'actionName' and can be accessed as 'namespace:actionName'
// Example: fetch in user.js -> 'user:fetch'
@@ -21,6 +22,7 @@ const actions = flattenAndNamespace({
members,
auth,
quests,
challenges,
});
export default actions;

View File

@@ -186,5 +186,27 @@
"emptyMessagesLine1": "You dont have any messages",
"emptyMessagesLine2": "Send a message to start a conversation!",
"contributing": "Contributing",
"askAQuestion": "Ask a Question"
"askAQuestion": "Ask a Question",
"myChallenges": "My Challenges",
"findChallenges": "Find Challenges",
"noChallengeTitle": "You dont have any Challenges.",
"challengeDescription1": "Challenges are community events in which players compete and earn prizes by completing a group of related tasks.",
"challengeDescription2": "Find recommended Challenges based on your interests, browse Habiticas public Challenges, or create your own Challenges.",
"createdBy": "Created By:",
"endDate": "End Date:",
"joinChallenge": "Join Challenge",
"leaveChallenge": "Leave Challenge",
"addTask": "Add Task",
"editChallenge": "Edit Challenge",
"challengeDescription": "Challenge Description",
"selectChallengeWinnersDescription": "Select winners from the Challenge participants",
"awardWinners": "Award Winners",
"doYouWantedToDeleteChallenge": "Do you want to delete this Callenge?",
"deleteChallenge": "Delete Challenge",
"challengeNamePlaceHolder": "What is your Challenge name?",
"challengeDescriptionPlaceHolder": "Write a short description advertising your Challenge to other Habiticans. What is the main purpose of your Challenge and why should people join it? Try to include useful keywords in the description so that Habiticans can easily find it when they search!",
"markdownFormattingHelp": "Markdown formatting help",
"challengeInformationPlaceHolder": "Write a short description advertising your Challenge to other Habiticans. What is the main purpose of your Challenge and why should people join it? Try to include useful keywords in the description so that Habiticans can easily find it when they search!",
"where": "Where*",
"challengeMinimum": "Minimum 1 Gem for public Challenges (helps prevent spam, it really does)."
}