Client Redesign: Inventory pages, secondary menu, misc css and design items (#8631)

* add colors palette

* add secondary menu component and style it

* add box shadow to secondary menu

* misc css, fixes for secondary menu

* client: add equipment page with grouping, css: add some styles

* add typography

* more equipment

* stable: fix linting

* equipment: add styles (lots of general styles too)

* remove duplicate google fonts loading

* add dropdowns

* design: white search input background, remove gray from items

* start adding drawer and selected indicator

* wip equipment

* fix equipment

* equipment: correctly bind new properties on items.gear.equipped

* equipment: fix vue binding. version 2

* equipment: fix vue binding. version 3

* back to first fix for equip op, fix for sourcemaps, send http request when an item is equipped, load bootstrap-vue components where needed

* checkboxes and radio buttons

* correctly renders selected items in first postion during the first render

* add search

* general changes, constants part of app state, add popovers

* add toggle switch, rename css

* correct offset

* upgrade deps

* upgrade deps

* drawer and lot of other work

* update equipping mechanism

* finish equipment

* fix compilation and upgrade deps

* use v-show in place of v-if to fix ui issues

* v-show -> v-if

* fix linting in test/client

* fix es6 compilation in test/client

* fix babel compilation for tests

* fix groupsUtilities mixin tests

* client: buttons

* client: buttons: fix colors

* client: finish buttons and dropdowns

* upgrade bootstrap-vue, finish buttons and dropdowns

* fix tasks page layout

* misc fixes for buttons

* add textareas

* fix app menu

* add inputs

* fixes for toggleSwitch

* typography

* checkboxes and radio buttons

* add checkbox icon

* fix equip.js

* extract strings to newClient.json

* add Popover above 'Use Costume' / 'Auto Equip' slider - disable item select if costume-mode and 'useCostume' isn't active

* show "you have disabled your costume" error above the drawer items

* check errorMessage for null

* hide star if costume not enabled

* fix errorMessage (!errorMessage seems not to work for string)

* show minimize / expand icon - always centered by css

* drawer test

* drawer: fix centering on large screens

* fix show more button

* add margin when two dropdowns are next to each other

* adjust the page padding based on the drawer, misc fixes

* drawer fixes
This commit is contained in:
Matteo Pagliazzi
2017-05-16 21:09:55 +02:00
committed by GitHub
parent 1de379a2c3
commit 0af1203832
52 changed files with 3395 additions and 828 deletions

2387
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@
"async": "^1.5.0",
"autoprefixer": "^6.4.0",
"aws-sdk": "^2.0.25",
"axios": "^0.15.3",
"axios": "^0.16.0",
"babel-core": "^6.0.0",
"babel-loader": "^6.0.0",
"babel-plugin-syntax-async-functions": "^6.13.0",
@@ -29,13 +29,14 @@
"bluebird": "^3.3.5",
"body-parser": "^1.15.0",
"bootstrap": "^4.0.0-alpha.6",
"bootstrap-vue": "^0.15.8",
"bower": "~1.3.12",
"browserify": "~12.0.1",
"compression": "^1.6.1",
"connect-ratelimit": "0.0.7",
"cookie-session": "^1.2.0",
"coupon-code": "^0.4.5",
"css-loader": "^0.26.1",
"css-loader": "^0.28.0",
"csv-stringify": "^1.0.2",
"cwait": "^1.0.0",
"domain-middleware": "~0.1.0",
@@ -96,7 +97,7 @@
"postcss-easy-import": "^2.0.0",
"pretty-data": "^0.40.0",
"ps-tree": "^1.0.0",
"pug": "^2.0.0-beta11",
"pug": "^2.0.0-beta.12",
"push-notify": "habitrpg/push-notify#v1.2.0",
"pusher": "^1.3.0",
"request": "~2.74.0",
@@ -119,10 +120,10 @@
"vue-loader": "^11.0.0",
"vue-mugen-scroll": "^0.2.1",
"vue-router": "^2.0.0-rc.5",
"vue-style-loader": "^2.0.0",
"vue-style-loader": "^3.0.0",
"vue-template-compiler": "^2.1.10",
"webpack": "^2.2.1",
"webpack-merge": "^2.6.1",
"webpack-merge": "^4.0.0",
"winston": "^2.1.0",
"winston-loggly-bulk": "^1.4.2",
"xml2js": "^0.4.4"
@@ -168,7 +169,7 @@
"chromedriver": "^2.27.2",
"connect-history-api-fallback": "^1.1.0",
"coveralls": "^2.11.2",
"cross-env": "^3.1.4",
"cross-env": "^4.0.0",
"cross-spawn": "^5.0.1",
"csv": "~0.3.6",
"deep-diff": "~0.1.4",
@@ -192,7 +193,7 @@
"karma-mocha": "^0.2.0",
"karma-mocha-reporter": "^1.1.1",
"karma-phantomjs-launcher": "^1.0.0",
"karma-sinon-chai": "^1.2.0",
"karma-sinon-chai": "~1.2.0",
"karma-sinon-stub-promise": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.24",

View File

@@ -2,5 +2,10 @@
"env": {
"node": true,
"browser": true,
}
},
"extends": [
"habitrpg/browser",
"habitrpg/mocha",
"habitrpg/esnext",
],
}

View File

@@ -0,0 +1,19 @@
import Vue from 'vue';
import DrawerComponent from 'client/components/inventory/drawer.vue';
describe('DrawerComponent', () => {
it('sets the correct default data', () => {
expect(DrawerComponent.data).to.be.a('function');
const defaultData = DrawerComponent.data();
expect(defaultData.open).to.be.true;
});
it('renders the correct title', () => {
const Ctor = Vue.extend(DrawerComponent);
const vm = new Ctor({propsData: {
title: 'My title',
}}).$mount();
expect(vm.$el.textContent).to.be.equal('My title');
});
});

View File

