Client/Inventory/Items (#8734)

* client: start working on Inventory/Items

* i18n changes and fixes

* initial displaying of eggs, food and potions + sorting

* add missing files

* remove comment

* show food, eggs and potions

* add label to dropdowns acting as select menus

* popovers

* move badge to slot and component if necessary, general refactor

* fix quantity ordering

* some special items, reorganize
This commit is contained in:
Matteo Pagliazzi
2017-05-22 16:30:52 +02:00
committed by GitHub
parent f267456a30
commit 9e1f7f3811
16 changed files with 350 additions and 120 deletions

20
website/client/README.md Normal file
View File

@@ -0,0 +1,20 @@
#Running
- Open a terminal and type `npm run client:dev`
- Open a second terminal and type `npm start`
#Preparation Reading
- Vue 2 (https://vuejs.org)
- Webpack (https://webpack.github.io/) is the build system and it includes plugins for code transformation, right now we have: BabelJS for ES6 transpilation, eslint for code style, less and postcss for css compilation. The code comes from https://github.com/vuejs-templates/webpack which is a Webpack template for Vue, with some small modifications to adapt it to our use case. Docs http://vuejs-templates.github.io/webpack/
- Were using `.vue` files that make it possible to have HTML, JS and CSS for each component together in a single location. Theyre implemented as a webpack plugin and the docs can be found here http://vue-loader.vuejs.org/en/
- SemanticUI is the UI framework http://semantic-ui.com/. So far Ive only used the CSS part, it also has JS plugins but Ive yet to use them. It supports theming so if its not too difficult well want to customize the base theme with our own styles instead of writing CSS rules to override the original styling.
The code is in `/website/client`. Were using something very similar to Vuex (equivalent of Reacts Redux) for state management http://vuex.vuejs.org/en/index.html
The API is almost the same except that we dont use mutations but only actions because it would make it difficult to work with common code
The project is developed directly in the `develop` branch as long as well be able to avoid splitting it into a different branch.
So far most of the work has been on the template, so theres no complex logic to understand. The only thing I would suggest you to read about is Vuex for data management: its basically a Flux implementation: theres a central store that hold the data for the entire app, and every change to the data must happen through an action, the data cannot be mutated directly.

View File

@@ -4,6 +4,7 @@
line-height: 1.33;
color: $gray-200;
padding: 4px 8px;
box-shadow: 0 1px 1px 0 rgba($black, 0.12);
}
.badge-pill {
@@ -12,4 +13,17 @@
.badge-default {
background: $gray-500;
box-shadow: none;
}
.badge-item {
position: absolute;
top: -9px;
}
.badge-quantity {
color: $white;
right: -9px;
padding: 4.5px 8.5px;
background-color: $orange-100;
}

View File

@@ -40,7 +40,7 @@
outline: none;
}
&:active, &:hover, &:focus, &.active, &.dropdown-item-active {
&:active, &:hover, &:focus, &.active {
background-color: rgba(#d5c8ff, 0.32);
color: $purple-200;
}
@@ -49,3 +49,12 @@
.dropdown + .dropdown {
margin-left: 12px;
}
.dropdown-label {
font-size: 14px;
font-weight: bold;
line-height: 1.43;
color: $gray-10;
margin-right: 20px;
margin-left: 20px;
}

View File

@@ -1,36 +1,16 @@
// 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
// Variables and Functions
@import './utils';
@import './colors';
html, body {
height: 100%;
background: $gray-700;
}
* {
transition-duration: .15s;
transition-property: border-color, box-shadow, color;
transition-timing-function: ease-in;
}
// Generic components
@import './page';
// Global styles
@import './typography';
@import './form';
@@ -40,5 +20,3 @@ html, body {
@import './popover';
@import './item';
// Generic components
@import './page';

View File

@@ -46,13 +46,13 @@
}
.item .item-content {
position: absolute;
width: 40px;
height: 40px;
padding: 4px;
top: 22px;
right: 26px;
margin: auto;
display: block;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.item-label {
@@ -65,25 +65,3 @@
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

@@ -1,3 +1,8 @@
html, body {
height: 100%;
background: $gray-700;
}
.standard-sidebar {
background: $gray-600;
padding: 24px;

View File

@@ -0,0 +1,16 @@
// 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;
}

View File

@@ -7,10 +7,10 @@ nav.navbar.navbar-inverse.fixed-top.navbar-toggleable-sm
ul.navbar-nav.mr-auto
router-link.nav-item(tag="li", :to="{name: 'tasks'}", exact)
a.nav-link(v-once) {{ $t('tasks') }}
router-link.nav-item.dropdown(tag="li", :to="{name: 'inventory'}", :class="{'active': $route.path.startsWith('/inventory')}")
router-link.nav-item.dropdown(tag="li", :to="{name: 'items'}", :class="{'active': $route.path.startsWith('/inventory')}")
a.nav-link(v-once) {{ $t('inventory') }}
.dropdown-menu
router-link.dropdown-item(:to="{name: 'inventory'}", exact) {{ $t('inventory') }}
router-link.dropdown-item(:to="{name: 'items'}", exact) {{ $t('items') }}
router-link.dropdown-item(:to="{name: 'equipment'}") {{ $t('equipment') }}
router-link.dropdown-item(:to="{name: 'stable'}") {{ $t('stable') }}
router-link.nav-item(tag="li", :to="{name: 'market'}", exact)

View File

@@ -6,7 +6,7 @@
.form
h2(v-once) {{ $t('filter') }}
h3 {{ this.groupBy === 'type' ? 'Type' : $t('class') }}
h3 {{ this.groupBy === 'type' ? $t('equipmentType') : $t('class') }}
.form-group
.form-check(
v-for="group in itemsGroups",
@@ -15,19 +15,21 @@
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) }}
span.custom-control-description(v-once) {{ 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)
span.dropdown-label {{ $t('sortBy') }}
b-dropdown(:text="'Sort 1'", 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') }}
span.dropdown-label {{ $t('groupBy2') }}
b-dropdown(:text="$t(groupBy === 'type' ? 'equipmentType' : 'class')", right=true)
b-dropdown-item(@click="groupBy = 'type'", :active="groupBy === 'type'") {{ $t('equipmentType') }}
b-dropdown-item(@click="groupBy = 'class'", :active="groupBy === 'class'") {{ $t('class') }}
drawer(
:title="$t('equipment')",
@@ -52,7 +54,7 @@
:placement="'top'"
)
span(slot="content")
.popover-content-title {{ $t(drawerPreference+'PopoverText') }}
.popover-content-text {{ $t(drawerPreference+'PopoverText') }}
toggle-switch.float-right(
:label="$t(costume ? 'useCostume' : 'autoEquipBattleGear')",
@@ -66,23 +68,25 @@
:key="group",
:item="flatGear[activeItems[group]]",
:itemContentClass="flatGear[activeItems[group]] ? 'shop_' + flatGear[activeItems[group]].key : null",
:showPopover="!!flatGear[activeItems[group]] && flatGear[activeItems[group]].key.indexOf('_base_0') === -1",
:label="$t(label)",
:selected="true",
:emptyItem="!flatGear[activeItems[group]] || flatGear[activeItems[group]].key.indexOf('_base_0') !== -1",
:label="label",
:popoverPosition="'top'",
:starVisible="!costume || user.preferences.costume",
@click="equip",
)
template(slot="popoverContent", scope="ctx")
equipmentAttributesPopover(:item="ctx.item")
template(slot="itemBadge", scope="ctx")
starBadge(
:selected="true",
:show="!costume || user.preferences.costume",
@click="equip(ctx.item)",
)
div(
v-for="group in itemsGroups",
v-if="viewOptions[group.key].selected",
:key="group.key",
)
h2
| {{ $t(group.label) }}
| {{ group.label }}
|
span.badge.badge-pill.badge-default {{items[group.key].length}}
@@ -92,20 +96,23 @@
v-if="viewOptions[group.key].open || index < itemsPerLine",
:item="item",
:itemContentClass="'shop_' + item.key",
:showPopover="item && item.key.indexOf('_base_0') === -1",
:emptyItem="!item || item.key.indexOf('_base_0') !== -1",
:key="item.key",
:selected="activeItems[item.type] === item.key",
:starVisible="!costume || user.preferences.costume",
@click="equip",
)
template(slot="itemBadge", scope="ctx")
starBadge(
:selected="activeItems[ctx.item.type] === ctx.item.key",
:show="!costume || user.preferences.costume",
@click="equip(ctx.item)",
)
template(slot="popoverContent", scope="ctx")
equipmentAttributesPopover(:item="ctx.item")
div(v-if="items[group.key].length === 0")
p(v-once) {{ $t('noGearItemsOfType', { type: $t(group.label) }) }}
p(v-once) {{ $t('noGearItemsOfType', { type: 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 }) }}
) {{ viewOptions[group.key].open ? $t('showLessGearItems', { type: group.label }) : $t('showAllGearItems', { type: group.label, items: items[group.key].length }) }}
</template>
<style lang="scss" scoped>
@@ -126,13 +133,18 @@ import bPopover from 'bootstrap-vue/lib/components/popover';
import toggleSwitch from 'client/components/ui/toggleSwitch';
import Item from 'client/components/inventory/item';
import EquipmentAttributesPopover from 'client/components/inventory/equipmentAttributesPopover';
import EquipmentAttributesPopover from 'client/components/inventory/equipment/attributesPopover';
import StarBadge from 'client/components/inventory/starBadge';
import Drawer from 'client/components/inventory/drawer';
import i18n from 'common/script/i18n';
export default {
name: 'Equipment',
components: {
Item,
EquipmentAttributesPopover,
StarBadge,
Drawer,
bDropdown,
bDropdownItem,
@@ -145,25 +157,25 @@ export default {
searchText: null,
searchTextThrottled: null,
costume: false,
groupBy: 'type', // or 'class' TODO move to router?
groupBy: 'type', // or 'class'
gearTypesToStrings: Object.freeze({ // TODO use content.itemList?
headAccessory: 'headAccessoryCapitalized',
head: 'headgearCapitalized',
eyewear: 'eyewear',
weapon: 'weaponCapitalized',
shield: 'offhandCapitalized',
armor: 'armorCapitalized',
body: 'body',
back: 'back',
headAccessory: i18n.t('headAccessoryCapitalized'),
head: i18n.t('headgearCapitalized'),
eyewear: i18n.t('eyewear'),
weapon: i18n.t('weaponCapitalized'),
shield: i18n.t('offhandCapitalized'),
armor: i18n.t('armorCapitalized'),
body: i18n.t('body'),
back: i18n.t('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',
warrior: i18n.t('warrior'),
wizard: i18n.t('mage'),
rogue: i18n.t('rogue'),
healer: i18n.t('healer'),
special: i18n.t('special'),
mystery: i18n.t('mystery'),
armoire: i18n.t('armoireText'),
}),
viewOptions: {},
};

View File

@@ -1,7 +1,7 @@
<template lang="pug">
.row
secondary-menu.col-12
router-link.nav-link(:to="{name: 'inventory'}", exact) {{ $t('inventory') }}
router-link.nav-link(:to="{name: 'items'}", exact) {{ $t('items') }}
router-link.nav-link(:to="{name: 'equipment'}") {{ $t('equipment') }}
router-link.nav-link(:to="{name: 'stable'}") {{ $t('stable') }}
.col-12

View File

@@ -1,27 +1,23 @@
<template lang="pug">
div(v-if="emptyItem")
.item-wrapper
.item.item-empty
.item-content
span.item-label(v-if="label") {{ label }}
b-popover(
v-else,
:triggers="['hover']",
:placement="popoverPosition",
v-if="showPopover",
@click="click",
)
span(slot="content")
slot(name="popoverContent", :item="item")
.item-wrapper
.item
span.badge.badge-pill(
:class="{'item-selected-badge': selected === true}",
@click="click",
v-if="starVisible"
) &#9733;
slot(name="itemBadge", :item="item")
span.item-content(:class="itemContentClass")
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>
@@ -39,18 +35,12 @@ export default {
itemContentClass: {
type: String,
},
selected: {
type: Boolean,
},
starVisible: {
type: Boolean,
},
label: {
type: String,
},
showPopover: {
emptyItem: {
type: Boolean,
default: true,
default: false,
},
popoverPosition: {
type: String,

View File

@@ -0,0 +1,154 @@
<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(v-once) {{ $t('equipmentType') }}
.form-group
.form-check(
v-for="group in groups",
:key="group.key",
)
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", v-model="group.selected")
span.custom-control-indicator
span.custom-control-description(v-once) {{ $t(group.key) }}
.col-10.standard-page
.clearfix
h1.float-left.mb-0.page-header(v-once) {{ $t('items') }}
.float-right
span.dropdown-label {{ $t('sortBy') }}
b-dropdown(:text="$t(sortBy)", right=true)
b-dropdown-item(@click="sortBy = 'quantity'", :active="sortBy === 'quantity'") {{ $t('quantity') }}
b-dropdown-item(@click="sortBy = 'AZ'", :active="sortBy === 'AZ'") {{ $t('AZ') }}
div(
v-for="group in groups",
v-if="group.selected",
:key="group.key",
)
h2
| {{ $t(group.key) }}
|
span.badge.badge-pill.badge-default {{group.quantity}}
.items
item(
v-for="({data: item, quantity}, index) in items[group.key]",
v-if="group.open || index < itemsPerLine",
:item="item",
:key="item.key",
:itemContentClass="`${group.classPrefix}${item.key}`"
:selected="true",
)
template(slot="popoverContent", scope="ctx")
h4.popover-content-title {{ ctx.item.text() }}
.popover-content-text {{ ctx.item.notes() }}
template(slot="itemBadge", scope="ctx")
span.badge.badge-pill.badge-item.badge-quantity {{ quantity }}
div(v-if="items[group.key].length === 0")
p(v-once) {{ $t('noGearItemsOfType', { type: $t(group.key) }) }}
a.btn.btn-show-more(
v-if="items[group.key].length > itemsPerLine",
@click="group.open = !group.open"
) {{ group.open ? $t('showLessGearItems', { type: $t(group.key) }) : $t('showAllGearItems', { type: $t(group.key), items: items[group.key].length }) }}
</template>
<style lang="scss" scoped>
</style>
<script>
import { mapState } from 'client/libs/store';
import each from 'lodash/each';
import throttle from 'lodash/throttle';
import bDropdown from 'bootstrap-vue/lib/components/dropdown';
import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item';
import Item from 'client/components/inventory/item';
const allowedSpecialItems = ['snowball', 'spookySparkles', 'shinySeed', 'seafoam'];
const groups = [
['eggs', 'Pet_Egg_'],
['hatchingPotions', 'Pet_HatchingPotion_'],
['food', 'Pet_Food_'],
['special', 'inventory_special_', allowedSpecialItems],
].map(([group, classPrefix, allowedItems]) => {
return {
key: group,
quantity: 0,
selected: true,
open: false,
classPrefix,
allowedItems,
};
});
export default {
name: 'Items',
components: {
Item,
bDropdown,
bDropdownItem,
},
data () {
return {
itemsPerLine: 9,
searchText: null,
searchTextThrottled: null,
groups,
sortBy: 'quantity', // or 'AZ'
};
},
watch: {
searchText: throttle(function throttleSearch () {
this.searchTextThrottled = this.searchText;
}, 250),
},
computed: {
...mapState({
content: 'content',
user: 'user.data',
}),
items () {
const searchText = this.searchTextThrottled;
const itemsByType = {};
this.groups.forEach(group => {
const groupKey = group.key;
let itemsArray = itemsByType[groupKey] = [];
const contentItems = this.content[groupKey];
each(this.user.items[groupKey], (itemQuantity, itemKey) => {
if (itemQuantity > 0 && (!group.allowedItems || group.allowedItems.indexOf(itemKey) !== -1)) {
const item = contentItems[itemKey];
const isSearched = !searchText || item.text().toLowerCase().indexOf(searchText) !== -1;
if (isSearched) {
itemsArray.push({
data: item,
quantity: itemQuantity,
});
group.quantity += itemQuantity;
}
}
});
itemsArray.sort((a, b) => {
if (this.sortBy === 'quantity') {
return b.quantity - a.quantity;
} else { // AZ
return a.data.text().localeCompare(b.data.text());
}
});
});
return itemsByType;
},
},
};
</script>

View File

@@ -0,0 +1,48 @@
<template lang="pug">
span.badge.badge-pill.badge-item.badge-star(
:class="{'item-selected-badge': selected === true}",
@click="click",
v-if="show",
) &#9733;
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
.badge-star {
cursor: pointer;
display: none;
left: -9px;
color: $gray-400;
background: $white;
padding: 4.5px 6px;
&.item-selected-badge {
display: block;
background: $teal-50;
color: $white;
}
}
.item:hover > .badge-star {
display: block;
}
</style>
<script>
export default {
props: {
show: {
type: Boolean,
},
selected: {
type: Boolean,
},
},
methods: {
click () {
this.$emit('click');
},
},
};
</script>

View File

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

View File

@@ -4,5 +4,10 @@
"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."
"costumeDisabled": "You have disabled your costume.",
"items": "Items",
"sortBy": "Sort By",
"groupBy2": "Group By",
"quantity": "Quantity",
"AZ": "A-Z"
}