[WIP] New Client - Shops/Market (#8884)

* initial market - routing - store - load market data

* move drawer/drawerSlider / count/star badge to components/ui

* filter market categories

* shopItem with gem / gold

* show count of purchable items

* show count of purchable itemsshow drawer with currently owned items + DrawerHeaderTabs-Component

* show featured gear

* show Gear - filter by class - sort by (type, price, stats) - sort market items

* Component: ItemRows - shows only the max items in one row (depending on the available width)

* Sell Dialog + Balance Component

* generic buy-dialog / attributes grid with highlight

* buyItem - hide already owned gear

* filter: hide locked/pinned - lock items if not enough gold

* API: Sell multiple items

* show avatar in buy-equipment-dialog with changed gear

* market banner

* misc fixes

* filter by text

* pin/unpin gear store actions

* Sell API: amount as query-parameter

* Update user.js

* fixes

* fix sell api amount test

* add back stroke/fill currentColor

* use scss variables
This commit is contained in:
negue
2017-07-27 19:41:23 +02:00
committed by GitHub
parent 18b04e713e
commit f72f71fd32
43 changed files with 1754 additions and 49 deletions

View File

@@ -1,5 +1,5 @@
import Vue from 'vue';
import DrawerComponent from 'client/components/inventory/drawer.vue';
import DrawerComponent from 'client/components/ui/drawer.vue';
describe('DrawerComponent', () => {
it('sets the correct default data', () => {

View File

@@ -65,6 +65,16 @@ describe('shared.ops.sell', () => {
}
});
it('returns an error when the requested amount is above the available amount', (done) => {
try {
sell(user, {params: { type, key }, query: {amount: 2} });
} catch (err) {
expect(err).to.be.an.instanceof(NotFound);
expect(err.message).to.equal(i18n.t('userItemsNotEnough', {type}));
done();
}
});
it('reduces item count from user', () => {
sell(user, {params: { type, key } });

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -0,0 +1,36 @@
@import '~client/assets/scss/colors.scss';
.featured-label {
width: auto;
height: 28px;
border-radius: 2px;
background-color: #b36213;
display: inline-flex;
align-items: center;
justify-content: center;
&.with-border {
border: solid 2px $yellow-10;
height: 32px;
}
.rectangle {
margin: 9px;
display: inline-block;
width: 6px;
height: 6px;
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
background-color: $yellow-100;
border: solid 2px $yellow-10;
}
.text {
font-size: 12px;
font-weight: bold;
line-height: 1.33;
color: $white;
flex: 1;
}
}

View File

@@ -59,3 +59,14 @@
margin-right: 20px;
margin-left: 20px;
}
.dropdown-icon-item {
.svg-icon {
margin: 0px 16px 0px 0px;
vertical-align: middle;
}
.text {
vertical-align: middle;
}
}

View File

@@ -1,9 +1,9 @@
.svg-icon {
display: block;
stroke-width: 0;
transition: none !important;
stroke: currentColor;
fill: currentColor;
transition: none !important;
svg {
display: block;
@@ -12,6 +12,13 @@
* {
transition: none !important;
}
&.color {
svg path {
stroke: currentColor;
fill: currentColor;
}
}
}
.icon-16 {

View File

@@ -25,3 +25,4 @@
@import './task';
@import './categories';
@import './dragdrop';
@import './banner';

View File

@@ -48,6 +48,10 @@
}
}
.flat .item {
box-shadow: none;
}
.drawer-content .item:hover {
border-color: transparent;
box-shadow: none;

View File

@@ -0,0 +1,36 @@
@import '~client/assets/scss/colors.scss';
@mixin centeredModal() {
display: flex;
justify-content: center;
flex-direction: column;
header, footer {
border: 0;
}
}
.modal-dialog {
.title {
height: 24px;
margin-top: 24px;
font-family: 'Roboto Condensed';
font-size: 20px;
font-weight: bold;
line-height: 1.2;
text-align: center;
color: $gray-50;
}
.text {
height: 60px;
font-family: Roboto;
font-size: 14px;
line-height: 1.43;
text-align: center;
color: $gray-100;
}
}

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<g fill="none" fill-rule="evenodd">
<path fill="#D5C8FF" d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0m0 2c3.308 0 6 2.692 6 6s-2.692 6-6 6-6-2.692-6-6 2.692-6 6-6"/>
<path stroke="#D5C8FF" stroke-linecap="round" stroke-width="2" d="M8 5v3.031L10 10"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 364 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="12" viewBox="0 0 10 12">
<path fill="#C3C0C7" fill-rule="evenodd" d="M4 9h2V7H4v2zm4 1H2V6h6v4zM5 2c1.103 0 2 .897 2 2H3c0-1.103.897-2 2-2zm4 2.277V4a4 4 0 0 0-8 0v.277C.405 4.624 0 5.262 0 6v4a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6c0-.738-.405-1.376-1-1.723z"/>
</svg>

After

Width:  |  Height:  |  Size: 326 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
<path fill="#FFF" fill-rule="evenodd" d="M6.252 8.46l-2.71-2.712 4.602-3.58 1.688 1.689-3.58 4.602zm5.554-4.497l-.626-.627-.001-.001L8.666.822 8.037.194a.66.66 0 1 0-.934.934l.1.1L2.6 4.806l-.216-.216a.66.66 0 1 0-.934.934l.627.627v.001l1.418 1.418-3.301 3.302a.66.66 0 1 0 .934.934L4.43 8.505l1.417 1.417.001.002.628.627a.658.658 0 0 0 .934 0 .66.66 0 0 0 0-.934L7.194 9.4l3.58-4.602.098.099a.659.659 0 0 0 .934 0 .66.66 0 0 0 0-.934z"/>
</svg>

After

Width:  |  Height:  |  Size: 534 B

View File

@@ -14,8 +14,13 @@ div
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: 'shops'}", exact)
router-link.nav-item.dropdown(tag="li", :to="{name: 'market'}", :class="{'active': $route.path.startsWith('/shop')}")
a.nav-link(v-once) {{ $t('shops') }}
.dropdown-menu
router-link.dropdown-item(:to="{name: 'market'}", exact) {{ $t('market') }}
router-link.dropdown-item(:to="{name: 'quests'}") {{ $t('quests') }}
router-link.dropdown-item(:to="{name: 'seasonal'}") {{ $t('titleSeasonalShop') }}
router-link.dropdown-item(:to="{name: 'time'}") {{ $t('titleTimeTravelers') }}
router-link.nav-item(tag="li", :to="{name: 'party'}")
a.nav-link(v-once) {{ $t('party') }}
router-link.nav-item.dropdown(tag="li", :to="{name: 'tavern'}", :class="{'active': $route.path.startsWith('/guilds')}")

View File

@@ -16,21 +16,21 @@
// Show avatar only if not currently affected by visual buff
template(v-if!="!member.stats.buffs.snowball && !member.stats.buffs.spookySparkles && !member.stats.buffs.shinySeed && !member.stats.buffs.seafoam")
span(:class="'chair_' + member.preferences.chair")
span(:class="member.items.gear[costumeClass].back")
span(:class="getGearClass('back')")
span(:class="skinClass")
span.head_0
span(:class="member.preferences.size + '_shirt_' + member.preferences.shirt")
span(:class="member.preferences.size + '_' + member.items.gear[costumeClass].armor")
span(:class="member.items.gear[costumeClass].back_collar")
span(:class="member.items.gear[costumeClass].body")
span(:class="member.preferences.size + '_' + getGearClass('armor')")
span(:class="getGearClass('back_collar')")
span(:class="getGearClass('body')")
template(v-for="type in ['base', 'bangs', 'mustache', 'beard']")
span(:class="'hair_' + type + '_' + member.preferences.hair[type] + '_' + member.preferences.hair.color")
span(:class="member.items.gear[costumeClass].eyewear")
span(:class="member.items.gear[costumeClass].head")
span(:class="member.items.gear[costumeClass].headAccessory")
span(:class="getGearClass('eyewear')")
span(:class="getGearClass('head')")
span(:class="getGearClass('headAccessory')")
span(:class="'hair_flower_' + member.preferences.hair.flower")
span(:class="member.items.gear[costumeClass].shield")
span(:class="member.items.gear[costumeClass].weapon")
span(:class="getGearClass('shield')")
span(:class="getGearClass('weapon')")
// Resting
span.zzz(v-if="member.preferences.sleep")
@@ -105,6 +105,12 @@ export default {
type: Boolean,
default: false,
},
withBackground: {
type: Boolean,
},
overrideAvatarGear: {
type: Object,
},
width: {
type: Number,
default: 140,
@@ -144,7 +150,9 @@ export default {
backgroundClass () {
let background = this.member.preferences.background;
if (background && !this.avatarOnly) {
let allowToShowBackground = !this.avatarOnly || this.withBackground;
if (background && allowToShowBackground) {
return `background_${this.member.preferences.background}`;
}
@@ -167,5 +175,16 @@ export default {
return this.member.preferences.costume ? 'costume' : 'equipped';
},
},
methods: {
getGearClass (gearType) {
let result = this.member.items.gear[this.costumeClass][gearType];
if (this.overrideAvatarGear && this.overrideAvatarGear[gearType]) {
result = this.overrideAvatarGear[gearType];
}
return result;
},
},
};
</script>

View File

@@ -134,8 +134,8 @@ import toggleSwitch from 'client/components/ui/toggleSwitch';
import Item from 'client/components/inventory/item';
import EquipmentAttributesPopover from 'client/components/inventory/equipment/attributesPopover';
import StarBadge from 'client/components/inventory/starBadge';
import Drawer from 'client/components/inventory/drawer';
import StarBadge from 'client/components/ui/starBadge';
import Drawer from 'client/components/ui/drawer';
import i18n from 'common/script/i18n';

View File

@@ -6,7 +6,7 @@ div(v-if="emptyItem")
span.item-label(v-if="label") {{ label }}
b-popover(
v-else,
:triggers="['hover']",
:triggers="[showPopover?'hover':'']",
:placement="popoverPosition",
)
span(slot="content")
@@ -46,6 +46,10 @@ export default {
type: String,
default: 'bottom',
},
showPopover: {
type: Boolean,
default: true,
},
},
methods: {
click () {

View File

@@ -25,7 +25,7 @@ b-popover(
import bPopover from 'bootstrap-vue/lib/components/popover';
import DragDropDirective from 'client/directives/dragdrop.directive';
import CountBadge from './countBadge';
import CountBadge from 'client/components/ui/countBadge';
export default {
components: {

View File

@@ -248,6 +248,7 @@
<style lang="scss">
@import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/modal.scss';
.standard-page .clearfix .float-right {
margin-right: 24px;
@@ -329,22 +330,8 @@
height: 114px;
}
@mixin habitModal() {
display: flex;
justify-content: center;
flex-direction: column;
header, footer {
border: 0;
}
.modal-footer {
justify-content: center;
}
}
#welcome-modal {
@include habitModal();
@include centeredModal();
.npc_matt {
margin: 0 auto 21px auto;
@@ -367,10 +354,14 @@
width: 400px;
}
.modal-footer {
justify-content: center;
}
}
#hatching-modal {
@include habitModal();
@include centeredModal();
.content {
text-align: center;
@@ -405,6 +396,10 @@
right: 10px;
top: 10px;
}
.modal-footer {
justify-content: center;
}
}
.modal-backdrop.fade.show {
@@ -466,11 +461,11 @@
import PetItem from './petItem';
import MountItem from './mountItem.vue';
import FoodItem from './foodItem';
import Drawer from 'client/components/inventory/drawer';
import Drawer from 'client/components/ui/drawer';
import toggleSwitch from 'client/components/ui/toggleSwitch';
import StarBadge from 'client/components/inventory/starBadge';
import CountBadge from './countBadge';
import DrawerSlider from './drawerSlider';
import StarBadge from 'client/components/ui/starBadge';
import CountBadge from 'client/components/ui/countBadge';
import DrawerSlider from 'client/components/ui/drawerSlider';
import ResizeDirective from 'client/directives/resize.directive';
import DragDropDirective from 'client/directives/dragdrop.directive';
@@ -556,7 +551,6 @@
petGroups () {
let petGroups = [
{
label: this.$t('filterByStandard'),
key: 'standardPets',
petSource: {
eggs: this.content.dropEggs,
@@ -649,7 +643,7 @@
drawerTabs () {
return [
{
label: this.$t('food'),
label: this.$t('foodTitle'),
items: _filter(this.content.food, f => {
return f.key !== 'Saddle' && this.userItems.food[f.key];
}),

View File

@@ -0,0 +1,20 @@
<template lang="pug">
.row
secondary-menu.col-12
router-link.nav-link(:to="{name: 'market'}", exact) {{ $t('market') }}
router-link.nav-link(:to="{name: 'quests'}") {{ $t('quests') }}
router-link.nav-link(:to="{name: 'seasonal'}") {{ $t('titleSeasonalShop') }}
router-link.nav-link(:to="{name: 'time'}") {{ $t('titleTimeTravelers') }}
.col-12
router-view
</template>
<script>
import SecondaryMenu from 'client/components/secondaryMenu';
export default {
components: {
SecondaryMenu,
},
};
</script>

View File

@@ -0,0 +1,56 @@
<template lang="pug">
div
.svg-icon(v-html="icons.gem")
span {{userGems | roundBigNumber}}
.svg-icon(v-html="icons.gold")
span {{userGold | roundBigNumber}}
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
span {
font-weight: normal;
font-size: 12px;
line-height: 1.33;
color: $gray-200;
display: inline-block;
}
.svg-icon {
vertical-align: middle;
width: 16px;
height: 16px;
margin-right: 8px;
margin-left: 4px;
display: inline-block;
}
</style>
<script>
import { mapState, mapGetters } from 'client/libs/store';
import gemIcon from 'assets/svg/gem.svg';
import goldIcon from 'assets/svg/gold.svg';
export default {
data () {
return {
icons: Object.freeze({
gem: gemIcon,
gold: goldIcon,
}),
};
},
computed: {
...mapGetters({
userGems: 'user:gems',
}),
...mapState({
userGold: 'user.data.stats.gp',
}),
},
};
</script>

View File

@@ -0,0 +1,195 @@
<template lang="pug">
b-modal#buy-modal(
:visible="true",
v-if="item != null",
:hide-header="true",
@change="onChange($event)"
)
span.badge.badge-pill.badge-dialog(
:class="{'item-selected-badge': true}",
v-if="withPin"
)
span.svg-icon.inline.color.icon-10(v-html="icons.pin")
div.close
span.svg-icon.inline.icon-10(aria-hidden="true", v-html="icons.close", @click="hideDialog()")
div.content(v-if="item != null")
div.inner-content
slot(name="item", :item="item")
h4.title {{ itemText }}
div.text {{ itemNotes }}
slot(name="additionalInfo", :item="item")
div
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="(priceType === 'gems') ? icons.gem : icons.gold")
span.value(:class="priceType") {{ item.value }}
button.btn.btn-primary(@click="buyItem()") {{ $t('buyNow') }}
div.clearfix(slot="modal-footer")
span.balance.float-left {{ $t('yourBalance') }}
balanceInfo.float-right
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/modal.scss';
#buy-modal {
@include centeredModal();
.content {
text-align: center;
}
.item-wrapper {
margin-bottom: 0 !important;
}
.inner-content {
margin: 33px auto auto;
width: 282px;
}
.content-text {
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: normal;
line-height: 1.43;
width: 400px;
}
span.svg-icon.inline.icon-32 {
height: 32px;
width: 32px;
margin-right: 8px;
vertical-align: middle;
}
.value {
width: 28px;
height: 32px;
font-family: Roboto;
font-size: 24px;
font-weight: bold;
line-height: 1.33;
vertical-align: middle;
&.gems {
color: $green-10;
}
&.gold {
color: $yellow-10
}
}
button.btn.btn-primary {
margin-top: 24px;
margin-bottom: 24px;
}
.balance {
width: 74px;
height: 16px;
font-size: 12px;
font-weight: bold;
line-height: 1.33;
color: $gray-200;
}
.modal-footer {
height: 48px;
background-color: $gray-700;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
display: block;
}
.badge-dialog {
color: $gray-300;
position: absolute;
left: -14px;
padding: 8px 10px;
top: -12px;
background: white;
}
}
</style>
<script>
import bModal from 'bootstrap-vue/lib/components/modal';
import svgClose from 'assets/svg/close.svg';
import svgGold from 'assets/svg/gold.svg';
import svgGem from 'assets/svg/gem.svg';
import svgPin from 'assets/svg/pin.svg';
import BalanceInfo from './balanceInfo.vue';
export default {
components: {
bModal,
BalanceInfo,
},
data () {
return {
icons: Object.freeze({
close: svgClose,
gold: svgGold,
gem: svgGem,
pin: svgPin,
}),
};
},
computed: {
itemText () {
if (this.item.text instanceof Function) {
return this.item.text();
} else {
return this.item.text;
}
},
itemNotes () {
if (this.item.notes instanceof Function) {
return this.item.notes();
} else {
return this.item.notes;
}
},
},
methods: {
onChange ($event) {
this.$emit('change', $event);
},
buyItem () {
this.$store.dispatch('shops:buyItem', {key: this.item.key});
this.hideDialog();
},
hideDialog () {
this.$root.$emit('hide::modal', 'buy-modal');
},
},
props: {
item: {
type: Object,
},
priceType: {
type: String,
},
withPin: {
type: Boolean,
},
},
};
</script>

View File

@@ -0,0 +1,55 @@
<template lang="pug">
div
.attribute-entry(v-for="attr in ATTRIBUTES", :key="attr")
span.key(:class="{'no-value': item[attr] == '0'}") {{ `${$t(attr)}: ` }}
span.val(:class="{'no-value': item[attr] == '0'}") {{ `+${item[attr]}` }}
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.attribute-entry {
width: 50%;
display: inline-block;
font-weight: bold;
margin-bottom: 16px;
}
span{
width: 38px;
height: 16px;
font-size: 16px;
font-weight: bold;
line-height: 1.0;
}
.no-value {
color: $gray-400 !important;
}
.key {
color: $gray-100;
}
.val {
color: $green-10;
}
</style>
<script>
import { mapState } from 'client/libs/store';
export default {
props: {
item: {
type: Object,
},
},
computed: {
...mapState({
ATTRIBUTES: 'constants.ATTRIBUTES',
}),
},
};
</script>

View File

@@ -0,0 +1,667 @@
<template lang="pug">
.row.market
.standard-sidebar
.form-group
input.form-control.input-search(type="text", v-model="searchText", :placeholder="$t('search')")
.form
h2(v-once) {{ $t('filter') }}
.form-group
.form-check(
v-for="category in categories",
:key="category.identifier",
)
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", v-model="viewOptions[category.identifier].selected")
span.custom-control-indicator
span.custom-control-description(v-once) {{ category.text }}
div.form-group.clearfix
h3.float-left Hide locked
toggle-switch.float-right.hideMissing(
:label="''",
v-model="hideLocked",
)
div.form-group.clearfix
h3.float-left Hide pinned
toggle-switch.float-right.hideMissing(
:label="''",
v-model="hidePinned",
)
.standard-page
div.featuredItems
.background
div.npc
div.featured-label
span.rectangle
span.text Alex
span.rectangle
div.content
div.featured-label.with-border
span.rectangle
span.text(v-once) {{ $t('featuredItems') }}
span.rectangle
div.items.margin-center
shopItem(
v-for="item in featuredItems",
:key="item.key",
:item="item",
:price="item.value",
:priceType="item.currency",
:itemContentClass="'shop_'+item.key",
:emptyItem="false",
:popoverPosition="'top'",
@click="selectedGearToBuy = item"
)
template(slot="popoverContent", scope="ctx")
equipmentAttributesPopover(:item="ctx.item")
h1.mb-0.page-header(v-once) {{ $t('market') }}
.clearfix
h2.float-left
| {{ $t('classEquipment') }}
div.float-right
span.dropdown-label {{ $t('class') }}
b-dropdown(right=true)
span.dropdown-icon-item(slot="text")
span.svg-icon.inline.icon-16(v-html="icons[selectedGroupGearByClass]")
span.text {{ getClassName(selectedGroupGearByClass) }}
b-dropdown-item(
v-for="sort in content.classes",
@click="selectedGroupGearByClass = sort",
:active="selectedGroupGearByClass === sort",
:key="sort"
)
span.dropdown-icon-item
span.svg-icon.inline.icon-16(v-html="icons[sort]")
span.text {{ getClassName(sort) }}
span.dropdown-label {{ $t('sortBy') }}
b-dropdown(:text="$t(selectedSortGearBy)", right=true)
b-dropdown-item(
v-for="sort in sortGearBy",
@click="selectedSortGearBy = sort",
:active="selectedSortGearBy === sort",
:key="sort"
) {{ $t(sort) }}
br
itemRows(
:items="filteredGear(selectedGroupGearByClass, searchTextThrottled, selectedSortGearBy, hideLocked, hidePinned)",
:itemWidth=94,
:itemMargin=24,
:showAllLabel="$t('showAllEquipment', { classType: getClassName(selectedGroupGearByClass) })",
:showLessLabel="$t('showLessEquipment', { classType: getClassName(selectedGroupGearByClass) })"
)
template(slot="item", scope="ctx")
shopItem(
:key="ctx.item.key",
:item="ctx.item",
:price="ctx.item.value",
:priceType="ctx.item.currency",
:itemContentClass="'shop_'+ctx.item.key",
:emptyItem="userItems.gear[ctx.item.key] === undefined",
:popoverPosition="'top'",
@click="selectedGearToBuy = ctx.item"
)
template(slot="popoverContent", scope="ctx")
equipmentAttributesPopover(:item="ctx.item")
div {{ ctx.item }}
template(slot="itemBadge", scope="ctx")
span.badge.badge-pill.badge-item.badge-svg(
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
@click.prevent.stop="togglePinned(ctx.item)"
)
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
.clearfix
h2.float-left
| {{ $t('items') }}
div.float-right
span.dropdown-label {{ $t('sortBy') }}
b-dropdown(:text="$t(selectedSortItemsBy)", right=true)
b-dropdown-item(
v-for="sort in sortItemsBy",
@click="selectedSortItemsBy = sort",
:active="selectedSortItemsBy === sort",
:key="sort"
) {{ $t(sort) }}
div(
v-for="category in categories",
v-if="viewOptions[category.identifier].selected"
)
h4 {{ category.text }}
div.items
shopItem(
v-for="item in sortedMarketItems(category, selectedSortItemsBy, searchTextThrottled)",
:key="item.key",
:item="item",
:price="item.value",
:priceType="item.currency",
:itemContentClass="item.class",
:emptyItem="false",
:popoverPosition="'top'",
@click="selectedItemToBuy = item"
)
span(slot="popoverContent")
h4.popover-content-title {{ item.text }}
div {{ item }}
div {{ userItems[item.purchaseType][item.key] }}
template(slot="itemBadge", scope="ctx")
countBadge(
:show="true",
:count="userItems[item.purchaseType][item.key] || 0"
)
drawer(
:title="$t('quickInventory')"
)
div(slot="drawer-header")
drawer-header-tabs(
:tabs="drawerTabs",
@changedPosition="tabSelected($event)"
)
div(slot="right-item")
b-popover(
:triggers="['click']",
:placement="'top'",
)
span(slot="content")
.popover-content-text(v-html="$t('petLikeToEatText')", v-once)
div.hand-cursor(v-once)
| {{ $t('petLikeToEat') + ' ' }}
span.svg-icon.inline.icon-16(v-html="icons.information")
drawer-slider(
:items="ownedItems(selectedDrawerItemType) || []",
slot="drawer-slider",
:itemWidth=94,
:itemMargin=24,
)
template(slot="item", scope="ctx")
item(
:item="ctx.item",
:itemContentClass="getItemClass(selectedDrawerItemType, ctx.item.key)",
popoverPosition="top",
@click="selectedItemToSell = ctx.item"
)
template(slot="itemBadge", scope="ctx")
countBadge(
:show="true",
:count="userItems[drawerTabs[selectedDrawerTab].contentType][ctx.item.key] || 0"
)
span(slot="popoverContent")
h4.popover-content-title {{ ctx.item.text() }}
sellModal(
:item="selectedItemToSell",
:itemType="selectedDrawerItemType",
:itemCount="selectedItemToSell != null ? userItems[drawerTabs[selectedDrawerTab].contentType][selectedItemToSell.key] : 0",
@change="resetItemToSell($event)"
)
template(slot="item", scope="ctx")
item.flat(
:item="ctx.item",
:itemContentClass="getItemClass(selectedDrawerItemType, ctx.item.key)",
:showPopover="false"
)
template(slot="itemBadge", scope="ctx")
countBadge(
:show="true",
:count="userItems[drawerTabs[selectedDrawerTab].contentType][ctx.item.key] || 0"
)
buyModal(
:item="selectedGearToBuy",
priceType="gold",
:withPin="true",
@change="resetGearToBuy($event)"
)
template(slot="item", scope="ctx")
div
avatar(
:member="user",
:avatarOnly="true",
:withBackground="true",
:overrideAvatarGear="memberOverrideAvatarGear(selectedGearToBuy)"
)
template(slot="additionalInfo", scope="ctx")
equipmentAttributesGrid.bordered(:item="ctx.item")
buyModal(
:item="selectedItemToBuy",
:priceType="selectedItemToBuy ? selectedItemToBuy.currency : ''",
@change="resetItemToBuy($event)"
)
template(slot="item", scope="ctx")
item.flat(
:item="ctx.item",
:itemContentClass="ctx.item.class",
:showPopover="false"
)
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
.badge-svg {
left: calc((100% - 18px) / 2);
cursor: pointer;
color: $gray-400;
background: $white;
padding: 4.5px 6px;
&.item-selected-badge {
background: $purple-300;
color: $white;
}
}
span.badge.badge-pill.badge-item.badge-svg:not(.item-selected-badge) {
color: #a5a1ac;
}
span.badge.badge-pill.badge-item.badge-svg.hide {
display: none;
}
.item:hover {
span.badge.badge-pill.badge-item.badge-svg.hide {
display: block;
}
}
.icon-12 {
width: 12px;
height: 12px;
}
.hand-cursor {
cursor: pointer;
}
.featuredItems {
height: 216px;
.background {
background: url('~assets/images/market/shop_background.png');
background-repeat: repeat-x;
width: 100%;
height: 216px;
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.content {
display: flex;
flex-direction: column;
}
.npc {
position: absolute;
left: 0;
width: 100%;
height: 216px;
background: url('~assets/images/market/market_banner_web_alexnpc.png');
background-repeat: no-repeat;
.featured-label {
position: absolute;
bottom: -14px;
margin: 0;
left: 80px;
}
}
}
.featured-label {
margin: 24px auto;
}
.bordered {
border-radius: 2px;
background-color: #f9f9f9;
margin-bottom: 24px;
padding: 24px 24px 10px;
}
.market {
.avatar {
cursor: default;
margin: 0 auto;
.character-sprites span {
left: 25px;
}
}
.standard-page {
position: relative;
}
}
</style>
<script>
import {mapState} from 'client/libs/store';
import ShopItem from '../shopItem';
import Item from 'client/components/inventory/item';
import CountBadge from 'client/components/ui/countBadge';
import Drawer from 'client/components/ui/drawer';
import DrawerSlider from 'client/components/ui/drawerSlider';
import DrawerHeaderTabs from 'client/components/ui/drawerHeaderTabs';
import ItemRows from 'client/components/ui/itemRows';
import toggleSwitch from 'client/components/ui/toggleSwitch';
import Avatar from 'client/components/avatar';
import EquipmentAttributesPopover from 'client/components/inventory/equipment/attributesPopover';
import SellModal from './sellModal.vue';
import BuyModal from './buyModal.vue';
import EquipmentAttributesGrid from './equipmentAttributesGrid.vue';
import bPopover from 'bootstrap-vue/lib/components/popover';
import bDropdown from 'bootstrap-vue/lib/components/dropdown';
import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item';
import svgPin from 'assets/svg/pin.svg';
import svgInformation from 'assets/svg/information.svg';
import svgWarrior from 'assets/svg/warrior.svg';
import svgWizard from 'assets/svg/wizard.svg';
import svgRogue from 'assets/svg/rogue.svg';
import svgHealer from 'assets/svg/healer.svg';
import featuredItems from 'common/script/content/shop-featuredItems';
import _filter from 'lodash/filter';
import _sortBy from 'lodash/sortBy';
import _map from 'lodash/map';
import _throttle from 'lodash/throttle';
const sortGearTypes = ['sortByType', 'sortByPrice', 'sortByCon', 'sortByPer', 'sortByStr', 'sortByInt'];
const sortGearTypeMap = {
sortByType: 'type',
sortByPrice: 'value',
sortByCon: 'con',
sortByStr: 'str',
sortByInt: 'int',
};
export default {
components: {
ShopItem,
Item,
CountBadge,
Drawer,
DrawerSlider,
DrawerHeaderTabs,
ItemRows,
toggleSwitch,
bPopover,
bDropdown,
bDropdownItem,
EquipmentAttributesPopover,
SellModal,
BuyModal,
EquipmentAttributesGrid,
Avatar,
},
watch: {
searchText: _throttle(function throttleSearch () {
this.searchTextThrottled = this.searchText.toLowerCase();
}, 250),
},
data () {
return {
viewOptions: {},
searchText: null,
searchTextThrottled: null,
icons: Object.freeze({
pin: svgPin,
information: svgInformation,
warrior: svgWarrior,
wizard: svgWizard,
rogue: svgRogue,
healer: svgHealer,
}),
selectedDrawerTab: 0,
selectedDrawerItemType: 'eggs',
selectedGroupGearByClass: '',
sortGearBy: sortGearTypes,
selectedSortGearBy: 'sortByType',
sortItemsBy: ['AZ', 'sortByNumber'],
selectedSortItemsBy: 'AZ',
selectedItemToSell: null,
selectedGearToBuy: null,
selectedItemToBuy: null,
hideLocked: false,
hidePinned: false,
};
},
computed: {
...mapState({
content: 'content',
market: 'shops.market.data',
user: 'user.data',
userStats: 'user.data.stats',
userItems: 'user.data.items',
}),
categories () {
if (this.market) {
this.market.categories.map((category) => {
this.$set(this.viewOptions, category.identifier, {
selected: true,
});
});
return this.market.categories;
} else {
return [];
}
},
drawerTabs () {
return [
{
key: 'eggs',
contentType: 'eggs',
label: this.$t('eggs'),
},
{
key: 'food',
contentType: 'food',
label: this.$t('foodTitle'),
},
{
key: 'hatchingPotions',
contentType: 'hatchingPotions',
label: this.$t('hatchingPotions'),
},
{
key: 'special',
contentType: 'food',
label: this.$t('special'),
},
];
},
featuredItems () {
return featuredItems.market.map(i => {
return this.content.gear.flat[i];
});
},
},
methods: {
getClassName (classType) {
if (classType === 'wizard') {
return this.$t('mage');
} else {
return this.$t(classType);
}
},
tabSelected ($event) {
this.selectedDrawerTab = $event;
this.selectedDrawerItemType = this.drawerTabs[$event].key;
},
ownedItems (type) {
let mappedItems = _filter(this.content[type], i => {
return this.userItems[type][i.key] > 0;
});
switch (type) {
case 'food':
return _filter(mappedItems, f => {
return f.key !== 'Saddle';
});
case 'special':
if (this.userItems.food.Saddle) {
return _filter(this.content.food, f => {
return f.key === 'Saddle';
});
} else {
return [];
}
default:
return mappedItems;
}
},
getItemClass (type, itemKey) {
switch (type) {
case 'food':
case 'special':
return `Pet_Food_${itemKey}`;
case 'eggs':
return `Pet_Egg_${itemKey}`;
case 'hatchingPotions':
return `Pet_HatchingPotion_${itemKey}`;
default:
return '';
}
},
filteredGear (groupByClass, searchBy, sortBy, hideLocked, hidePinned) {
let result = _filter(this.content.gear.flat, ['klass', groupByClass]);
result = _map(result, (e) => {
return {
...e,
pinned: false, // TODO read pinned state
locked: this.isGearLocked(e),
};
});
result = _filter(result, (gear) => {
if (hideLocked && gear.locked) {
return false;
}
if (hidePinned && gear.pinned) {
return false;
}
if (searchBy) {
let foundPosition = gear.text().toLowerCase().indexOf(searchBy);
if (foundPosition === -1) {
return false;
}
}
// hide already owned
return !this.userItems.gear.owned[gear.key];
});
result = _sortBy(result, [sortGearTypeMap[sortBy]]);
return result;
},
sortedMarketItems (category, sortBy, searchBy) {
let result = _filter(category.items, (i) => {
return !searchBy || i.text.toLowerCase().indexOf(searchBy) !== -1;
});
switch (sortBy) {
case 'AZ': {
result = _sortBy(result, ['text']);
break;
}
case 'sortByNumber': {
result = _sortBy(result, i => {
return this.userItems[i.purchaseType][i.key] || 0;
});
break;
}
}
return result;
},
resetItemToSell ($event) {
if (!$event) {
this.selectedItemToSell = null;
}
},
resetGearToBuy ($event) {
if (!$event) {
this.selectedGearToBuy = null;
}
},
resetItemToBuy ($event) {
if (!$event) {
this.selectedItemToBuy = null;
}
},
isGearLocked (gear) {
if (gear.value > this.userStats.gp) {
return true;
}
return false;
},
memberOverrideAvatarGear (gear) {
return {
[gear.type]: gear.key,
};
},
togglePinned (item) {
let isPinned = Boolean(item.pinned);
item.pinned = !isPinned;
this.$store.dispatch(isPinned ? 'shops:unpinGear' : 'shops:pinGear', {key: item.key});
},
},
created () {
this.$store.dispatch('shops:fetch');
this.selectedGroupGearByClass = this.userStats.class;
},
};
</script>

View File

@@ -0,0 +1,184 @@
<template lang="pug">
b-modal#sell-modal(
:visible="item != null",
:hide-header="true",
@change="onChange($event)"
)
div.close
span.svg-icon.inline.icon-10(aria-hidden="true", v-html="icons.close", @click="hideDialog()")
div.content(v-if="item != null")
div.inner-content
slot(name="item", :item="item")
h4.title {{ item.text() }}
div.text {{ item.notes() }}
div
b.how-many-to-sell {{ $t('howManyToSell') }}
div
b-dropdown(:text="selectedAmountToSell +''", right=true)
b-dropdown-item(
v-for="num of dropDownItems",
@click="selectedAmountToSell = num",
:active="selectedAmountToSell === num",
:key="num"
) {{ num }}
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="icons.gold")
span.value {{ item.value }}
button.btn.btn-primary(@click="sellItems()") {{ $t('sell') }}
div.clearfix(slot="modal-footer")
span.balance.float-left {{ $t('yourBalance') }}
balanceInfo.float-right
</template>
<style lang="scss">
@import '~client/assets/scss/modal.scss';
@import '~client/assets/scss/colors.scss';
#sell-modal {
@include centeredModal();
.content {
text-align: center;
}
.inner-content {
margin: 33px auto auto;
width: 282px;
}
.content-text {
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: normal;
line-height: 1.43;
width: 400px;
}
span.svg-icon.inline.icon-32 {
height: 32px;
width: 32px;
margin-left: 24px;
margin-right: 8px;
vertical-align: middle;
}
.value {
width: 28px;
height: 32px;
font-size: 24px;
font-weight: bold;
line-height: 1.33;
color: #df911e;
vertical-align: middle;
}
button.btn.btn-primary {
margin-top: 24px;
margin-bottom: 24px;
}
.balance {
width: 74px;
height: 16px;
font-size: 12px;
font-weight: bold;
line-height: 1.33;
color: $gray-200;
}
.modal-footer {
height: 48px;
background-color: $gray-700;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
display: block;
}
.how-many-to-sell {
margin-bottom: 16px;
display: block;
}
}
</style>
<script>
import bModal from 'bootstrap-vue/lib/components/modal';
import bDropdown from 'bootstrap-vue/lib/components/dropdown';
import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item';
import svgClose from 'assets/svg/close.svg';
import svgGold from 'assets/svg/gold.svg';
import svgGem from 'assets/svg/gem.svg';
import BalanceInfo from './balanceInfo.vue';
export default {
components: {
bModal,
bDropdown,
bDropdownItem,
BalanceInfo,
},
data () {
return {
selectedAmountToSell: 1,
icons: Object.freeze({
close: svgClose,
gold: svgGold,
gem: svgGem,
}),
};
},
computed: {
dropDownItems () {
let result = [];
for (let i = 1; i <= this.itemCount; i++) {
result.push(i);
}
return result;
},
},
methods: {
onChange ($event) {
this.$emit('change', $event);
},
sellItems () {
this.$store.dispatch('shops:sellItems', {
type: this.itemType,
key: this.item.key,
amount: this.selectedAmountToSell,
});
this.hideDialog();
},
hideDialog () {
this.$root.$emit('hide::modal', 'sell-modal');
},
},
props: {
item: {
type: Object,
},
itemType: {
type: String,
},
itemCount: {
type: Number,
},
},
};
</script>

View File

@@ -0,0 +1,142 @@
<template lang="pug">
b-popover(
:triggers="[showPopover?'hover':'']",
:placement="popoverPosition",
)
span(slot="content")
slot(name="popoverContent", :item="item")
.item-wrapper(@click="click()")
.item(
:class="{'item-empty': emptyItem, 'highlight': highlightBorder}",
)
slot(name="itemBadge", :item="item", :emptyItem="emptyItem")
div.shop-content
span.svg-icon.inline.lock(v-if="item.locked" v-html="icons.lock")
div.image(:class="itemContentClass")
div.price
span.svg-icon.inline.icon-16(v-html="(priceType === 'gems') ? icons.gem : icons.gold")
span.price-label(:class="priceType") {{ price }}
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.item {
min-height: 106px;
}
.item.item-empty {
border-radius: 2px;
background-color: #f9f9f9;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
}
.shop-content {
display: flex;
flex-direction: column;
align-items: center;
& > * {
margin-top : 12px;
}
}
.price {
.svg-icon {
padding-top: 2px;
margin-right: 4px;
}
margin-bottom: 8px;
}
.price-label {
height: 16px;
font-family: Roboto;
font-size: 16px;
font-weight: bold;
line-height: 1.33;
&.gems {
color: $green-10;
}
&.gold {
color: $yellow-10
}
}
span.svg-icon.inline.lock {
height: 12px;
width: 10px;
position: absolute;
right: 8px;
top: 8px;
margin-top: 0;
}
</style>
<script>
import bPopover from 'bootstrap-vue/lib/components/popover';
import svgGem from 'assets/svg/gem.svg';
import svgGold from 'assets/svg/gold.svg';
import svgLock from 'assets/svg/lock.svg';
export default {
components: {
bPopover,
},
data () {
return {
icons: Object.freeze({
gem: svgGem,
gold: svgGold,
lock: svgLock,
}),
};
},
props: {
item: {
type: Object,
},
itemContentClass: {
type: String,
},
price: {
type: Number,
default: -1,
},
priceType: {
type: String,
},
emptyItem: {
type: Boolean,
default: false,
},
highlightBorder: {
type: Boolean,
default: false,
},
popoverPosition: {
type: String,
default: 'bottom',
},
showPopover: {
type: Boolean,
default: true,
},
},
methods: {
click () {
this.$emit('click', {});
},
},
};
</script>

View File

@@ -2,7 +2,7 @@
.drawer-container
.drawer-title(@click="open = !open")
| {{title}}
.drawer-toggle-icon.svg-icon(v-html="open ? icons.minimize : icons.expand", :class="{ closed: !open }")
.drawer-toggle-icon.svg-icon.icon-10(v-html="open ? icons.minimize : icons.expand", :class="{ closed: !open }")
transition(name="slide-up", @afterLeave="adjustPagePadding", @afterEnter="adjustPagePadding")
.drawer-content(v-show="open")
slot(name="drawer-header")
@@ -36,6 +36,7 @@
.drawer-toggle-icon {
float: right;
margin-right: 16px;
margin-top: 16px;
&.closed {
margin-top: 3px;

View File

@@ -0,0 +1,65 @@
<template lang="pug">
div.header-tabs
.drawer-tab-container
.drawer-tab(v-for="(tab, index) in tabs")
a.drawer-tab-text(
@click="changeTab(index)",
:class="{'drawer-tab-text-active': selectedTabPosition === index}",
:title="tab.label"
) {{ tab.label }}
span.right-item
slot(name="right-item")
</template>
<style lang="scss" scoped>
.drawer-tab-text {
overflow-x: hidden;
display: block;
text-overflow: ellipsis;
}
.drawer-tab {
white-space: nowrap;
overflow-x: hidden;
flex: inherit;
}
.drawer-tab-container {
max-width: 50%;
margin: 0 auto;
}
.right-item {
position: absolute;
right: -11px;
top: -2px;
}
.header-tabs {
position: relative;
display: flex;
}
</style>
<script>
export default {
props: {
tabs: {
type: Array,
required: true,
},
},
data () {
return {
selectedTabPosition: 0,
};
},
methods: {
changeTab (newIndex) {
this.selectedTabPosition = newIndex;
this.$emit('changedPosition', newIndex);
},
},
};
</script>

View File

@@ -0,0 +1,71 @@
<template lang="pug">
.item-rows
div.items(v-resize="500", @resized="currentWidth = $event.width")
template(v-for="item in itemsToShow(showAll)")
slot(
name="item",
:item="item",
)
.btn.btn-show-more(
@click="showAll = !showAll",
v-if="items.length > itemsPerRow()"
) {{ showAll ? showLessLabel : showAllLabel }}
</template>
<script>
import ResizeDirective from 'client/directives/resize.directive';
import _take from 'lodash/take';
import _drop from 'lodash/drop';
export default {
directives: {
resize: ResizeDirective,
},
data () {
return {
currentWidth: 0,
currentPage: 0,
showAll: false,
};
},
methods: {
itemsToShow (showAll) {
let itemsPerRow = this.itemsPerRow();
let rowsToShow = showAll ? Math.ceil(this.items.length / itemsPerRow) : 1;
let result = [];
for (let i = 0; i < rowsToShow; i++) {
let skipped = _drop(this.items, i * itemsPerRow);
let row = _take(skipped, itemsPerRow);
result = result.concat(row);
}
return result;
},
itemsPerRow () {
return Math.floor(this.currentWidth / (this.itemWidth + this.itemMargin));
},
},
props: {
items: {
type: Array,
},
itemWidth: {
type: Number,
},
itemMargin: {
type: Number,
},
showAllLabel: {
type: String,
},
showLessLabel: {
type: String,
},
},
};
</script>

View File

@@ -84,6 +84,10 @@ const MyChallenges = () => import(/* webpackChunkName: "challenges" */ './compon
const FindChallenges = () => import(/* webpackChunkName: "challenges" */ './components/challenges/findChallenges');
const ChallengeDetail = () => import(/* webpackChunkName: "challenges" */ './components/challenges/challengeDetail');
// Shops
const ShopsContainer = () => import(/* webpackChunkName: "shops" */'./components/shops/index');
const MarketPage = () => import(/* webpackChunkName: "shops-market" */'./components/shops/market/index');
Vue.use(VueRouter);
const router = new VueRouter({
@@ -111,7 +115,16 @@ const router = new VueRouter({
{ name: 'stable', path: 'stable', component: StablePage },
],
},
{ name: 'shops', path: '/shops', component: Page },
{
path: '/shops',
component: ShopsContainer,
children: [
{ name: 'market', path: 'market', component: MarketPage },
{ name: 'quests', path: 'quests', component: Page },
{ name: 'seasonal', path: 'seasonal', component: Page },
{ name: 'time', path: 'time', component: Page },
],
},
{ name: 'party', path: '/party', component: GuildPage },
{ name: 'groupPlan', path: '/group-plans', component: GroupPlansAppPage },
{

View File

@@ -13,6 +13,7 @@ import * as chat from './chat';
import * as notifications from './notifications';
import * as tags from './tags';
import * as hall from './hall';
import * as shops from './shops';
// Actions should be named as 'actionName' and can be accessed as 'namespace:actionName'
// Example: fetch in user.js -> 'user:fetch'
@@ -31,6 +32,7 @@ const actions = flattenAndNamespace({
notifications,
tags,
hall,
shops,
});
export default actions;

View File

@@ -0,0 +1,53 @@
import axios from 'axios';
import { loadAsyncResource } from 'client/libs/asyncResource';
import buyOp from 'common/script/ops/buy';
import sellOp from 'common/script/ops/sell';
export function fetch (store, forceLoad = false) { // eslint-disable-line no-shadow
return loadAsyncResource({
store,
path: 'shops.market',
url: '/api/v3/shops/market',
deserialize (response) {
return response.data.data;
},
forceLoad,
});
}
export function buyItem (store, params) {
const user = store.state.user.data;
buyOp(user, {params});
axios
.post(`/api/v3/user/buy/${params.key}`);
// TODO
// .then((res) => console.log('equip', res))
// .catch((err) => console.error('equip', err));
}
export function sellItems (store, params) {
const user = store.state.user.data;
sellOp(user, {params, query: {amount: params.amount}});
axios
.post(`/api/v3/user/sell/${params.type}/${params.key}?amount=${params.amount}`);
// TODO
// .then((res) => console.log('equip', res))
// .catch((err) => console.error('equip', err));
}
export function pinGear (store, params) {
//axios
// .post(`/api/v3/user/pin/${params.key}`);
// TODO
// .then((res) => console.log('equip', res))
// .catch((err) => console.error('equip', err));
}
export function unpinGear (store, params) {
//axios
// .post(`/api/v3/user/unpin/${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 user from './user';
import * as shops from './shops';
import * as tasks from './tasks';
import * as party from './party';
import * as members from './members';
@@ -12,6 +13,7 @@ const getters = flattenAndNamespace({
tasks,
party,
members,
shops,
});
export default getters;

View File

@@ -0,0 +1,3 @@
export function market (store) {
return store.state.shops.market;
}

View File

@@ -43,6 +43,9 @@ export default function () {
quest: {},
members: asyncResourceFactory(),
},
shops: {
market: asyncResourceFactory(),
},
myGuilds: [],
editingGroup: {}, // TODO move to local state
// content data, frozen to prevent Vue from modifying it since it's static and never changes

View File

@@ -43,6 +43,7 @@
"sortByColor": "Color",
"sortByHatchable": "Hatchable",
"hatch": "Hatch!",
"foodTitle": "Food",
"dragThisFood": "Drag this <%= foodName %> to a Pet and watch it grow!",
"clickOnPetToFeed": "Click on a Pet to feed <%= foodName %> and watch it grow!",
"dragThisPotion": "Drag this <%= potionName %> to an Egg and hatch a new pet!",
@@ -110,7 +111,6 @@
"wantToJoinPartyTitle": "Want to join a Party?",
"wantToJoinPartyDescription": "Aenean non mattis eros, quis semper ipsum. Phasellus vulputate in nibh et suscipit. In hac habitasse platea dictumst.",
"copy": "Copy",
"lookingForGroup": "Looking for Group",
"inviteToPartyOrQuest": "Invite Party to Quest",
"inviteInformation": "Clicking “Invite” will send an invitation to your party members. When all members have accepted or denied, the Quest begins.",
"questOwnerRewards": "Quest Owner Rewards",
@@ -167,7 +167,6 @@
"helpfulLinks": "Helpful Links",
"communityGuidelinesLink": "Community Guidelines",
"lookingForGroup": "Looking for Group (Party Wanted) Posts",
"faq": "FAQ",
"dataDisplayTool": "Data Display Tool",
"reportProblem": "Report a Problem",
"requestFeature": "Request a Feature",
@@ -213,5 +212,21 @@
"challengeInformationPlaceHolder": "Write a short description advertising your Challenge to other Habiticans. What is the main purpose of your Challenge and why should people join it? Try to include useful keywords in the description so that Habiticans can easily find it when they search!",
"where": "Where*",
"challengeMinimum": "Minimum 1 Gem for public Challenges (helps prevent spam, it really does).",
"group": "Group"
"group": "Group",
"sortByType": "Type",
"sortByPrice": "Price",
"sortByCon": "Con",
"sortByPer": "Per",
"sortByStr": "Str",
"sortByInt": "Int",
"classEquipment": "Class Equipment",
"showAllEquipment": "Show All <%= classType %> Equipment",
"showLessEquipment": "Show Less <%= classType %> Equipment",
"howManyToSell": "How many would you like to sell?",
"yourBalance": "Your balance",
"sell": "Sell",
"buyNow": "Buy Now",
"sortByNumber": "Number",
"featuredItems": "Featured Items!"
}

View File

@@ -41,6 +41,7 @@
"plusOneGem": "+1 Gem",
"typeNotSellable": "Type is not sellable. Must be one of the following <%= acceptedTypes %>",
"userItemsKeyNotFound": "Key not found for user.items <%= type %>",
"userItemsNotEnough": "Not enough items found for user.items <%= type %>",
"pathRequired": "Path string is required",
"unlocked": "Items have been unlocked",
"alreadyUnlocked": "Full set already unlocked.",

View File

@@ -0,0 +1,10 @@
const featuredItems = {
market: [
'head_armoire_vikingHelm',
'weapon_special_1',
'shield_special_0',
'armor_warrior_5',
],
};
export default featuredItems;

View File

@@ -14,6 +14,7 @@ const ACCEPTEDTYPES = ['eggs', 'hatchingPotions', 'food'];
module.exports = function sell (user, req = {}) {
let key = get(req.params, 'key');
let type = get(req.params, 'type');
let amount = get(req.query, 'amount', 1);
if (!type) {
throw new BadRequest(i18n.t('typeRequired', req.language));
@@ -31,8 +32,14 @@ module.exports = function sell (user, req = {}) {
throw new NotFound(i18n.t('userItemsKeyNotFound', {type}, req.language));
}
user.items[type][key]--;
user.stats.gp += content[type][key].value;
let currentAmount = user.items[type][key];
if (amount > currentAmount) {
throw new NotFound(i18n.t('userItemsNotEnough', {type}, req.language));
}
user.items[type][key] -= amount;
user.stats.gp += content[type][key].value * amount;
return [
pick(user, splitWhitespace('stats items')),

View File

@@ -1490,12 +1490,13 @@ api.userReleaseMounts = {
};
/**
* @api {post} /api/v3/user/sell/:type/:key Sell a gold-sellable item owned by the user
* @api {post} /api/v3/user/sell/:type/:key?amount=1 Sell a gold-sellable item owned by the user
* @apiName UserSell
* @apiGroup User
*
* @apiParam {String="eggs","hatchingPotions","food"} type The type of item to sell.
* @apiParam {String} key The key of the item
* @apiParam (Path) {String="eggs","hatchingPotions","food"} type The type of item to sell.
* @apiParam (Path) {String} key The key of the item
* @apiParam (Query) {Number} (optional) amount The amount to sell
*
* @apiSuccess {Object} data.stats
* @apiSuccess {Object} data.items