Style fixes: Onboarding (#11241)
* wip: createIntro / onboard ui rework * extract more methods - working body settings component * move justin above the dialog * extract submenu + fix styles * white background on items, working example of "none" item, item border radius * extract options as component * move more subMenu's to the component * add chair margins * move tasks to common/content * add menu indicator * extract more parts of onboarding-intro * refactor / fully converted hair-settings * extract extra-settings * fix sprite positions / lint * extract task-strings to be translatable * style fixes - hide submenu's if not editing * style / margin fixes * more style fixes * show hair styles at onboarding - use arrowleft/right as svg instead of image fix next color * finish button style - full set background/purchase button * fix footer - prev/next hover * Add Default Tasks + `byHabitica` property * customize-options click item on the full zone * purple tasks * footer animation => none * fix onboarding task habit up/down * onboarding circle color/position * task styles * fix onboarding position * show seasonal options * add hover to (locked-) options * added the correct behavior of shop-items to onboarding options * hide hover on active options
|
Before Width: | Height: | Size: 194 B |
|
Before Width: | Height: | Size: 330 B |
|
Before Width: | Height: | Size: 582 B |
|
Before Width: | Height: | Size: 191 B |
|
Before Width: | Height: | Size: 334 B |
|
Before Width: | Height: | Size: 614 B |
|
|
@ -78,3 +78,5 @@ $wizard-color: #2995CD;
|
|||
$gems-color: #24CC8F;
|
||||
$gold-color: #FFA624;
|
||||
$hourglass-color: #2995CD;
|
||||
|
||||
$purple-task: #925cf3;
|
||||
|
|
|
|||
|
|
@ -231,6 +231,19 @@
|
|||
}
|
||||
|
||||
&-purple { // purple, only used in modals
|
||||
&-control {
|
||||
&-bg {
|
||||
background: $purple-task !important;
|
||||
&:hover {
|
||||
.habit-control { background: rgba(26, 24, 29, 0.48) !important; }
|
||||
.daily-todo-control { background: rgba(255, 255, 255, 0.72) !important; }
|
||||
}
|
||||
}
|
||||
&-inner-habit { background: rgba(26, 24, 29, 0.24) !important; }
|
||||
&-inner-daily-todo { background: #ffffff80 !important; }
|
||||
&-checkbox { color: $purple-task !important; }
|
||||
}
|
||||
|
||||
&-modal {
|
||||
&-bg { background: $purple-300 !important; }
|
||||
&-icon { color: $purple-300 !important; }
|
||||
|
|
|
|||
3
website/client/assets/svg/arrow_left.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<path fill="#A5A1AC" fill-rule="evenodd" d="M16 0h-4v32h4V0zm16 8H16v16h16V8zM12 4H8v24h4V4zM8 8H4v16h4V8zm-4 4H0v8h4v-8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 220 B |
3
website/client/assets/svg/arrow_right.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<path fill="#A5A1AC" fill-rule="evenodd" d="M16 0h4v32h-4V0zM0 8h16v16H0V8zm20-4h4v24h-4V4zm4 4h4v16h-4V8zm4 4h4v8h-4v-8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 220 B |
92
website/client/components/avatarModal/body-settings.vue
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<template lang="pug">
|
||||
#body.section.customize-section
|
||||
sub-menu.text-center(:items="items", :activeSubPage="activeSubPage", @changeSubPage="changeSubPage($event)")
|
||||
div(v-if='activeSubPage === "size"')
|
||||
customize-options(
|
||||
:items="sizes",
|
||||
:currentValue="user.preferences.size"
|
||||
)
|
||||
div(v-if='activeSubPage === "shirt"')
|
||||
customize-options(
|
||||
:items="freeShirts",
|
||||
:currentValue="user.preferences.shirt"
|
||||
)
|
||||
customize-options(
|
||||
v-if='editing',
|
||||
:items='specialShirts',
|
||||
:currentValue="user.preferences.shirt",
|
||||
:fullSet='!userOwnsSet("shirt", specialShirtKeys)',
|
||||
@unlock='unlock(`shirt.${specialShirtKeys.join(",shirt.")}`)'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import appearance from 'common/script/content/appearance';
|
||||
import {subPageMixin} from '../../mixins/subPage';
|
||||
import {userStateMixin} from '../../mixins/userState';
|
||||
import {avatarEditorUtilies} from '../../mixins/avatarEditUtilities';
|
||||
import subMenu from './sub-menu';
|
||||
import customizeOptions from './customize-options';
|
||||
import gem from 'assets/svg/gem.svg';
|
||||
|
||||
const freeShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price === 0);
|
||||
const specialShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
|
||||
|
||||
|
||||
export default {
|
||||
props: [
|
||||
'editing',
|
||||
],
|
||||
components: {
|
||||
subMenu,
|
||||
customizeOptions,
|
||||
},
|
||||
mixins: [
|
||||
subPageMixin,
|
||||
userStateMixin,
|
||||
avatarEditorUtilies,
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
specialShirtKeys,
|
||||
icons: Object.freeze({
|
||||
gem,
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
id: 'size',
|
||||
label: this.$t('size'),
|
||||
},
|
||||
{
|
||||
id: 'shirt',
|
||||
label: this.$t('shirt'),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sizes () {
|
||||
return ['slim', 'broad'].map(s => this.mapKeysToFreeOption(s, 'size'));
|
||||
},
|
||||
freeShirts () {
|
||||
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt'));
|
||||
},
|
||||
specialShirts () {
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.specialShirtKeys;
|
||||
let options = keys.map(key => this.mapKeysToOption(key, 'shirt'));
|
||||
return options;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.changeSubPage('size');
|
||||
},
|
||||
methods: {
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
304
website/client/components/avatarModal/customize-options.vue
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
<template lang="pug">
|
||||
.customize-options(:class="{'background-set': fullSet}")
|
||||
.outer-option-background(
|
||||
v-for='option in items',
|
||||
:key='option.key',
|
||||
@click='option.click(option)',
|
||||
:class='{locked: option.gemLocked || option.goldLocked, premium: Boolean(option.gem), active: option.active || currentValue === option.key, none: option.none, hide: option.hide }'
|
||||
)
|
||||
.option
|
||||
.sprite.customize-option(:class='option.class')
|
||||
.redline-outer(v-if="option.none")
|
||||
.redline
|
||||
.gem-lock(v-if='option.gemLocked')
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span {{ option.gem }}
|
||||
.gold-lock(v-if='option.goldLocked')
|
||||
.svg-icon.gold(v-html='icons.gold')
|
||||
span {{ option.gold }}
|
||||
.purchase-set(v-if='fullSet', @click='unlock()')
|
||||
span.label {{ $t('purchaseAll') }}
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span.price 5
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gem from 'assets/svg/gem.svg';
|
||||
import gold from 'assets/svg/gold.svg';
|
||||
import {avatarEditorUtilies} from '../../mixins/avatarEditUtilities';
|
||||
|
||||
export default {
|
||||
props: ['items', 'currentValue', 'fullSet'],
|
||||
mixins: [
|
||||
avatarEditorUtilies,
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
gem,
|
||||
gold,
|
||||
}),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
unlock () {
|
||||
this.$emit('unlock');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.customize-options {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.outer-option-background {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
pointer-events: visible;
|
||||
cursor: pointer;
|
||||
|
||||
&.premium {
|
||||
height: 112px;
|
||||
width: 96px;
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.option {
|
||||
margin: 12px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&.locked {
|
||||
border-radius: 2px;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
background-color: $white;
|
||||
|
||||
.option {
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
padding-left: 6px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 4px 0 rgba($black, 0.16), 0 1px 8px 0 rgba($black, 0.12);
|
||||
border: 1px solid $purple-500;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.locked):not(.active) {
|
||||
.option:hover {
|
||||
background-color: rgba(213, 200, 255, .32);
|
||||
}
|
||||
}
|
||||
|
||||
&.premium:not(.locked):not(.active) {
|
||||
border-radius: 2px;
|
||||
background-color: rgba(59, 202, 215, 0.1);
|
||||
}
|
||||
|
||||
&.none .option {
|
||||
.sprite {
|
||||
opacity: 0.24;
|
||||
}
|
||||
|
||||
.redline-outer {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
margin: 0 auto 0 0;
|
||||
|
||||
.redline {
|
||||
width: 60px;
|
||||
height: 4px;
|
||||
display: block;
|
||||
background: red;
|
||||
transform: rotate(-45deg);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 20px;
|
||||
margin-left: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active .option {
|
||||
background: white;
|
||||
border: solid 4px $purple-300;
|
||||
}
|
||||
|
||||
&.premium:not(.active) .option {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
.option {
|
||||
vertical-align: bottom;
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
|
||||
margin: 12px 8px;
|
||||
border: 4px solid transparent;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.outer-option-background:not(.none) {
|
||||
|
||||
.sprite.customize-option {
|
||||
// margin: 0 auto;
|
||||
//margin-left: -3px;
|
||||
//margin-top: -7px;
|
||||
margin-top: 0;
|
||||
margin-left: 0;
|
||||
|
||||
&.size, &.shirt {
|
||||
margin-top: -8px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
&.color-bangs {
|
||||
margin-top: 3px;
|
||||
}
|
||||
&.skin {
|
||||
margin-top: -4px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
&.chair {
|
||||
margin-left: -1px;
|
||||
margin-top: -1px;
|
||||
|
||||
&.button_chair_black {
|
||||
// different sprite margin?
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
&.handleless {
|
||||
margin-left: -5px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
}
|
||||
&.color, &.bangs {
|
||||
margin-top: 4px;
|
||||
margin-left: -3px;
|
||||
}
|
||||
|
||||
&.hair.base {
|
||||
margin-top: 0px;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
&.headAccessory {
|
||||
margin-top: 0;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
&.headband {
|
||||
margin-top: -6px;
|
||||
margin-left: -27px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text-center {
|
||||
.gem-lock, .gold-lock {
|
||||
display: inline-block;
|
||||
margin: 0 auto 8px;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.gem-lock, .gold-lock {
|
||||
.svg-icon {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.svg-icon, span {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.gem-lock span {
|
||||
color: $green-10
|
||||
}
|
||||
|
||||
.purchase-set {
|
||||
background: #fff;
|
||||
padding: 0.5em;
|
||||
border-radius: 0 0 2px 2px;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
cursor: pointer;
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
span.price {
|
||||
color: #24cc8f;
|
||||
}
|
||||
|
||||
.gem, .coin {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
&.single {
|
||||
width: 141px;
|
||||
}
|
||||
|
||||
width: 100%;
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.gem, .coin {
|
||||
width: 20px;
|
||||
margin: 0 .5em;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.background-set {
|
||||
background-color: #edecee;
|
||||
border-radius: 2px;
|
||||
|
||||
padding-top: 12px;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
|
||||
width: calc(100% - 24px);
|
||||
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
max-width: unset; // disable col12 styling
|
||||
flex: unset;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
272
website/client/components/avatarModal/extra-settings.vue
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
<template lang="pug">
|
||||
#extra.section.container.customize-section
|
||||
sub-menu.text-center(:items="extraSubMenuItems", :activeSubPage="activeSubPage", @changeSubPage="changeSubPage($event)")
|
||||
|
||||
#hair-color(v-if='activeSubPage === "glasses"')
|
||||
customize-options(
|
||||
:items="eyewear"
|
||||
)
|
||||
|
||||
#animal-ears(v-if='activeSubPage === "ears"')
|
||||
customize-options(
|
||||
:items="animalItems('headAccessory')",
|
||||
:fullSet='!animalItemsOwned("headAccessory")',
|
||||
@unlock='unlock(animalItemsUnlockString("headAccessory"))'
|
||||
)
|
||||
|
||||
#animal-tails(v-if='activeSubPage === "tails"')
|
||||
customize-options(
|
||||
:items="animalItems('back')",
|
||||
:fullSet='!animalItemsOwned("back")',
|
||||
@unlock='unlock(animalItemsUnlockString("back"))'
|
||||
)
|
||||
#headband(v-if='activeSubPage === "headband"')
|
||||
customize-options(
|
||||
:items="headbands",
|
||||
)
|
||||
|
||||
#wheelchairs(v-if='activeSubPage === "wheelchair"')
|
||||
customize-options(
|
||||
:items="chairs",
|
||||
)
|
||||
#flowers(v-if='activeSubPage === "flower"')
|
||||
customize-options(
|
||||
:items="flowers",
|
||||
)
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import appearance from 'common/script/content/appearance';
|
||||
import {subPageMixin} from '../../mixins/subPage';
|
||||
import {userStateMixin} from '../../mixins/userState';
|
||||
import {avatarEditorUtilies} from '../../mixins/avatarEditUtilities';
|
||||
import subMenu from './sub-menu';
|
||||
import customizeOptions from './customize-options';
|
||||
import gem from 'assets/svg/gem.svg';
|
||||
|
||||
const freeShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price === 0);
|
||||
const specialShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
|
||||
|
||||
|
||||
export default {
|
||||
props: [
|
||||
'editing',
|
||||
],
|
||||
components: {
|
||||
subMenu,
|
||||
customizeOptions,
|
||||
},
|
||||
mixins: [
|
||||
subPageMixin,
|
||||
userStateMixin,
|
||||
avatarEditorUtilies,
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
animalItemKeys: {
|
||||
back: ['bearTail', 'cactusTail', 'foxTail', 'lionTail', 'pandaTail', 'pigTail', 'tigerTail', 'wolfTail'],
|
||||
headAccessory: ['bearEars', 'cactusEars', 'foxEars', 'lionEars', 'pandaEars', 'pigEars', 'tigerEars', 'wolfEars'],
|
||||
},
|
||||
chairKeys: ['none', 'black', 'blue', 'green', 'pink', 'red', 'yellow', 'handleless_black', 'handleless_blue', 'handleless_green', 'handleless_pink', 'handleless_red', 'handleless_yellow'],
|
||||
specialShirtKeys,
|
||||
icons: Object.freeze({
|
||||
gem,
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
id: 'size',
|
||||
label: this.$t('size'),
|
||||
},
|
||||
{
|
||||
id: 'shirt',
|
||||
label: this.$t('shirt'),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
extraSubMenuItems () {
|
||||
const items = [];
|
||||
|
||||
if (this.editing) {
|
||||
items.push({
|
||||
id: 'glasses',
|
||||
label: this.$t('glasses'),
|
||||
});
|
||||
}
|
||||
|
||||
items.push(
|
||||
{
|
||||
id: 'wheelchair',
|
||||
label: this.$t('wheelchair'),
|
||||
},
|
||||
{
|
||||
id: 'flower',
|
||||
label: this.$t('accent'),
|
||||
},
|
||||
);
|
||||
|
||||
if (this.editing) {
|
||||
items.push({
|
||||
id: 'ears',
|
||||
label: this.$t('animalEars'),
|
||||
});
|
||||
|
||||
items.push({
|
||||
id: 'tails',
|
||||
label: this.$t('animalTails'),
|
||||
});
|
||||
|
||||
items.push({
|
||||
id: 'headband',
|
||||
label: this.$t('headband'),
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
eyewear () {
|
||||
let keys = [
|
||||
'blackTopFrame', 'blueTopFrame', 'greenTopFrame', 'pinkTopFrame', 'redTopFrame', 'whiteTopFrame', 'yellowTopFrame',
|
||||
'blackHalfMoon', 'blueHalfMoon', 'greenHalfMoon', 'pinkHalfMoon', 'redHalfMoon', 'whiteHalfMoon', 'yellowHalfMoon',
|
||||
];
|
||||
let options = keys.map(key => {
|
||||
let newKey = `eyewear_special_${key}`;
|
||||
let option = {};
|
||||
option.key = key;
|
||||
option.active = this.user.preferences.costume ? this.user.items.gear.costume.eyewear === newKey : this.user.items.gear.equipped.eyewear === newKey;
|
||||
option.class = `eyewear_special_${key}`;
|
||||
option.click = () => {
|
||||
let type = this.user.preferences.costume ? 'costume' : 'equipped';
|
||||
|
||||
return this.equip(newKey, type);
|
||||
};
|
||||
return option;
|
||||
});
|
||||
return options;
|
||||
},
|
||||
freeShirts () {
|
||||
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt'));
|
||||
},
|
||||
specialShirts () {
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.specialShirtKeys;
|
||||
let options = keys.map(key => this.mapKeysToOption(key, 'shirt'));
|
||||
return options;
|
||||
},
|
||||
headbands () {
|
||||
let keys = ['blackHeadband', 'blueHeadband', 'greenHeadband', 'pinkHeadband', 'redHeadband', 'whiteHeadband', 'yellowHeadband'];
|
||||
let options = keys.map(key => {
|
||||
let newKey = `headAccessory_special_${key}`;
|
||||
let option = {};
|
||||
option.key = key;
|
||||
option.active = this.user.preferences.costume ? this.user.items.gear.costume.headAccessory === newKey : this.user.items.gear.equipped.headAccessory === newKey;
|
||||
option.class = `headAccessory_special_${option.key} headband`;
|
||||
option.click = () => {
|
||||
let type = this.user.preferences.costume ? 'costume' : 'equipped';
|
||||
return this.equip(newKey, type);
|
||||
};
|
||||
return option;
|
||||
});
|
||||
return options;
|
||||
},
|
||||
chairs () {
|
||||
let options = this.chairKeys.map(key => {
|
||||
let option = {};
|
||||
option.key = key;
|
||||
if (key === 'none') {
|
||||
option.none = true;
|
||||
}
|
||||
option.active = this.user.preferences.chair === key;
|
||||
option.class = `button_chair_${key} chair ${key.includes('handleless_') ? 'handleless' : ''}`;
|
||||
option.click = () => {
|
||||
return this.set({'preferences.chair': key});
|
||||
};
|
||||
return option;
|
||||
});
|
||||
return options;
|
||||
},
|
||||
flowers () {
|
||||
let keys = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
|
||||
let options = keys.map(key => {
|
||||
let option = {};
|
||||
option.key = key;
|
||||
if (key === 0) {
|
||||
option.none = true;
|
||||
}
|
||||
option.active = this.user.preferences.hair.flower === key;
|
||||
option.class = `hair_flower_${key} flower`;
|
||||
option.click = () => {
|
||||
return this.set({'preferences.hair.flower': key});
|
||||
};
|
||||
return option;
|
||||
});
|
||||
return options;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.changeSubPage(this.extraSubMenuItems[0].id);
|
||||
},
|
||||
methods: {
|
||||
animalItems (category) {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.animalItemKeys[category];
|
||||
let options = keys.map(key => {
|
||||
let newKey = `${category}_special_${key}`;
|
||||
let userPurchased = this.user.items.gear.owned[newKey];
|
||||
|
||||
let option = {};
|
||||
option.key = key;
|
||||
option.active = this.user.preferences.costume ? this.user.items.gear.costume[category] === newKey : this.user.items.gear.equipped[category] === newKey;
|
||||
option.class = `headAccessory_special_${option.key} ${category}`;
|
||||
if (category === 'back') {
|
||||
option.class = `icon_back_special_${option.key} back`;
|
||||
}
|
||||
option.gemLocked = userPurchased === undefined;
|
||||
option.goldLocked = userPurchased === false;
|
||||
if (option.goldLocked) {
|
||||
option.gold = 20;
|
||||
}
|
||||
if (option.gemLocked) {
|
||||
option.gem = 2;
|
||||
}
|
||||
option.locked = option.gemLocked || option.goldLocked;
|
||||
option.click = () => {
|
||||
if (option.gemLocked) {
|
||||
return this.unlock(`items.gear.owned.${newKey}`);
|
||||
} else if (option.goldLocked) {
|
||||
return this.buy(newKey);
|
||||
} else {
|
||||
let type = this.user.preferences.costume ? 'costume' : 'equipped';
|
||||
return this.equip(newKey, type);
|
||||
}
|
||||
};
|
||||
return option;
|
||||
});
|
||||
return options;
|
||||
},
|
||||
animalItemsUnlockString (category) {
|
||||
const keys = this.animalItemKeys[category].map(key => {
|
||||
return `items.gear.owned.${category}_special_${key}`;
|
||||
});
|
||||
|
||||
return keys.join(',');
|
||||
},
|
||||
animalItemsOwned (category) {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
|
||||
let own = true;
|
||||
this.animalItemKeys[category].forEach(key => {
|
||||
if (this.user.items.gear.owned[`${category}_special_${key}`] === undefined) own = false;
|
||||
});
|
||||
return own;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
341
website/client/components/avatarModal/hair-settings.vue
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
<template lang="pug">
|
||||
#hair.section.customize-section
|
||||
sub-menu.text-center(:items="hairSubMenuItems", :activeSubPage="activeSubPage", @changeSubPage="changeSubPage($event)")
|
||||
|
||||
#hair-color(v-if='activeSubPage === "color"')
|
||||
customize-options(
|
||||
:items="freeHairColors",
|
||||
:currentValue="user.preferences.hair.color"
|
||||
)
|
||||
|
||||
div(v-if='editing && set.key !== "undefined"', v-for='set in seasonalHairColors')
|
||||
customize-options(
|
||||
:items='set.options',
|
||||
:currentValue="user.preferences.skin",
|
||||
:fullSet='!hideSet(set) && !userOwnsSet("hair", set.keys, "color")',
|
||||
@unlock='unlock(`hair.color.${set.keys.join(",hair.color.")}`)'
|
||||
)
|
||||
|
||||
#style(v-if='activeSubPage === "style"')
|
||||
div(v-for='set in styleSets')
|
||||
customize-options(
|
||||
:items='set.options',
|
||||
:fullSet='set.fullSet',
|
||||
@unlock='set.unlock()'
|
||||
)
|
||||
#bangs(v-if='activeSubPage === "bangs"')
|
||||
customize-options(
|
||||
:items='hairBangs',
|
||||
:currentValue="user.preferences.hair.bangs"
|
||||
)
|
||||
#facialhair(v-if='activeSubPage === "facialhair"')
|
||||
customize-options(
|
||||
v-if='editing',
|
||||
:items='mustacheList'
|
||||
)
|
||||
customize-options(
|
||||
v-if='editing',
|
||||
:items='beardList',
|
||||
:fullSet='isPurchaseAllNeeded("hair", ["baseHair5", "baseHair6"], ["mustache", "beard"])',
|
||||
@unlock='unlock(`hair.mustache.${baseHair5Keys.join(",hair.mustache.")},hair.beard.${baseHair6Keys.join(",hair.beard.")}`)'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import appearance from 'common/script/content/appearance';
|
||||
import {subPageMixin} from '../../mixins/subPage';
|
||||
import {userStateMixin} from '../../mixins/userState';
|
||||
import {avatarEditorUtilies} from '../../mixins/avatarEditUtilities';
|
||||
import subMenu from './sub-menu';
|
||||
import customizeOptions from './customize-options';
|
||||
import gem from 'assets/svg/gem.svg';
|
||||
import appearanceSets from 'common/script/content/appearance/sets';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
|
||||
const hairColorBySet = groupBy(appearance.hair.color, 'set.key');
|
||||
const freeHairColorKeys = hairColorBySet[undefined].map(s => s.key);
|
||||
|
||||
export default {
|
||||
props: [
|
||||
'editing',
|
||||
],
|
||||
components: {
|
||||
subMenu,
|
||||
customizeOptions,
|
||||
},
|
||||
mixins: [
|
||||
subPageMixin,
|
||||
userStateMixin,
|
||||
avatarEditorUtilies,
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
freeHairColorKeys,
|
||||
icons: Object.freeze({
|
||||
gem,
|
||||
}),
|
||||
baseHair1: [1, 3],
|
||||
baseHair2Keys: [2, 4, 5, 6, 7, 8],
|
||||
baseHair3Keys: [9, 10, 11, 12, 13, 14],
|
||||
baseHair4Keys: [15, 16, 17, 18, 19, 20],
|
||||
baseHair5Keys: [1, 2],
|
||||
baseHair6Keys: [1, 2, 3],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hairSubMenuItems () {
|
||||
const items = [
|
||||
{
|
||||
id: 'color',
|
||||
label: this.$t('color'),
|
||||
},
|
||||
{
|
||||
id: 'bangs',
|
||||
label: this.$t('bangs'),
|
||||
},
|
||||
{
|
||||
id: 'style',
|
||||
label: this.$t('style'),
|
||||
},
|
||||
];
|
||||
|
||||
if (this.editing) {
|
||||
items.push({
|
||||
id: 'facialhair',
|
||||
label: this.$t('facialhair'),
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
freeHairColors () {
|
||||
return freeHairColorKeys.map(s => this.mapKeysToFreeOption(s, 'hair', 'color'));
|
||||
},
|
||||
seasonalHairColors () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
|
||||
let seasonalHairColors = [];
|
||||
for (let key in hairColorBySet) {
|
||||
let set = hairColorBySet[key];
|
||||
|
||||
let keys = set.map(item => {
|
||||
return item.key;
|
||||
});
|
||||
|
||||
let options = keys.map(optionKey => {
|
||||
const option = this.mapKeysToOption(optionKey, 'hair', 'color', key);
|
||||
return option;
|
||||
});
|
||||
|
||||
let text = this.$t(key);
|
||||
if (appearanceSets[key] && appearanceSets[key].text) {
|
||||
text = appearanceSets[key].text();
|
||||
}
|
||||
|
||||
let compiledSet = {
|
||||
key,
|
||||
options,
|
||||
keys,
|
||||
text,
|
||||
};
|
||||
seasonalHairColors.push(compiledSet);
|
||||
}
|
||||
|
||||
return seasonalHairColors;
|
||||
},
|
||||
premiumHairColors () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.premiumHairColorKeys;
|
||||
let options = keys.map(key => {
|
||||
return this.mapKeysToOption(key, 'hair', 'color');
|
||||
});
|
||||
return options;
|
||||
},
|
||||
baseHair2 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.baseHair2Keys;
|
||||
let options = keys.map(key => {
|
||||
return this.mapKeysToOption(key, 'hair', 'base');
|
||||
});
|
||||
return options;
|
||||
},
|
||||
baseHair3 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.baseHair3Keys;
|
||||
let options = keys.map(key => {
|
||||
const option = this.mapKeysToOption(key, 'hair', 'base');
|
||||
return option;
|
||||
});
|
||||
return options;
|
||||
},
|
||||
baseHair4 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.baseHair4Keys;
|
||||
let options = keys.map(key => {
|
||||
return this.mapKeysToOption(key, 'hair', 'base');
|
||||
});
|
||||
return options;
|
||||
},
|
||||
baseHair5 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.baseHair5Keys;
|
||||
let options = keys.map(key => {
|
||||
return this.mapKeysToOption(key, 'hair', 'mustache');
|
||||
});
|
||||
return options;
|
||||
},
|
||||
baseHair6 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.baseHair6Keys;
|
||||
let options = keys.map(key => {
|
||||
return this.mapKeysToOption(key, 'hair', 'beard');
|
||||
});
|
||||
return options;
|
||||
},
|
||||
hairBangs () {
|
||||
const none = this.mapKeysToFreeOption(0, 'hair', 'bangs');
|
||||
none.none = true;
|
||||
|
||||
const options = [1, 2, 3, 4].map(s => this.mapKeysToFreeOption(s, 'hair', 'bangs'));
|
||||
|
||||
return [none, ...options];
|
||||
},
|
||||
mustacheList () {
|
||||
const noneOption = this.mapKeysToFreeOption(0, 'hair', 'mustache');
|
||||
noneOption.none = true;
|
||||
|
||||
return [noneOption, ...this.baseHair5];
|
||||
},
|
||||
beardList () {
|
||||
const noneOption = this.mapKeysToFreeOption(0, 'hair', 'beard');
|
||||
noneOption.none = true;
|
||||
|
||||
return [noneOption, ...this.baseHair6];
|
||||
},
|
||||
styleSets () {
|
||||
const sets = [];
|
||||
|
||||
const emptyHairBase = {
|
||||
...this.mapKeysToFreeOption(0, 'hair', 'base'),
|
||||
none: true,
|
||||
};
|
||||
|
||||
if (this.editing) {
|
||||
sets.push({
|
||||
fullSet: !this.userOwnsSet('hair', this.baseHair3Keys, 'base'),
|
||||
unlock: () => this.unlock(`hair.base.${this.baseHair3Keys.join(',hair.base.')}`),
|
||||
options: [
|
||||
emptyHairBase,
|
||||
...this.baseHair3,
|
||||
],
|
||||
});
|
||||
|
||||
sets.push({
|
||||
fullSet: !this.userOwnsSet('hair', this.baseHair4Keys, 'base'),
|
||||
unlock: () => this.unlock(`hair.base.${this.baseHair4Keys.join(',hair.base.')}`),
|
||||
options: [
|
||||
...this.baseHair4,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
sets.push({
|
||||
options: [
|
||||
emptyHairBase,
|
||||
...this.baseHair1.map(key => this.mapKeysToFreeOption(key, 'hair', 'base')),
|
||||
],
|
||||
});
|
||||
|
||||
if (this.editing) {
|
||||
sets.push({
|
||||
fullSet: !this.userOwnsSet('hair', this.baseHair2Keys, 'base'),
|
||||
unlock: () => this.unlock(`hair.base.${this.baseHair2Keys.join(',hair.base.')}`),
|
||||
options: [
|
||||
...this.baseHair2,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return sets;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.changeSubPage('color');
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Allows you to find out whether you need the "Purchase All" button or not. If there are more than 2 unpurchased items, returns true, otherwise returns false.
|
||||
* @param {string} category - The selected category.
|
||||
* @param {string[]} keySets - The items keySets.
|
||||
* @param {string[]} [types] - The items types (subcategories). Optional.
|
||||
* @returns {boolean} - Determines whether the "Purchase All" button is needed (true) or not (false).
|
||||
*/
|
||||
isPurchaseAllNeeded (category, keySets, types) {
|
||||
const purchasedItemsLengths = [];
|
||||
// If item types are specified, count them
|
||||
if (types && types.length > 0) {
|
||||
// Types can be undefined, so we must check them.
|
||||
types.forEach((type) => {
|
||||
if (this.user.purchased[category][type]) {
|
||||
purchasedItemsLengths
|
||||
.push(Object.keys(this.user.purchased[category][type]).length);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let purchasedItemsCounter = 0;
|
||||
|
||||
// If types are not specified, recursively
|
||||
// search for purchased items in the category
|
||||
const findPurchasedItems = (item) => {
|
||||
if (typeof item === 'object') {
|
||||
Object.values(item)
|
||||
.forEach((innerItem) => {
|
||||
if (typeof innerItem === 'boolean' && innerItem === true) {
|
||||
purchasedItemsCounter += 1;
|
||||
}
|
||||
return findPurchasedItems(innerItem);
|
||||
});
|
||||
}
|
||||
return purchasedItemsCounter;
|
||||
};
|
||||
|
||||
findPurchasedItems(this.user.purchased[category]);
|
||||
if (purchasedItemsCounter > 0) {
|
||||
purchasedItemsLengths.push(purchasedItemsCounter);
|
||||
}
|
||||
}
|
||||
|
||||
// We don't need to count the key sets (below)
|
||||
// if there are no purchased items at all.
|
||||
if (purchasedItemsLengths.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const allItemsLengths = [];
|
||||
// Key sets must be specify correctly.
|
||||
keySets.forEach((keySet) => {
|
||||
allItemsLengths.push(Object.keys(this[keySet]).length);
|
||||
});
|
||||
|
||||
// Simply sum all the length values and
|
||||
// write them into variables for the convenience.
|
||||
const allItems = allItemsLengths.reduce((acc, val) => acc + val);
|
||||
const purchasedItems = purchasedItemsLengths.reduce((acc, val) => acc + val);
|
||||
|
||||
const unpurchasedItems = allItems - purchasedItems;
|
||||
return unpurchasedItems > 2;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
114
website/client/components/avatarModal/skin-settings.vue
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<template lang="pug">
|
||||
#skin.section.customize-section
|
||||
sub-menu.text-center(:items="skinSubMenuItems", :activeSubPage="activeSubPage", @changeSubPage="changeSubPage($event)")
|
||||
customize-options(
|
||||
:items="freeSkins",
|
||||
:currentValue="user.preferences.skin"
|
||||
)
|
||||
|
||||
div(v-if='editing && set.key !== "undefined"', v-for='set in seasonalSkins')
|
||||
customize-options(
|
||||
:items='set.options',
|
||||
:currentValue="user.preferences.skin",
|
||||
:fullSet='!hideSet(set) && !userOwnsSet("skin", set.keys)',
|
||||
@unlock='unlock(`skin.${set.keys.join(",skin.")}`)'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import appearance from 'common/script/content/appearance';
|
||||
import {subPageMixin} from '../../mixins/subPage';
|
||||
import {userStateMixin} from '../../mixins/userState';
|
||||
import {avatarEditorUtilies} from '../../mixins/avatarEditUtilities';
|
||||
import appearanceSets from 'common/script/content/appearance/sets';
|
||||
import subMenu from './sub-menu';
|
||||
import customizeOptions from './customize-options';
|
||||
import gem from 'assets/svg/gem.svg';
|
||||
|
||||
import groupBy from 'lodash/groupBy';
|
||||
|
||||
const skinsBySet = groupBy(appearance.skin, 'set.key');
|
||||
|
||||
const freeSkinKeys = skinsBySet[undefined].map(s => s.key);
|
||||
|
||||
// const specialSkinKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
|
||||
|
||||
|
||||
export default {
|
||||
props: [
|
||||
'editing',
|
||||
],
|
||||
components: {
|
||||
subMenu,
|
||||
customizeOptions,
|
||||
},
|
||||
mixins: [
|
||||
subPageMixin,
|
||||
userStateMixin,
|
||||
avatarEditorUtilies,
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
freeSkinKeys,
|
||||
icons: Object.freeze({
|
||||
gem,
|
||||
}),
|
||||
skinSubMenuItems: [
|
||||
{
|
||||
id: 'color',
|
||||
label: this.$t('color'),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
freeSkins () {
|
||||
return freeSkinKeys.map(s => this.mapKeysToFreeOption(s, 'skin'));
|
||||
},
|
||||
seasonalSkins () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
|
||||
let seasonalSkins = [];
|
||||
for (let setKey in skinsBySet) {
|
||||
let set = skinsBySet[setKey];
|
||||
|
||||
let keys = set.map(item => {
|
||||
return item.key;
|
||||
});
|
||||
|
||||
let options = keys.map(optionKey => {
|
||||
const option = this.mapKeysToOption(optionKey, 'skin', '', setKey);
|
||||
|
||||
return option;
|
||||
});
|
||||
|
||||
let text = this.$t(setKey);
|
||||
if (appearanceSets[setKey] && appearanceSets[setKey].text) {
|
||||
text = appearanceSets[setKey].text();
|
||||
}
|
||||
|
||||
let compiledSet = {
|
||||
key: setKey,
|
||||
options,
|
||||
keys,
|
||||
text,
|
||||
};
|
||||
seasonalSkins.push(compiledSet);
|
||||
}
|
||||
|
||||
return seasonalSkins;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.changeSubPage('color');
|
||||
},
|
||||
methods: {
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
52
website/client/components/avatarModal/sub-menu.vue
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<template lang="pug">
|
||||
.sub-menu.text-center
|
||||
.sub-menu-item(
|
||||
v-for="item of items",
|
||||
:key="item.id",
|
||||
@click='$emit("changeSubPage", item.id)',
|
||||
:class='{active: activeSubPage === item.id}'
|
||||
)
|
||||
strong(v-once) {{ item.label }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['items', 'activeSubPage'],
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.sub-menu {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
padding-top: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sub-menu:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sub-menu-item {
|
||||
padding: 6px 16px;
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #f9f9f9;
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-stretch: normal;
|
||||
line-height: 1.67;
|
||||
letter-spacing: normal;
|
||||
text-align: center;
|
||||
color: $gray-100;
|
||||
}
|
||||
|
||||
.sub-menu .sub-menu-item:hover, .sub-menu .sub-menu-item.active {
|
||||
color: $purple-200;
|
||||
border-bottom: 2px solid $purple-200;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -154,7 +154,6 @@
|
|||
text-align: center;
|
||||
|
||||
overflow-y: hidden;
|
||||
max-height: 65px; // approximate max height
|
||||
}
|
||||
|
||||
.quick-add-tip-slide-enter-active {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template lang="pug">
|
||||
.task-wrapper
|
||||
.task(@click='castEnd($event, task)')
|
||||
.task(@click='castEnd($event, task)', :class="`type_${task.type}`")
|
||||
approval-header(:task='task', v-if='this.task.group.id', :group='group')
|
||||
.d-flex(:class="{'task-not-scoreable': isUser !== true}")
|
||||
// Habits left side control
|
||||
|
|
@ -418,7 +418,6 @@
|
|||
transition-property: border-color, background, color;
|
||||
transition-timing-function: ease-in;
|
||||
}
|
||||
|
||||
.left-control {
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
|
|
@ -428,8 +427,14 @@
|
|||
|
||||
& + .task-content {
|
||||
border-left: none;
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
}
|
||||
.task:not(.type_habit) {
|
||||
.left-control {
|
||||
& + .task-content {
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
178
website/client/mixins/avatarEditUtilities.js
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import moment from 'moment';
|
||||
import axios from 'axios';
|
||||
|
||||
import unlock from '../../common/script/ops/unlock';
|
||||
import buy from '../../common/script/ops/buy/buy';
|
||||
|
||||
import get from 'lodash/get';
|
||||
|
||||
import appearanceSets from 'common/script/content/appearance/sets';
|
||||
|
||||
|
||||
import {userStateMixin} from './userState';
|
||||
|
||||
export const avatarEditorUtilies = {
|
||||
mixins: [userStateMixin],
|
||||
data () {
|
||||
return {
|
||||
backgroundUpdate: new Date(),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
hideSet (set) {
|
||||
return moment(appearanceSets[set.key].availableUntil).isBefore(moment());
|
||||
},
|
||||
mapKeysToFreeOption (key, type, subType) {
|
||||
const userPreference = subType ? this.user.preferences[type][subType] : this.user.preferences[type];
|
||||
const pathKey = subType ? `${type}.${subType}` : `${type}`;
|
||||
|
||||
const option = {};
|
||||
option.key = key;
|
||||
option.pathKey = pathKey;
|
||||
option.active = userPreference === key;
|
||||
option.class = this.createClass(type, subType, key);
|
||||
option.click = (optionParam) => {
|
||||
return option.gemLocked ? this.unlock(`${optionParam.pathKey}.${key}`) : this.set({[`preferences.${optionParam.pathKey}`]: optionParam.key});
|
||||
};
|
||||
return option;
|
||||
},
|
||||
mapKeysToOption (key, type, subType, set) {
|
||||
const option = this.mapKeysToFreeOption(key, type, subType);
|
||||
|
||||
let userPurchased = subType ? this.user.purchased[type][subType] : this.user.purchased[type];
|
||||
let locked = !userPurchased || !userPurchased[key];
|
||||
let hide = false;
|
||||
|
||||
if (set && appearanceSets[set]) {
|
||||
if (locked) hide = moment(appearanceSets[set].availableUntil).isBefore(moment());
|
||||
}
|
||||
|
||||
option.gemLocked = locked;
|
||||
option.hide = hide;
|
||||
if (locked) {
|
||||
option.gem = 2;
|
||||
}
|
||||
|
||||
return option;
|
||||
},
|
||||
createClass (type, subType, key) {
|
||||
let str = `${type} ${subType} `;
|
||||
|
||||
switch (type) {
|
||||
case 'shirt': {
|
||||
str += `${this.user.preferences.size}_shirt_${key}`;
|
||||
break;
|
||||
}
|
||||
case 'size': {
|
||||
str += `${key}_shirt_black`;
|
||||
break;
|
||||
}
|
||||
case 'hair': {
|
||||
if (subType === 'color') {
|
||||
str += `hair_bangs_1_${key}`; // todo get current hair-bang setting
|
||||
} else {
|
||||
str += `hair_${subType}_${key}_${this.user.preferences.hair.color}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'skin': {
|
||||
str += `skin skin_${key}`;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// `hair_base_${option.key}_${user.preferences.hair.color}`
|
||||
// console.warn('unknown type', type, key);
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
},
|
||||
userOwnsSet (type, setKeys, subType) {
|
||||
let owns = true;
|
||||
|
||||
setKeys.forEach(key => {
|
||||
if (subType) {
|
||||
if (!this.user.purchased[type] || !this.user.purchased[type][subType] || !this.user.purchased[type][subType][key]) owns = false;
|
||||
return;
|
||||
}
|
||||
if (!this.user.purchased[type][key]) owns = false;
|
||||
});
|
||||
|
||||
return owns;
|
||||
},
|
||||
set (settings) {
|
||||
this.$store.dispatch('user:set', settings);
|
||||
},
|
||||
equip (key, type) {
|
||||
this.$store.dispatch('common:equip', {key, type});
|
||||
},
|
||||
/**
|
||||
* For gem-unlockable preferences, (a) if owned, select preference (b) else, purchase
|
||||
* @param path: User.preferences <-> User.purchased maps like User.preferences.skin=abc <-> User.purchased.skin.abc.
|
||||
* Pass in this paramater as "skin.abc". Alternatively, pass as an array ["skin.abc", "skin.xyz"] to unlock sets
|
||||
*/
|
||||
async unlock (path) {
|
||||
let fullSet = path.indexOf(',') !== -1;
|
||||
let isBackground = path.indexOf('background.') !== -1;
|
||||
|
||||
let cost;
|
||||
|
||||
if (isBackground) {
|
||||
cost = fullSet ? 3.75 : 1.75; // (Backgrounds) 15G per set, 7G per individual
|
||||
} else {
|
||||
cost = fullSet ? 1.25 : 0.5; // (Hair, skin, etc) 5G per set, 2G per individual
|
||||
}
|
||||
|
||||
let loginIncentives = [
|
||||
'background.blue',
|
||||
'background.green',
|
||||
'background.red',
|
||||
'background.purple',
|
||||
'background.yellow',
|
||||
'background.violet',
|
||||
];
|
||||
|
||||
if (loginIncentives.indexOf(path) === -1) {
|
||||
if (fullSet) {
|
||||
if (confirm(this.$t('purchaseFor', {cost: cost * 4})) !== true) return;
|
||||
// @TODO: implement gem modal
|
||||
// if (this.user.balance < cost) return $rootScope.openModal('buyGems');
|
||||
} else if (!get(this.user, `purchased.${path}`)) {
|
||||
if (confirm(this.$t('purchaseFor', {cost: cost * 4})) !== true) return;
|
||||
// @TODO: implement gem modal
|
||||
// if (this.user.balance < cost) return $rootScope.openModal('buyGems');
|
||||
}
|
||||
}
|
||||
|
||||
await axios.post(`/api/v4/user/unlock?path=${path}`);
|
||||
try {
|
||||
unlock(this.user, {
|
||||
query: {
|
||||
path,
|
||||
},
|
||||
});
|
||||
this.backgroundUpdate = new Date();
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
},
|
||||
async buy (item) {
|
||||
const options = {
|
||||
currency: 'gold',
|
||||
key: item,
|
||||
type: 'marketGear',
|
||||
quantity: 1,
|
||||
pinType: 'marketGear',
|
||||
};
|
||||
await axios.post(`/api/v4/user/buy/${item}`, options);
|
||||
try {
|
||||
buy(this.user, {
|
||||
params: options,
|
||||
});
|
||||
this.backgroundUpdate = new Date();
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
12
website/client/mixins/subPage.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export const subPageMixin = {
|
||||
data () {
|
||||
return {
|
||||
activeSubPage: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
changeSubPage (page) {
|
||||
this.activeSubPage = page;
|
||||
},
|
||||
},
|
||||
};
|
||||
7
website/client/mixins/userState.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export const userStateMixin = {
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
};
|
||||
|
|
@ -16,7 +16,7 @@ export function getTagsFor (store) {
|
|||
}
|
||||
|
||||
function getTaskColor (task) {
|
||||
if (task.type === 'reward') return 'purple';
|
||||
if (task.type === 'reward' || task.byHabitica) return 'purple';
|
||||
|
||||
const value = task.value;
|
||||
|
||||
|
|
|
|||
|
|
@ -34,5 +34,51 @@
|
|||
"defaultTag4": "School",
|
||||
"defaultTag5": "Teams",
|
||||
"defaultTag6": "Chores",
|
||||
"defaultTag7": "Creativity"
|
||||
"defaultTag7": "Creativity",
|
||||
|
||||
"workHabitMail": "Process email",
|
||||
"workDailyImportantTask": "Most important task >> Worked on today’s most important task",
|
||||
"workDailyImportantTaskNotes": "Tap to specify your most important task",
|
||||
"workTodoProject": "Work project >> Complete work project",
|
||||
"workTodoProjectNotes": "Tap to specify the name of your current project + set a due date!",
|
||||
|
||||
"exerciseHabit": "10 min cardio >> + 10 minutes cardio",
|
||||
"exerciseDailyText": "Stretching >> Daily workout routine",
|
||||
"exerciseDailyNotes": "Tap to choose your schedule and specify exercises!",
|
||||
"exerciseTodoText": "Set up workout schedule",
|
||||
"exerciseTodoNotes": "Tap to add a checklist!",
|
||||
|
||||
"healthHabit": "Eat Health/Junk Food",
|
||||
"healthDailyText": "Floss",
|
||||
"healthDailyNotes": "Tap to make any changes!",
|
||||
|
||||
"healthTodoText": "Schedule check-up >> Brainstorm a healthy change",
|
||||
"healthTodoNotes": "Tap to add checklists!",
|
||||
|
||||
"schoolHabit": "Study/Procrastinate",
|
||||
"schoolDailyText": "Finish homework",
|
||||
"schoolDailyNotes": "Tap to choose your homework schedule!",
|
||||
"schoolTodoText": "Finish assignment for class",
|
||||
"schoolTodoNotes": "Tap to name the assignment and choose a due date!]",
|
||||
|
||||
"selfCareHabit": "Take a short break",
|
||||
"selfCareDailyText": "5 minutes of quiet breathing",
|
||||
"selfCareDailyNotes": "Tap to choose your schedule!",
|
||||
"selfCareTodoText": "Engage in a fun activity",
|
||||
"selfCareTodoNotes": "Tap to specify what you plan to do!",
|
||||
|
||||
"choresHabit": "10 minutes cleaning",
|
||||
"choresDailyText": "Wash dishes",
|
||||
"choresDailyNotes": "Tap to choose your schedule!",
|
||||
"choresTodoText": "Organize closet >> Organize clutter",
|
||||
"choresTodoNotes": "Tap to specify the cluttered area!",
|
||||
|
||||
"creativityHabit": "Study a master of the craft >> + Practiced a new creative technique",
|
||||
"creativityDailyText": "Work on creative project",
|
||||
"creativityDailyNotes": "Tap to specify the name of your current project + set the schedule!",
|
||||
"creativityTodoText": "Finish creative project",
|
||||
"creativityTodoNotes": "Tap to specify the name of your project",
|
||||
|
||||
"defaultHabitText": "Click here to edit this into a bad habit you'd like to quit",
|
||||
"defaultHabitNotes": "Or delete from the edit screen"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
"onward": "Onward!",
|
||||
"done": "Done",
|
||||
"finish": "Finish",
|
||||
"gotIt": "Got it!",
|
||||
|
||||
"titleTasks": "Tasks",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import defaults from 'lodash/defaults';
|
|||
import each from 'lodash/each';
|
||||
import moment from 'moment';
|
||||
import t from './translation';
|
||||
import {tasksByCategory} from './tasks';
|
||||
|
||||
import {
|
||||
CLASSES,
|
||||
|
|
@ -898,6 +899,7 @@ api.userDefaults = {
|
|||
},
|
||||
],
|
||||
};
|
||||
api.tasksByCategory = tasksByCategory;
|
||||
|
||||
api.userDefaultsMobile = {
|
||||
habits: [],
|
||||
|
|
|
|||
158
website/common/script/content/tasks.js
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import t from './translation';
|
||||
|
||||
export const tasksByCategory = {
|
||||
work: [
|
||||
{
|
||||
type: 'habit',
|
||||
text: t('workHabitMail'),
|
||||
up: true,
|
||||
down: false,
|
||||
},
|
||||
{
|
||||
type: 'daily',
|
||||
text: t('workDailyImportantTask'),
|
||||
notes: t('workDailyImportantTaskNotes'),
|
||||
},
|
||||
{
|
||||
type: 'todo',
|
||||
text: t('workTodoProject'),
|
||||
notes: t('workTodoProjectNotes'),
|
||||
},
|
||||
],
|
||||
exercise: [
|
||||
{
|
||||
type: 'habit',
|
||||
text: t('exerciseHabit'),
|
||||
up: true,
|
||||
down: false,
|
||||
},
|
||||
{
|
||||
type: 'daily',
|
||||
text: t('exerciseDailyText'),
|
||||
notes: t('exerciseDailyNotes'),
|
||||
},
|
||||
{
|
||||
type: 'todo',
|
||||
text: t('exerciseTodoText'),
|
||||
notes: t('exerciseTodoNotes'),
|
||||
},
|
||||
],
|
||||
health_wellness: [ // eslint-disable-line
|
||||
{
|
||||
type: 'habit',
|
||||
text: t('healthHabit'),
|
||||
up: true,
|
||||
down: true,
|
||||
},
|
||||
{
|
||||
type: 'daily',
|
||||
text: t('healthDailyText'),
|
||||
notes: t('healthDailyNotes'),
|
||||
},
|
||||
{
|
||||
type: 'todo',
|
||||
text: t('healthTodoText'),
|
||||
notes: t('healthTodoNotes'),
|
||||
},
|
||||
],
|
||||
school: [
|
||||
{
|
||||
type: 'habit',
|
||||
text: t('schoolHabit'),
|
||||
up: true,
|
||||
down: true,
|
||||
},
|
||||
{
|
||||
type: 'daily',
|
||||
text: t('schoolDailyText'),
|
||||
notes: t('schoolDailyNotes'),
|
||||
},
|
||||
{
|
||||
type: 'todo',
|
||||
text: t('schoolTodoText'),
|
||||
notes: t('schoolTodoNotes'),
|
||||
},
|
||||
],
|
||||
self_care: [ // eslint-disable-line
|
||||
{
|
||||
type: 'habit',
|
||||
text: t('selfCareHabit'),
|
||||
up: true,
|
||||
down: false,
|
||||
},
|
||||
{
|
||||
type: 'daily',
|
||||
text: t('selfCareDailyText'),
|
||||
notes: t('selfCareDailyNotes'),
|
||||
},
|
||||
{
|
||||
type: 'todo',
|
||||
text: t('selfCareTodoText'),
|
||||
notes: t('selfCareTodoNotes'),
|
||||
},
|
||||
],
|
||||
chores: [
|
||||
{
|
||||
type: 'habit',
|
||||
text: t('choresHabit'),
|
||||
up: true,
|
||||
down: false,
|
||||
},
|
||||
{
|
||||
type: 'daily',
|
||||
text: t('choresDailyText'),
|
||||
notes: t('choresDailyNotes'),
|
||||
},
|
||||
{
|
||||
type: 'todo',
|
||||
text: t('choresTodoText'),
|
||||
notes: t('choresTodoNotes'),
|
||||
},
|
||||
],
|
||||
creativity: [
|
||||
{
|
||||
type: 'habit',
|
||||
text: t('creativityHabit'),
|
||||
up: true,
|
||||
down: false,
|
||||
},
|
||||
{
|
||||
type: 'daily',
|
||||
text: t('creativityDailyText'),
|
||||
notes: t('creativityDailyNotes'),
|
||||
},
|
||||
{
|
||||
type: 'todo',
|
||||
text: t('creativityTodoText'),
|
||||
notes: t('creativityTodoNotes'),
|
||||
},
|
||||
],
|
||||
defaults: [
|
||||
{
|
||||
type: 'habit',
|
||||
text: t('defaultHabit4Text'),
|
||||
notes: t('defaultHabit4Notes'),
|
||||
up: true,
|
||||
down: false,
|
||||
},
|
||||
{
|
||||
type: 'habit',
|
||||
text: t('defaultHabitText'),
|
||||
notes: t('defaultHabitNotes'),
|
||||
up: false,
|
||||
down: true,
|
||||
},
|
||||
{
|
||||
type: 'todo',
|
||||
text: t('defaultTodo1Text'),
|
||||
notes: t('defaultTodoNotes'),
|
||||
byHabitica: true,
|
||||
},
|
||||
{
|
||||
type: 'reward',
|
||||
text: t('defaultReward2Text'),
|
||||
notes: t('defaultReward2Notes'),
|
||||
value: 10,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -128,6 +128,8 @@ export let TaskSchema = new Schema({
|
|||
},
|
||||
|
||||
reminders: [reminderSchema],
|
||||
|
||||
byHabitica: {$type: Boolean, default: false}, // Flag of Tasks that were created by Habitica
|
||||
}, _.defaults({
|
||||
minimize: false, // So empty objects are returned
|
||||
strict: true,
|
||||
|
|
|
|||