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
This commit is contained in:
negue 2019-09-26 12:43:47 +02:00 committed by Matteo Pagliazzi
parent f792513a26
commit 5f2032a9d5
28 changed files with 2142 additions and 1245 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 614 B

View file

@ -78,3 +78,5 @@ $wizard-color: #2995CD;
$gems-color: #24CC8F;
$gold-color: #FFA624;
$hourglass-color: #2995CD;
$purple-task: #925cf3;

View file

@ -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; }

View 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

View 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

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

File diff suppressed because it is too large Load diff

View file

@ -154,7 +154,6 @@
text-align: center;
overflow-y: hidden;
max-height: 65px; // approximate max height
}
.quick-add-tip-slide-enter-active {

View file

@ -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;
}
}
}

View 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);
}
},
},
};

View file

@ -0,0 +1,12 @@
export const subPageMixin = {
data () {
return {
activeSubPage: '',
};
},
methods: {
changeSubPage (page) {
this.activeSubPage = page;
},
},
};

View file

@ -0,0 +1,7 @@
import { mapState } from 'client/libs/store';
export const userStateMixin = {
computed: {
...mapState({user: 'user.data'}),
},
};

View file

@ -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;

View file

@ -34,5 +34,51 @@
"defaultTag4": "School",
"defaultTag5": "Teams",
"defaultTag6": "Chores",
"defaultTag7": "Creativity"
"defaultTag7": "Creativity",
"workHabitMail": "Process email",
"workDailyImportantTask": "Most important task >> Worked on todays 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"
}

View file

@ -7,6 +7,7 @@
"onward": "Onward!",
"done": "Done",
"finish": "Finish",
"gotIt": "Got it!",
"titleTasks": "Tasks",

View file

@ -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: [],

View 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,
},
],
};

View file

@ -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,