@@ -1,5 +1,6 @@
import groupsUtilities from 'client/mixins/groupsUtilities';
import { TAVERN_ID } from 'common/script/constants';
import generateStore from 'client/store';
import Vue from 'vue';
describe('Groups Utilities Mixin', () => {
@@ -7,6 +8,7 @@ describe('Groups Utilities Mixin', () => {
before(() => {
instance = new Vue({
store: generateStore(),
mixins: [groupsUtilities],
});

View File

@@ -17,21 +17,31 @@ const baseConfig = {
path: config.build.assetsRoot,
publicPath: IS_PROD ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
filename: '[name].js',
devtoolModuleFilenameTemplate (info) {
// Fix source maps, code from
// https://github.com/Darkside73/bbsmile.com.ua/commit/3596d3c42ef91b69d8380359c3e8908edc08acdb
let filename = info.resourcePath;
if (info.resource.match(/\.vue$/) && !info.allLoaders.match(/type=script/)) {
filename = 'generated';
}
return filename;
},
},
resolve: {
extensions: ['*', '.js', '.vue', '.json'],
modules: [
path.join(__dirname, '..', 'website'),
path.join(__dirname, '..', 'test/client/unit'),
path.join(__dirname, '..', 'node_modules'),
path.join(projectRoot, 'website'),
path.join(projectRoot, 'test/client/unit'),
path.join(projectRoot, 'node_modules'),
],
alias: {
jquery: 'jquery/src/jquery',
website: path.resolve(__dirname, '../website'),
common: path.resolve(__dirname, '../website/common'),
client: path.resolve(__dirname, '../website/client'),
assets: path.resolve(__dirname, '../website/client/assets'),
components: path.resolve(__dirname, '../website/client/components'),
website: path.resolve(projectRoot, 'website'),
common: path.resolve(projectRoot, 'website/common'),
client: path.resolve(projectRoot, 'website/client'),
assets: path.resolve(projectRoot, 'website/client/assets'),
components: path.resolve(projectRoot, 'website/client/components'),
},
},
plugins: [
@@ -63,8 +73,14 @@ const baseConfig = {
{
test: /\.js$/,
loader: 'babel-loader',
include: projectRoot,
exclude: /node_modules/,
include: [
path.join(projectRoot, 'test'),
path.join(projectRoot, 'website'),
path.join(projectRoot, 'node_modules', 'bootstrap-vue'),
],
options: {
cacheDirectory: true,
},
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="11" viewBox="0 0 10 11">
<g fill="none" fill-rule="evenodd" stroke="#878190" stroke-width="2">
<path d="M5 3v8M9 6L5 2 1 6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 213 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="2" viewBox="0 0 10 2">
<path fill="none" fill-rule="evenodd" stroke="#878190" stroke-width="2" d="M0 1h10"/>
</svg>

After

Width:  |  Height:  |  Size: 179 B

View File

@@ -0,0 +1,15 @@
.badge {
font-size: 12px;
font-weight: bold;
line-height: 1.33;
color: $gray-200;
padding: 4px 8px;
}
.badge-pill {
border-radius: 100px;
}
.badge-default {
background: $gray-500;
}

View File

@@ -0,0 +1,125 @@
@mixin btn-focus-hover-shadow () {
box-shadow: 0 4px 4px 0 rgba($black, 0.16), 0 1px 8px 0 rgba($black, 0.12);
}
.btn {
cursor: pointer;
font-family: 'Roboto Condensed', sans-serif;
font-size: 16px;
font-weight: bold;
line-height: 1.5;
border: 1px solid transparent !important;
padding: 7.5px 15.5px;
border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
color: $white;
&:focus {
outline: none;
border-color: $purple-400;
@include btn-focus-hover-shadow();
}
&:hover {
@include btn-focus-hover-shadow();
border-color: transparent;
}
&:active {
box-shadow: none;
border: 1px solid transparent;
}
}
.btn:disabled, .btn.disabled {
box-shadow: none;
opacity: 0.64;
border-color: transparent;
}
.btn-primary {
background: $purple-200;
&:disabled {
background: $purple-200;
}
&:hover:not(:disabled), &:active {
background: #5d3b9c;
}
}
.btn-secondary, .dropdown > .btn-secondary {
color: $gray-50;
background: $white !important;
&:hover:not(:disabled):not(.disabled), &:active, &:focus {
color: $purple-200 !important;
}
&:active, &:focus {
border-color: $purple-500 !important;
}
&:disabled, &.disabled {
background: $gray-500 !important;
color: $gray-100 !important;
}
}
.btn-success {
background: $green-10;
&:disabled {
background: $green-10;
}
&:hover:not(:disabled), &:active {
background: $green-50;
}
}
.btn-info {
background: $blue-50;
&:disabled {
background: $blue-50;
}
&:hover:not(:disabled), &:active {
background: $blue-100;
}
}
.btn-danger {
background: $red-50;
&:disabled {
background: $red-50;
}
&:hover:not(:disabled), &:active {
background: $red-100;
}
}
.btn-show-more {
display: block;
width: 50%;
max-width: 448px;
margin: 0 auto;
margin-top: 12px;
padding: 8px;
font-size: 14px;
line-height: 1.43;
font-weight: bold;
text-align: center;
background: $gray-600;
color: $gray-200 !important; // Otherwise it gets ignored when used on an A element
box-shadow: none;
&:hover {
box-shadow: none;
color: inherit !important;
}
}

View File

@@ -0,0 +1,58 @@
// Colors taken from the Habitica Color Palette
// The palette is available at TODO ADD LINK TO PALETTE PDF
// The colors are named from the darkest to the lightest
$white: #FFFFFF;
$black: #1A181D;
$gray-10: #34313A;
$gray-50: #4E4A57;
$gray-100: #686274;
$gray-200: #878190;
$gray-300: #A5A1AC;
$gray-400: #C3C0C7;
$gray-500: #E1E0E3;
$gray-600: #EDECEE;
$gray-700: #F9F9F9;
$purple-50: #36205D;
$purple-100: #432874;
$purple-200: #4F2A93;
$purple-300: #6133B4;
$purple-400: #9A62FF;
$purple-500: #BDA8FF;
$red-10: #F23035;
$red-50: #F74E52;
$red-100: #FF6165;
$red-500: #FFB6B8;
$maroon-10: #B01515;
$maroon-50: #C92B2B;
$maroon-100: #DE3F3F;
$maroon-500: #F19595;
$yellow-10: #FFA623;
$yellow-50: #FFB445;
$yellow-100: #FFBE5D;
$yellow-500: #FFD9A0;
$orange-10: #F47825;
$orange-50: #FA8537;
$orange-100: #FF944C;
$orange-500: #FFBF98;
$blue-10: #2995CD;
$blue-50: #46A7D9;
$blue-100: #50B5E9;
$blue-500: #A9DCF6;
$teal-10: #20B2BF;
$teal-50: #3BCAD7;
$teal-100: #5EDDE9;
$teal-500: #A5F7FF;
$green-10: #24CC8F;
$green-50: #3FDAA2;
$green-100: #5AEAB2;
$green-500: #A6FFDF;

View File

@@ -0,0 +1,51 @@
.dropdown > .btn {
padding: 9px 15.5px;
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: normal;
line-height: 1.43;
}
.dropdown.show > .dropdown-toggle {
color: $purple-200;
border-color: $purple-500 !important;
box-shadow: none;
}
.dropdown-toggle::after {
margin-left: 16px;
border-top: 6px solid;
border-right: 5px solid transparent;
border-left: 5px solid transparent;
}
.dropdown-menu {
padding: 0px;
border: none;
border-radius: 4px;
box-shadow: 0 2px 2px 0 rgba($black, 0.15), 0 1px 4px 0 rgba($white, 0.1);
}
.dropdown-item {
padding-left: 24px;
padding-top: 8px;
padding-bottom: 8px;
font-size: 14px;
line-height: 1.71;
color: $gray-50;
cursor: pointer;
border-bottom: 1px solid $gray-500;
&:focus {
outline: none;
}
&:active, &:hover, &:focus, &.active, &.dropdown-item-active {
background-color: rgba(#d5c8ff, 0.32);
color: $purple-200;
}
}
.dropdown + .dropdown {
margin-left: 12px;
}

View File

@@ -0,0 +1,168 @@
.nested-field {
padding-left: 1.5rem;
}
// Inputs and texteares
input, textarea, input.form-control, textarea.form-control {
padding: 10px 16px;
border-radius: 2px;
font-size: 14px;
line-height: 1.43;
color: $gray-200;
border: 1px solid $gray-400;
&:hover:not(:disabled) {
border-color: $gray-300;
}
&:active:not(:disabled), &:focus:not(:disabled) {
border-color: $purple-500;
color: $gray-50;
outline: 0;
}
&:disabled {
opacity: 0.64;
background: $gray-500;
}
&.input-search { // TODO Abstract to work with all icons
background-repeat: no-repeat;
background-position: center left 16px;
background-size: 16px 16px;
background-image: url(~client/assets/svg/search.svg);
padding-left: 40px;
}
&.input-valid, &.input-invalid {
background-repeat: no-repeat;
background-position: center right 16px;
}
&.input-valid {
padding-right: 37px;
background-image: url(~client/assets/svg/check.svg);
background-size: 13px 10px;
}
&.input-invalid {
padding-right: 40px;
background-image: url(~client/assets/svg/alert.svg);
background-size: 16px 16px;
}
}
// Checkboxes and radios
$bg-focused-active-control: #4f2993;
$bg-disabled-control: #34303a;
.custom-control {
&-description {
padding-top: 3px;
padding-left: 3px;
}
& .custom-control-indicator {
width: 18px;
height: 18px;
background-size: 75% 75%;
background-color: transparent;
border: 2px solid $gray-200;
transition-property: box-shadow;
}
& .custom-control-input {
display: none;
}
}
.custom-checkbox {
.custom-control-indicator {
border-radius: 2px;
}
.custom-control-input {
&:checked~.custom-control-indicator {
background-image: url(~client/assets/svg/checkbox-white.svg);
background-color: $purple-400;
border-color: $purple-400;
}
&:active~.custom-control-indicator {
background-color: inherit;
}
&:focus:not(:checked):not(:disabled)~.custom-control-indicator, &:active:not(:checked):not(:disabled)~.custom-control-indicator {
box-shadow: 0 0 0 6px rgba($bg-focused-active-control, 0.1);
}
&:focus:checked:not(:disabled)~.custom-control-indicator, &:active:checked:not(:disabled)~.custom-control-indicator {
box-shadow: 0 0 0 6px rgba($bg-focused-active-control, 0.1);
border-color: $purple-400;
background-color: $purple-400;
}
&:focus:disabled~.custom-control-indicator, &:active:disabled~.custom-control-indicator {
box-shadow: 0 0 0 6px rgba($bg-disabled-control, 0.1);
}
&:disabled:checked~.custom-control-indicator {
border-color: $gray-400;
background-color: $gray-400;
}
&:disabled:not(:checked)~.custom-control-indicator {
border-color: $gray-400;
background-color: transparent;
}
}
}
@mixin custom-radio-checked-icon ($bg-color) {
background-image: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='#{$bg-color}'/%3E%3C/svg%3E"), "#", "%23");
}
.custom-radio .custom-control-input {
&:checked~.custom-control-indicator {
@include custom-radio-checked-icon($purple-400);
background-color: $gray-700;
background-size: 12px 12px;
border-color: $purple-400;
}
&:active~.custom-control-indicator {
background-color: inherit;
}
&:focus:not(:checked):not(:disabled)~.custom-control-indicator, &:active:not(:checked):not(:disabled)~.custom-control-indicator {
box-shadow: 0 0 0 6px rgba($bg-focused-active-control, 0.1);
}
&:focus:checked:not(:disabled)~.custom-control-indicator, &:active:checked:not(:disabled)~.custom-control-indicator {
box-shadow: 0 0 0 6px rgba($bg-focused-active-control, 0.1);
border-color: $purple-400;
background-color: rgba($bg-focused-active-control, 0.1);
}
&:disabled:checked~.custom-control-indicator {
border-color: $gray-400;
background-color: transparent;
@include custom-radio-checked-icon($gray-400);
}
&:disabled:not(:checked)~.custom-control-indicator {
border-color: $gray-300;
background-color: transparent;
}
&:focus:disabled~.custom-control-indicator, &:active:disabled~.custom-control-indicator {
box-shadow: 0 0 0 6px rgba($bg-disabled-control, 0.1);
border-color: $gray-300;
background-color: rgba($bg-disabled-control, 0.1);
}
&:focus:disabled:checked~.custom-control-indicator, &:active:disabled:checked~.custom-control-indicator {
border-color: $gray-400;
}
}

View File

@@ -1,3 +0,0 @@
.nested-field {
padding-left: 1.5rem;
}

View File

@@ -1,12 +1,44 @@
// CSS that doesn't belong to any specific Vue compoennt
@import './forms';
// Functions
// From Bootstrap 4
// Replace `$search` with `$replace` in `$string`
// @author Hugo Giraudel
// @param {String} $string - Initial string
// @param {String} $search - Substring to replace
// @param {String} $replace ('') - New value
// @return {String} - Updated string
@function str-replace($string, $search, $replace: "") {
$index: str-index($string, $search);
@if $index {
@return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
}
@return $string;
}
// Variables
@import './colors';
html, body {
height: 100%;
background: $gray-700;
}
body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
* {
transition-duration: .15s;
transition-property: border-color, box-shadow, color;
transition-timing-function: ease-in;
}
// Global styles
@import './typography';
@import './form';
@import './button';
@import './badge';
@import './dropdown';
@import './popover';
@import './item';
// Generic components
@import './page';

View File

@@ -0,0 +1,89 @@
// TODO move to item component?
.items > div {
display: inline-block;
margin-right: 24px;
}
.items > div:last-of-type {
margin-right: 0px;
}
.item-wrapper {
position: relative;
display: inline-block;
margin-bottom: 12px;
}
.items-one-line .item-wrapper {
margin-bottom: 8px;
}
.item {
position: relative;
width: 94px;
height: 92px;
border-radius: 2px;
background: $white;
box-shadow: 0 2px 2px 0 rgba($black, 0.15), 0 1px 4px 0 rgba($black, 0.1);
border: 1px solid transparent;
&-empty {
background: $gray-10;
box-shadow: none;
cursor: auto;
}
&:hover {
box-shadow: 0 4px 4px 0 rgba($black, 0.16), 0 1px 8px 0 rgba($black, 0.12);
border-color: $purple-500;
}
}
.drawer-content .item:hover {
border-color: transparent;
box-shadow: none;
}
.item .item-content {
position: absolute;
width: 40px;
height: 40px;
padding: 4px;
top: 22px;
right: 26px;
display: block;
}
.item-label {
display: block;
width: 94px;
font-size: 12px;
font-weight: bold;
line-height: 1.33;
text-align: center;
color: $gray-400;
margin-top: 4px;
}
.item > .badge {
cursor: pointer;
display: none;
position: absolute;
top: -12px;
left: -12px;
color: $gray-400;
background: $white;
padding: 4.5px 6px;
box-shadow: 0 1px 1px 0 rgba($black, 0.12);
&.item-selected-badge {
display: block;
background: $teal-50;
color: $white;
}
}
.item:hover > .badge {
display: block;
}

View File

@@ -0,0 +1,14 @@
.standard-sidebar {
background: $gray-600;
padding: 24px;
font-size: 14px;
line-height: 1.43;
}
.standard-page {
padding: 24px;
}
.page-header {
color: $purple-200;
}

View File

@@ -0,0 +1,41 @@
.popover {
border-radius: 4px;
background-color: rgba(52, 49, 58, 0.96);
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
&::after, &::before {
display: none;
}
}
.popover-content {
padding: 12px 16px;
text-align: center;
color: $gray-500;
font-size: 12px;
line-height: 1.33;
}
.popover-content-title {
color: $white;
line-height: 1.14;
}
.popover-content-text {
margin-bottom: 16px;
}
.popover-content-attr {
width: 50%;
display: inline-block;
font-weight: bold;
margin-bottom: 4px;
&-key {
color: $white;
}
&-val {
color: $green-10;
}
}

View File

@@ -0,0 +1,51 @@
body {
font-family: 'Roboto', sans-serif;
color: $gray-50;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 14px;
line-height: 1.43;
}
.small-text {
font-size: 12px;
font-style: italic;
line-height: 1.33;
color: $gray-200;
}
a {
cursor: pointer;
}
// Headers
h1, h2, h3, h4, h5, h6 {
font-family: 'Roboto Condensed', sans-serif;
font-weight: bold;
color: $gray-10;
}
h1 {
font-size: 24px;
line-height: 1.67;
margin-bottom: 24px;
}
h2 {
font-size: 20px;
line-height: 1.2;
margin-bottom: 16px;
}
h3 {
font-size: 16px;
line-height: 1.5;
color: $gray-50;
margin-bottom: 9px;
}
h4 {
font-size: 14px;
line-height: 1.43;
}

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#FF6165" fill-rule="evenodd" d="M0 1.994C0 .893.895 0 1.994 0h12.012C15.107 0 16 .895 16 1.994v12.012A1.995 1.995 0 0 1 14.006 16H1.994A1.995 1.995 0 0 1 0 14.006V1.994zM2 2v12h12V2H2zm5 2h2v5H7V4zm0 6h2v2H7v-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 322 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="10" viewBox="0 0 13 10">
<path fill="#24CC8F" fill-rule="evenodd" d="M4.662 9.832c-.312 0-.61-.123-.831-.344L0 5.657l1.662-1.662 2.934 2.934L10.534 0l1.785 1.529-6.764 7.893a1.182 1.182 0 0 1-.848.409l-.045.001"/>
</svg>

After

Width:  |  Height:  |  Size: 284 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="10" viewBox="0 0 13 10">
<path fill="#FFF" fill-rule="evenodd" d="M4.662 9.832c-.312 0-.61-.123-.83-.344L0 5.657l1.662-1.662 2.934 2.934L10.534 0l1.785 1.529-6.764 7.893a1.182 1.182 0 0 1-.848.409l-.045.001"/>
</svg>

After

Width:  |  Height:  |  Size: 280 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#878190" fill-rule="evenodd" d="M15.7 14.3l-4.8-4.8c.7-1 1.1-2.2 1.1-3.5 0-3.3-2.7-6-6-6S0 2.7 0 6s2.7 6 6 6c1.3 0 2.5-.4 3.5-1.1l4.8 4.8c.4.4 1 .4 1.4 0 .4-.4.4-1 0-1.4zM6 10c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z"/>
</svg>

After

Width:  |  Height:  |  Size: 334 B

View File

@@ -7,8 +7,8 @@
.progress-container.d-flex
img.icon(src="~assets/header/png/health@3x.png")
.progress
.progress-bar.bg-danger(:style="{width: `${percent(user.stats.hp, maxHealth)}%`}")
span {{user.stats.hp | round}} / {{maxHealth}}
.progress-bar.bg-danger(:style="{width: `${percent(user.stats.hp, MAX_HEALTH)}%`}")
span {{user.stats.hp | round}} / {{MAX_HEALTH}}
.progress-container.d-flex
img.icon(src="~assets/header/png/experience@3x.png")
.progress
@@ -21,14 +21,20 @@
span {{user.stats.mp | round}} / {{maxMP}}
</template>
<style scoped>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
// TODO move to colors.scss if used in other places
$header-dark-background: #271B3D;
$header-text-color: #D5C8FF;
/* TODO refactor: only partially ported from SemanticUI; */
#app-header {
padding-left: 14px;
margin-top: 56px;
background: #36205d;
background: $purple-50;
height: 192px;
color: #d5c8ff;
color: $header-text-color;
}
.character-name {
@@ -36,7 +42,7 @@
font-size: 16px;
margin-top: 32px;
line-height: 1.5;
color: #fff;
color: $white;
font-weight: bold;
}
@@ -51,7 +57,6 @@
#header-avatar {
margin-top: 24px;
margin-right: 1rem;
box-shadow: 0 2px 4px 0 rgba(53, 32, 93, 0.4);
}
.progress-container {
@@ -75,7 +80,7 @@
margin: 0px;
border-radius: 0px;
height: 12px;
background-color: rgba(0, 0, 0, 0.35);
background-color: $header-dark-background;
}
.progress-container > .progress > .progress-bar {
@@ -90,25 +95,21 @@ import Avatar from './avatar';
import { mapState } from 'client/libs/store';
import { toNextLevel } from '../../common/script/statHelpers';
import { MAX_HEALTH as maxHealth } from '../../common/script/constants';
import statsComputed from '../../common/script/libs/statsComputed';
import percent from '../../common/script/libs/percent';
export default {
name: 'header',
components: {
Avatar,
},
methods: {
percent,
},
data () {
return {
maxHealth,
};
},
computed: {
...mapState({user: 'user.data'}),
...mapState({
user: 'user.data',
MAX_HEALTH: 'constants.MAX_HEALTH',
}),
maxMP () {
return statsComputed(this.user).maxMP;
},

View File

@@ -52,8 +52,10 @@ nav.navbar.navbar-inverse.fixed-top.navbar-toggleable-sm
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
nav.navbar {
background: #432874 url(~assets/header/png/bits.png) right no-repeat;
background: $purple-100 url(~assets/header/png/bits.png) right no-repeat;
padding: 0 1.5rem;
height: 56px;
}
@@ -66,26 +68,26 @@ nav.navbar {
}
}
$active-purple: #6133b4;
.nav-item {
.nav-link {
color: #fff;
font-size: 16px;
color: $white;
font-weight: bold;
line-height: 1.5;
padding: 1rem 1.5rem;
transition: none;
}
&:hover {
.nav-link {
color: #fff;
background: $active-purple;
color: $white;
background: $purple-300;
}
}
&.active,&:hover {
.nav-link {
box-shadow: 0px -4px 0px #6133b4 inset;
box-shadow: 0px -4px 0px $purple-300 inset;
}
}
}
@@ -97,25 +99,38 @@ $active-purple: #6133b4;
}
.dropdown-menu:not(.user-dropdown) {
background: $active-purple;
background: $purple-300;
border-radius: 0px;
border: none;
box-shadow: none;
padding: 0px;
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
.dropdown-item {
color: #fff;
font-size: 16px;
box-shadow: none;
color: $white;
border: none;
&.active {
background: $active-purple;
background: $purple-300;
}
&:hover {
background: #4f2a93;
background: $purple-200;
&:last-child {
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
}
}
}
}
.item-with-icon {
color: #fff;
color: $white;
font-weight: bold;
padding: 0.75rem 0;
padding-left: 1rem;

View File

@@ -67,7 +67,6 @@
<script>
export default {
name: 'avatar',
props: {
user: {
type: Object,

View File

@@ -0,0 +1,159 @@
<template lang="pug">
.drawer-container
.drawer-title(@click="open = !open")
| {{title}}
img.drawer-toggle-icon(src="~assets/drawer/minimize.svg", v-if="open")
img.drawer-toggle-icon.closed(src="~assets/drawer/expand.svg", v-else)
transition(name="slide-up", @afterLeave="adjustPagePadding", @afterEnter="adjustPagePadding")
.drawer-content(v-show="open")
slot(name="drawer-header")
.drawer-slider
slot(name="drawer-slider")
div.message(v-if="errorMessage != null")
.content {{ errorMessage }}
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
.drawer-container {
z-index: 19;
position: fixed;
font-size: 12px;
font-weight: bold;
bottom: 0;
left: 19%;
right: 3%;
max-width: 80%;
@media screen and (min-width: 1241px) {
max-width: 968px;
// 16.67% is the width of the .col-2 sidebar
left: calc((100% + 16.67% - 968px) / 2);
right: 0%;
}
}
.drawer-toggle-icon {
float: right;
margin: 10px;
&.closed {
margin-top: 5px;
}
}
.drawer-title {
background-color: $gray-10;
box-shadow: 0 1px 2px 0 rgba($black, 0.2);
cursor: pointer;
border-top-right-radius: 8px;
border-top-left-radius: 8px;
text-align: center;
line-height: 1.67;
color: $white;
padding: 6px 0;
}
.drawer-content {
line-height: 1.33;
max-height: 300px;
background-color: $gray-50;
color: $gray-500;
box-shadow: 0 2px 16px 0 rgba($black, 0.3);
padding-top: 6px;
padding-left: 24px;
padding-right: 24px;
}
.drawer-tab {
&-container {
display: flex;
margin-left: 24px;
& > div {
flex: 1;
}
}
&-text {
font-size: 12px;
font-weight: bold;
line-height: 1.67;
text-align: center;
color: $white;
border-bottom: 2px solid transparent;
padding: 0px 8px 8px 8px;
&-active {
border-color: $purple-400;
}
}
}
.drawer-slider {
padding: 12px 0 0 24px;
margin-left: -24px;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
position: relative;
& .message {
display: flex;
align-items: center;
justify-content: center;
top: calc(50% - 30px);
left: 24px;
right: 0;
position: absolute;
& .content {
background-color: rgba($gray-200, 0.5);
border-radius: 8px;
padding: 12px;
}
}
}
.slide-up-enter-active, .slide-up-leave-active {
transition-property: all;
transition-duration: 450ms;
transition-timing-function: cubic-bezier(0.445, 0.05, 0.55, 0.95);
}
.slide-up-enter, .slide-up-leave-to {
max-height: 0;
}
</style>
<script>
export default {
props: {
title: {
type: String,
required: true,
},
errorMessage: {
type: String,
},
},
data () {
return {
open: true,
};
},
methods: {
adjustPagePadding () {
let minPaddingBottom = 20;
let drawerHeight = this.$el.offsetHeight;
let standardPage = document.getElementsByClassName('standard-page')[0];
standardPage.style.paddingBottom = `${drawerHeight + minPaddingBottom}px`;
},
},
mounted () {
// Make sure the page has enough space so the drawer does not overlap content
this.adjustPagePadding();
},
};
</script>

View File

@@ -0,0 +1,292 @@
<template lang="pug">
.row
.col-2.standard-sidebar
.form-group
input.form-control.input-search(type="text", v-model="searchText", :placeholder="$t('search')")
.form
h2(v-once) {{ $t('filter') }}
h3 {{ this.groupBy === 'type' ? 'Type' : $t('class') }}
.form-group
.form-check(
v-for="group in itemsGroups",
:key="group.key",
)
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", v-model="viewOptions[group.key].selected")
span.custom-control-indicator
span.custom-control-description(v-once) {{ $t(group.label) }}
.col-10.standard-page
.clearfix
h1.float-left.mb-0.page-header(v-once) {{ $t('equipment') }}
.float-right
b-dropdown(text="Sort by", right=true)
b-dropdown-item(href="#") Option 1
b-dropdown-item(href="#") Option 2
b-dropdown-item(href="#") Option 3
b-dropdown(text="Group by", right=true)
b-dropdown-item(@click="groupBy = 'type'", :class="{'dropdown-item-active': groupBy === 'type'}") Type
b-dropdown-item(@click="groupBy = 'class'", :class="{'dropdown-item-active': groupBy === 'class'}") {{ $t('class') }}
drawer(
:title="$t('equipment')",
:errorMessage="(costume && !user.preferences.costume) ? $t('costumeDisabled') : null",
)
div(slot="drawer-header")
.drawer-tab-container
.drawer-tab.text-right
a.drawer-tab-text(
@click="costume = false",
:class="{'drawer-tab-text-active': costume === false}",
) {{ $t('equipment') }}
.clearfix
.drawer-tab.float-left
a.drawer-tab-text(
@click="costume = true",
:class="{'drawer-tab-text-active': costume === true}",
) {{ $t('costume') }}
b-popover(
:triggers="['hover']",
:placement="'top'"
)
span(slot="content")
.popover-content-title {{ $t(drawerPreference+'PopoverText') }}
toggle-switch.float-right(
:label="$t(costume ? 'useCostume' : 'autoEquipBattleGear')",
:checked="user.preferences[drawerPreference]",
@change="changeDrawerPreference",
)
.items.items-one-line(slot="drawer-slider")
item(
v-for="(label, group) in gearTypesToStrings",
:key="group",
:item="flatGear[activeItems[group]]",
:label="$t(label)",
:selected="true",
:popoverPosition="'top'",
:starVisible="!costume || user.preferences.costume",
@click="equip",
)
div(
v-for="group in itemsGroups",
v-if="viewOptions[group.key].selected",
:key="group.key",
)
h2
| {{ $t(group.label) }}
|
span.badge.badge-pill.badge-default {{items[group.key].length}}
.items
item(
v-for="(item, index) in items[group.key]",
v-if="viewOptions[group.key].open || index < itemsPerLine",
:item="item",
:key="item.key",
:selected="activeItems[item.type] === item.key",
:starVisible="!costume || user.preferences.costume",
@click="equip",
)
div(v-if="items[group.key].length === 0")
p(v-once) {{ $t('noGearItemsOfType', { type: $t(group.label) }) }}
a.btn.btn-show-more(
v-if="items[group.key].length > itemsPerLine",
@click="viewOptions[group.key].open = !viewOptions[group.key].open"
) {{ viewOptions[group.key].open ? $t('showLessGearItems', { type: $t(group.label) }) : $t('showAllGearItems', { type: $t(group.label), items: items[group.key].length }) }}
</template>
<style lang="scss" scoped>
h2 {
margin-top: 24px;
}
</style>
<script>
import { mapState } from 'client/libs/store';
import each from 'lodash/each';
import map from 'lodash/map';
import throttle from 'lodash/throttle';
import bDropdown from 'bootstrap-vue/lib/components/dropdown';
import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item';
import bPopover from 'bootstrap-vue/lib/components/popover';
import toggleSwitch from 'client/components/ui/toggleSwitch';
import Item from 'client/components/inventory/item';
import Drawer from 'client/components/inventory/drawer';
export default {
components: {
Item,
Drawer,
bDropdown,
bDropdownItem,
bPopover,
toggleSwitch,
},
data () {
return {
itemsPerLine: 9,
searchText: null,
searchTextThrottled: null,
costume: false,
groupBy: 'type', // or 'class' TODO move to router?
gearTypesToStrings: Object.freeze({ // TODO use content.itemList?
headAccessory: 'headAccessoryCapitalized',
head: 'headgearCapitalized',
eyewear: 'eyewear',
weapon: 'weaponCapitalized',
shield: 'offhandCapitalized',
armor: 'armorCapitalized',
body: 'body',
back: 'back',
}),
gearClassesToStrings: Object.freeze({
warrior: 'warrior', // TODO immediately calculate $(label) instead of all the times
wizard: 'mage',
rogue: 'rogue',
healer: 'healer',
special: 'special',
mystery: 'mystery',
armoire: 'armoireText',
}),
viewOptions: {},
};
},
watch: {
searchText: throttle(function throttleSearch () {
this.searchTextThrottled = this.searchText;
}, 250),
},
methods: {
equip (item) {
this.$store.dispatch('common:equip', {key: item.key, type: this.costume ? 'costume' : 'equipped'});
},
changeDrawerPreference (newVal) {
this.$store.dispatch('user:set', {
[`preferences.${this.drawerPreference}`]: newVal,
});
},
},
computed: {
...mapState({
content: 'content',
user: 'user.data',
ownedItems: 'user.data.items.gear.owned',
equippedItems: 'user.data.items.gear.equipped',
costumeItems: 'user.data.items.gear.costume',
flatGear: 'content.gear.flat',
}),
drawerPreference () {
return this.costume === true ? 'costume' : 'autoEquip';
},
activeItems () {
return this.costume === true ? this.costumeItems : this.equippedItems;
},
gearItemsByType () {
const searchText = this.searchTextThrottled;
const gearItemsByType = {};
each(this.gearTypesToStrings, (string, type) => {
gearItemsByType[type] = [];
});
each(this.ownedItems, (isOwned, gearKey) => {
if (isOwned === true) {
const ownedItem = this.flatGear[gearKey];
const isSearched = !searchText || ownedItem.text().toLowerCase().indexOf(searchText) !== -1;
if (ownedItem.klass !== 'base' && isSearched) {
const type = ownedItem.type;
const isEquipped = this.activeItems[type] === ownedItem.key;
const viewOptions = this.viewOptions[type];
const firstRender = viewOptions.firstRender;
const itemsInFirstPosition = viewOptions.itemsInFirstPosition;
// Render selected items in first postion only for the first render
if (itemsInFirstPosition.indexOf(ownedItem.key) !== -1 && firstRender === false) {
gearItemsByType[type].unshift(ownedItem);
} else if (isEquipped === true && firstRender === true) {
gearItemsByType[type].unshift(ownedItem);
itemsInFirstPosition.push(ownedItem.key);
} else {
gearItemsByType[type].push(ownedItem);
}
}
}
});
each(this.gearTypesToStrings, (string, type) => {
this.viewOptions[type].firstRender = false;
});
return gearItemsByType;
},
gearItemsByClass () {
const searchText = this.searchTextThrottled;
const gearItemsByClass = {};
each(this.gearClassesToStrings, (string, klass) => {
gearItemsByClass[klass] = [];
});
each(this.ownedItems, (isOwned, gearKey) => {
if (isOwned === true) {
const ownedItem = this.flatGear[gearKey];
const klass = ownedItem.klass;
const isSearched = !searchText || ownedItem.text().toLowerCase().indexOf(searchText) !== -1;
if (klass !== 'base' && isSearched) {
const isEquipped = this.activeItems[ownedItem.type] === ownedItem.key;
const viewOptions = this.viewOptions[klass];
const firstRender = viewOptions.firstRender;
const itemsInFirstPosition = viewOptions.itemsInFirstPosition;
// Render selected items in first postion only for the first render
if (itemsInFirstPosition.indexOf(ownedItem.key) !== -1 && firstRender === false) {
gearItemsByClass[klass].unshift(ownedItem);
} else if (isEquipped === true && firstRender === true) {
gearItemsByClass[klass].unshift(ownedItem);
itemsInFirstPosition.push(ownedItem.key);
} else {
gearItemsByClass[klass].push(ownedItem);
}
}
}
});
each(this.gearClassesToStrings, (string, klass) => {
this.viewOptions[klass].firstRender = false;
});
return gearItemsByClass;
},
groups () {
return this.groupBy === 'type' ? this.gearTypesToStrings : this.gearClassesToStrings;
},
items () {
return this.groupBy === 'type' ? this.gearItemsByType : this.gearItemsByClass;
},
itemsGroups () {
return map(this.groups, (label, group) => {
this.$set(this.viewOptions, group, {
selected: true,
open: false,
itemsInFirstPosition: [],
firstRender: true,
});
return {
key: group,
label,
};
});
},
},
};
</script>

View File

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

View File

@@ -0,0 +1,68 @@
<template lang="pug">
b-popover(
:triggers="['hover']",
:placement="popoverPosition",
v-if="item && item.key.indexOf('_base_0') === -1",
)
span(slot="content")
h4.popover-content-title {{ item.text() }}
.popover-content-text {{ item.notes() }}
.popover-content-attr(v-for="attr in ATTRIBUTES")
span.popover-content-attr-key {{ `${$t(attr)}: ` }}
span.popover-content-attr-val {{ `+${item[attr]}` }}
.item-wrapper
.item
span.badge.badge-pill(
:class="{'item-selected-badge': selected === true}",
@click="click",
v-if="starVisible"
) &#9733;
span.item-content(:class="'shop_' + item.key")
span.item-label(v-if="label") {{ label }}
div(v-else)
.item-wrapper
.item.item-empty
.item-content
span.item-label(v-if="label") {{ label }}
</template>
<script>
import bPopover from 'bootstrap-vue/lib/components/popover';
import { mapState } from 'client/libs/store';
export default {
components: {
bPopover,
},
props: {
item: {
type: Object,
},
selected: {
type: Boolean,
},
starVisible: {
type: Boolean,
},
label: {
type: String,
},
popoverPosition: {
type: String,
default: 'bottom',
},
},
computed: {
...mapState({
ATTRIBUTES: 'constants.ATTRIBUTES',
}),
},
methods: {
click () {
this.$emit('click', this.item);
},
},
};
</script>

View File

@@ -1,6 +1,6 @@
<template lang="pug">
.row
.col-3
.col-2.standard-sidebar
.form-group
input.form-control(type="text", :placeholder="$t('search')")
@@ -41,30 +41,33 @@
input.form-check-input(type="checkbox")
span {{ $t('special') }}
.col-9
h2 Pets
.inventory-item-container(v-for="pet in listAnimals('pet', content.dropEggs, content.dropHatchingPotions)")
.col-10.standard-page
h4 Pets
.inventory-item-container(v-for="pet in dropPets")
.PixelPaw
.btn.btn-secondary.d-block(@click="open.dropPets = !open.dropPets") {{ open.dropPets ? 'Close' : 'Open' }}
h2 Magic Potions Pets
ul
li(v-for="pet in listAnimals('pet', content.dropEggs, content.premiumHatchingPotions)") {{pet}}
.inventory-item-container(v-for="pet in magicPets")
.PixelPaw
.btn.btn-secondary.d-block(@click="open.magicPets = !open.magicPets") {{ open.magicPets ? 'Close' : 'Open' }}
h2 Quest Pets
ul
li(v-for="pet in listAnimals('pet', content.questEggs, content.dropHatchingPotions)") {{pet}}
.inventory-item-container(v-for="pet in questPets")
.PixelPaw
.btn.btn-secondary.d-block(@click="open.questPets = !open.questPets") {{ open.questPets ? 'Close' : 'Open' }}
h2 Rare Pets
ul
li(v-for="pet in listAnimals('pet', content.dropEggs, content.dropHatchingPotions)") {{pet}}
.inventory-item-container(v-for="pet in rarePets")
.PixelPaw
.btn.btn-secondary.d-block(@click="open.rarePets = !open.rarePets") {{ open.rarePets ? 'Close' : 'Open' }}
h2 Mounts
h2 Quest Mounts
h2 Rare Mounts
</template>
<style>
<style lang="scss">
.inventory-item-container {
padding: 20px;
border: 1px solid;
@@ -76,15 +79,45 @@
import { mapState } from 'client/libs/store';
import each from 'lodash/each';
// TODO Normalize special pets and mounts
// import Store from 'client/store';
// import deepFreeze from 'client/libs/deepFreeze';
// const specialMounts =
export default {
data () {
return {
open: {
dropPets: false,
magicPets: false,
questPets: false,
rarePets: false,
},
};
},
computed: {
...mapState(['content']),
dropPets () {
return this.listAnimals('pet', this.content.dropEggs, this.content.dropHatchingPotions, this.open.dropPets);
},
magicPets () {
return this.listAnimals('pet', this.content.dropEggs, this.content.premiumHatchingPotions, this.open.magicPets);
},
questPets () {
return this.listAnimals('pet', this.content.questEggs, this.content.dropHatchingPotions, this.open.questPets);
},
rarePets () {
return this.listAnimals('pet', this.content.dropEggs, this.content.dropHatchingPotions, this.open.rarePets);
},
},
methods: {
listAnimals (type, eggSource, potionSource) {
listAnimals (type, eggSource, potionSource, isOpen = false) {
let animals = [];
let iteration = 0;
each(eggSource, (egg) => {
if (iteration === 1 && !isOpen) return false;
iteration++;
each(potionSource, (potion) => {
let animalKey = `${egg.key}-${potion.key}`;
animals.push(this.content[`${type}Info`][animalKey].text());

View File

@@ -1,6 +1,6 @@
<template lang="pug">
.row
.col
h2 Page
h1.page-header Page
p {{ $route.path }}
</template>

View File

@@ -0,0 +1,31 @@
<template lang="pug">
nav.nav.d-flex.justify-content-center.secondary-menu
slot
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.secondary-menu {
background: $gray-600;
box-shadow: 0 1px 2px 0 rgba($black, 0.2);
z-index: 9;
}
.nav-link {
font-size: 16px;
line-height: 1.5;
padding: 16px 24px;
font-weight: bold;
color: $gray-50;
&.active {
color: $purple-200;
box-shadow: 0px -4px 0px $purple-300 inset;
}
&:hover {
background: $gray-500;
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template lang="pug">
.row
.col-3
.col-2.standard-sidebar
.form-group
input.form-control(type="text", :placeholder="$t('search')")
@@ -22,8 +22,8 @@
input.form-check-input(type="checkbox")
span Animals
.col-9
h2(v-once) {{ $t('publicGuilds') }}
.col-10.standard-page
h1.page-header(v-once) {{ $t('publicGuilds') }}
public-guild-item(v-for="guild in guilds", :key='guild._id', :guild="guild")
mugen-scroll(
:handler="fetchGuilds",
@@ -38,7 +38,7 @@
import axios from 'axios';
import MugenScroll from 'vue-mugen-scroll';
import PublicGuildItem from './publicGuildItem';
import { GUILDS_PER_PAGE } from 'common/script/constants';
import { mapState } from 'client/libs/store';
export default {
components: { PublicGuildItem, MugenScroll },
@@ -50,6 +50,11 @@ export default {
guilds: [],
};
},
computed: {
...mapState({
GUILDS_PER_PAGE: 'constants.GUILDS_PER_PAGE',
}),
},
created () {
this.fetchGuilds();
},
@@ -65,7 +70,7 @@ export default {
});
let guilds = response.data.data;
this.guilds.push(...guilds);
if (guilds.length < GUILDS_PER_PAGE) this.hasLoadedAllGuilds = true;
if (guilds.length < this.GUILDS_PER_PAGE) this.hasLoadedAllGuilds = true;
this.lastPageLoaded++;
this.loading = false;
},

View File

@@ -3,7 +3,7 @@
.row(v-if="guild")
.clearfix.col-12
.float-left
h2 {{guild.name}}
h1.page-header {{guild.name}}
strong.float-left {{$t('groupLeader')}}
span.float-left : {{guild.leader.profile.name}}
.float-right

View File

@@ -1,7 +1,7 @@
<template lang="pug">
.row
.col-12
h2 Conversation
h1.page-header Conversation
.card(v-for="message in messages")
.card-block
strong {{message.from}}

View File

@@ -1,7 +1,7 @@
<template lang="pug">
.row
.col-12
h2(v-once) {{ $t('inbox') }}
h1.page-header(v-once) {{ $t('inbox') }}
.card(v-for="conversation in conversations")
.card-block
router-link(:to="{ name: 'conversation', params: { id: conversation.fromUserId } }")

View File

@@ -1,10 +1,19 @@
<template lang="pug">
.row
.col-12
nav.nav
secondary-menu.col-12
router-link.nav-link(:to="{name: 'tavern'}", exact) {{ $t('tavern') }}
router-link.nav-link(:to="{name: 'inbox'}") {{ $t('inbox') }}
router-link.nav-link(:to="{name: 'guildsDiscovery'}") {{ $t('guilds') }}
.col-12
router-view
</template>
<script>
import SecondaryMenu from 'client/components/secondaryMenu';
export default {
components: {
SecondaryMenu,
},
};
</script>

View File

@@ -1,6 +1,6 @@
<template lang="pug">
.row
h2.col-12 Tavern
h1.page-header.col-12 Tavern
// TODO Example code based on Semantic UI .ui.grid
.four.wide.column
h2.ui.dividing.header SideMenu

View File

@@ -0,0 +1,119 @@
<template lang="pug">
.clearfix.toggle-switch-container
.float-left.toggle-switch-description {{ label }}
.toggle-switch.float-left
input.toggle-switch-checkbox(
type='checkbox', :id="id",
@change="$emit('change', $event.target.checked)",
:checked="checked",
)
label.toggle-switch-label(:for="id")
span.toggle-switch-inner
span.toggle-switch-switch
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.toggle-switch-container {
margin-top: 6px;
}
.toggle-switch {
position: relative;
width: 40px;
user-select: none;
margin-left: 9px;
}
.toggle-switch-description {
height: 20px;
border-bottom: 1px dashed $gray-200;
}
.toggle-switch-checkbox {
display: none;
}
.toggle-switch-label {
display: block;
overflow: hidden;
cursor: pointer;
border-radius: 100px;
margin-bottom: 0px;
margin-top: 3px;
}
.toggle-switch-inner {
display: block;
width: 200%;
margin-left: -100%;
transition: margin 0.3s ease-in 0s;
}
.toggle-switch-inner:before, .toggle-switch-inner:after {
display: block;
float: left;
width: 50%;
height: 16px;
padding: 0;
}
.toggle-switch-inner:before {
content: "";
padding-left: 10px;
background-color: $purple-400;
}
.toggle-switch-inner:after {
content: "";
padding-right: 10px;
background-color: $gray-200;
text-align: right;
}
.toggle-switch-switch {
box-shadow: 0 1px 2px 0 rgba($black, 0.32);
display: block;
width: 20px;
margin: -2px;
margin-top: 1px;
height: 20px;
background: $white;
position: absolute;
top: 0;
bottom: 0;
right: 22px;
border-radius: 100px;
transition: all 0.3s ease-in 0s;
}
.toggle-switch-checkbox:checked + .toggle-switch-label .toggle-switch-inner {
margin-left: 0;
}
.toggle-switch-checkbox:checked + .toggle-switch-label .toggle-switch-switch {
right: 0px;
}
</style>
<script>
export default {
data () {
return {
// A value is required for the required for the for and id attributes
id: Math.random(),
};
},
props: {
label: {
type: String,
required: true,
},
checked: {
type: Boolean,
default: false,
},
},
};
</script>

View File

@@ -1,5 +1,136 @@
<template lang="pug">
.row
.col-12
.row
.col-3.p-4
h3 Input
input.form-control(type="text", placeholder="Placeholder")
.col-3.p-4
h3 Input Disabled
input.form-control(type="text", placeholder="Placeholder", disabled)
.col-3.p-4
h3 Input With Icon
input.form-control.input-search(type="text", placeholder="Placeholder")
.col-3.p-4
h3 Input With Icon Disabled
input.form-control.input-search(type="text", placeholder="Placeholder", disabled)
.col-3.p-4
h3 Input Valid
input.form-control.input-valid(type="text", placeholder="Placeholder")
.col-3.p-4
h3 Input Invalid
input.form-control.input-invalid(type="text", placeholder="Placeholder")
.row
.col-6.p-4
h3 Textarea
textarea.form-control(rows="5", cols="50")
.col-6.p-4
h3 Textarea Disabled
textarea.form-control(disabled, rows="10", cols="50")
.row
.col-2.p-4
toggleSwitch(label="Toggle Switch")
.row
.col-3.p-4
h3 Checkbox
label.custom-control.custom-checkbox
input.custom-control-input(type='checkbox')
span.custom-control-indicator
span.custom-control-description Check this custom checkbox
.col-3.p-4
h3 Checkbox Disabled Checked
label.custom-control.custom-checkbox
input.custom-control-input(type='checkbox', disabled, checked)
span.custom-control-indicator
span.custom-control-description Check this custom checkbox
.col-3.p-4
h3 Checkbox Disabled Not Checked
label.custom-control.custom-checkbox
input.custom-control-input(type='checkbox', disabled)
span.custom-control-indicator
span.custom-control-description Check this custom checkbox
.col-6.p-4
h3 Radio Button
form
label.custom-control.custom-radio
input#radio1.custom-control-input(name='radio', type='radio')
span.custom-control-indicator
span.custom-control-description Toggle this custom radio
label.custom-control.custom-radio
input#radio2.custom-control-input(name='radio', type='radio')
span.custom-control-indicator
span.custom-control-description Toggle this custom radio
.col-3.p-4
h3 Radio Button Disabled Checked
form
label.custom-control.custom-radio
input#radio3.custom-control-input(name='radio', type='radio', disabled, checked)
span.custom-control-indicator
span.custom-control-description Toggle this custom radio
.col-3.p-4
h3 Radio Button Disabled Not Checked
form
label.custom-control.custom-radio
input#radio3.custom-control-input(name='radio', type='radio', disabled)
span.custom-control-indicator
span.custom-control-description Toggle this custom radio
.row
.col-3.p-4
h3 Main Button
button.btn.btn-primary Button
.col-3.p-4
h3 Secondary Button
button.btn.btn-secondary Button
.col-3.p-4
h3 Green Button
button.btn.btn-success Button
.col-3.p-4
h3 Blue Button
button.btn.btn-info Button
.col-3.p-4
h3 Red Button
button.btn.btn-danger Button
.row
.col-3.p-4
h3 Main Button Disabled
button.btn.btn-primary(disabled=true) Button
.col-3.p-4
h3 Secondary Button Disabled
button.btn.btn-secondary(disabled=true) Button
.col-3.p-4
h3 Green Button Disabled
button.btn.btn-success(disabled=true) Button
.col-3.p-4
h3 Blue Button Disabled
button.btn.btn-info(disabled=true) Button
.col-3.p-4
h3 Red Button Disabled
button.btn.btn-danger(disabled=true) Button
.row
.col-6.p-4
h3 Dropdown Menu
b-dropdown(text="Menu", right=false)
b-dropdown-item(href="#") Menu item 1
b-dropdown-item(href="#") Menu item 2
b-dropdown-item(href="#") Menu item 3
b-dropdown-item(href="#") Menu item 4
.col-6.p-4
h3 Dropdown Menu Disabled
b-dropdown(text="Menu", disabled)
b-dropdown-item(href="#") Menu item 1
b-dropdown-item(href="#") Menu item 2
b-dropdown-item(href="#") Menu item 3
b-dropdown-item(href="#") Menu item 4
.row
.col-6.p-4
h1 Heading 1
h2 Heading 2
h3 Heading 3
h4 Heading 4
.col-6.p-4
p Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vehicula, purus sit amet sodales pharetra, ipsum ipsum mollis orci, id pharetra velit diam et dui. Sed placerat ipsum eget pharetra rutrum. Ut vitae rutrum lacus, eu imperdiet velit. Pellentesque eu velit cursus, scelerisque dui quis, dapibus magna. Vestibulum molestie sed sapien et ultricies. Nam porta ipsum leo, non congue magna vestibulum a. Etiam dictum felis sit amet augue varius tincidunt. Sed eget urna auctor, convallis felis in, pretium justo. Curabitur aliquet, ligula id tincidunt ullamcorper, orci lorem pharetra neque, in ornare arcu magna accumsan arcu. Maecenas dignissim lorem sed eros accumsan scelerisque.
p.small-text Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vehicula, purus sit amet sodales pharetra, ipsum ipsum mollis orci, id pharetra velit diam et dui.
.row
.col(v-for="taskType in tasksTypes")
h3 {{taskType}}s
ul
@@ -9,10 +140,16 @@
<script>
import Task from './task';
import { mapState } from 'client/libs/store';
import bDropdown from 'bootstrap-vue/lib/components/dropdown';
import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item';
import toggleSwitch from 'client/components/ui/toggleSwitch';
export default {
components: {
Task,
bDropdown,
bDropdownItem,
toggleSwitch,
},
data () {
return {

View File

@@ -4,6 +4,8 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Habitica</title>
<!-- TODO load google fonts separately as @import is slow, find alternative -->
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:700|Roboto:400,400i,700,700i" rel="stylesheet">
</head>
<body>
<!-- #loading-screen needs to be rendered before vue, will be deleted once app is loaded -->

View File

@@ -58,8 +58,7 @@ export default new Vue({
this.$store.dispatch('user:fetch'),
this.$store.dispatch('tasks:fetchUserTasks'),
]).catch((err) => {
console.error(err); // eslint-disable-line no-console
alert('Impossible to fetch user. Copy into localStorage a valid habit-mobile-settings object.');
console.error('Impossible to fetch user. Copy into localStorage a valid habit-mobile-settings object.', err); // eslint-disable-line no-console
});
},
mounted () { // Remove the loading screen when the app is mounted

View File

@@ -1,12 +1,7 @@
// TODO if we only have a single method here, move it to an utility
// a full mixin is not needed
import { TAVERN_ID } from '../../common/script/constants';
export default {
methods: {
isMemberOfGroup (user, group) {
if (group._id === TAVERN_ID) return true;
if (group._id === this.$store.state.constants.TAVERN_ID) return true;
// If the group is a guild, just check for an intersection with the
// current user's guilds, rather than checking the members of the group.

View File

@@ -11,6 +11,7 @@ import UserTasks from './components/userTasks';
// Inventory
import InventoryContainer from './components/inventory/index';
import EquipmentPage from './components/inventory/equipment';
import StablePage from './components/inventory/stable';
// Social
@@ -39,7 +40,7 @@ export default new VueRouter({
component: InventoryContainer,
children: [
{ name: 'inventory', path: '', component: Page },
{ name: 'equipment', path: 'equipment', component: Page },
{ name: 'equipment', path: 'equipment', component: EquipmentPage },
{ name: 'stable', path: 'stable', component: StablePage },
],
},

View File

@@ -0,0 +1,12 @@
import axios from 'axios';
import equipOp from 'common/script/ops/equip';
export function equip (store, params) {
const user = store.state.user.data;
equipOp(user, {params});
axios
.post(`/api/v3/user/equip/${params.type}/${params.key}`);
// TODO
// .then((res) => console.log('equip', res))
// .catch((err) => console.error('equip', err));
}

View File

@@ -1,5 +1,6 @@
import { flattenAndNamespace } from 'client/libs/store/helpers/internals';
import * as common from './common';
import * as user from './user';
import * as tasks from './tasks';
@@ -7,6 +8,7 @@ import * as tasks from './tasks';
// Example: fetch in user.js -> 'user:fetch'
const actions = flattenAndNamespace({
common,
user,
tasks,
});

View File

@@ -1,4 +1,6 @@
import { loadAsyncResource } from 'client/libs/asyncResource';
import setProps from 'lodash/set';
import axios from 'axios';
export function fetch (store, forceLoad = false) { // eslint-disable-line no-shadow
return loadAsyncResource({
@@ -11,3 +13,16 @@ export function fetch (store, forceLoad = false) { // eslint-disable-line no-sha
forceLoad,
});
}
export function set (store, changes) {
const user = store.state.user.data;
for (let key in changes) {
setProps(user, key, changes[key]);
}
axios.put('/api/v3/user', changes);
// TODO
// .then((res) => console.log('set', res))
// .catch((err) => console.error('set', err));
}

View File

@@ -1,6 +1,7 @@
import Store from 'client/libs/store';
import deepFreeze from 'client/libs/deepFreeze';
import content from 'common/script/content/index';
import * as constants from 'common/script/constants';
import { asyncResourceFactory } from 'client/libs/asyncResource';
import actions from './actions';
@@ -20,6 +21,7 @@ export default function () {
// TODO apply freezing to the entire codebase (the server) and not only to the client side?
// NOTE this takes about 10-15ms on a fast computer
content: deepFreeze(content),
constants: deepFreeze(constants),
},
});
}

View File

@@ -0,0 +1,8 @@
{
"costumePopoverText": "Select \"Use Costume\" to equip items to your avatar without affecting the stats from your Battle Gear! This means that you can dress up your avatar in whatever outfit you like while still having your best Battle Gear equipped.",
"autoEquipPopoverText": "Select this option to automatically equip gear as soon as you purchase it.",
"showAllGearItems": "Show All <%= items %> <%= type %> Gear Items",
"showLessGearItems": "Show Less <%= type %> Gear Items",
"noGearItemsOfType": "You don't own any pieces of <%= type %>.",
"costumeDisabled": "You have disabled your costume."
}

View File

@@ -1,4 +1,5 @@
{
"stable": "Stable",
"pets": "Pets",
"activePet": "Active Pet",
"noActivePet": "No Active Pet",

View File

@@ -46,12 +46,20 @@ module.exports = function equip (user, req = {}) {
let item = content.gear.flat[key];
if (user.items.gear[type][item.type] === key) {
user.items.gear[type][item.type] = `${item.type}_base_0`;
user.items.gear[type] = Object.assign(
{},
user.items.gear[type].toObject ? user.items.gear[type].toObject() : user.items.gear[type],
{[item.type]: `${item.type}_base_0`}
);
message = i18n.t('messageUnEquipped', {
itemText: item.text(req.language),
}, req.language);
} else {
user.items.gear[type][item.type] = item.key;
user.items.gear[type] = Object.assign(
{},
user.items.gear[type].toObject ? user.items.gear[type].toObject() : user.items.gear[type],
{[item.type]: item.key}
);
message = handleTwoHanded(user, item, type, req);
}
break;