mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
[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:
@@ -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', () => {
|
||||
@@ -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 |
BIN
website/client/assets/images/market/shop_background.png
Normal file
BIN
website/client/assets/images/market/shop_background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
36
website/client/assets/scss/banner.scss
Normal file
36
website/client/assets/scss/banner.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -25,3 +25,4 @@
|
||||
@import './task';
|
||||
@import './categories';
|
||||
@import './dragdrop';
|
||||
@import './banner';
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.flat .item {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.drawer-content .item:hover {
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
36
website/client/assets/scss/modal.scss
Normal file
36
website/client/assets/scss/modal.scss
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
6
website/client/assets/svg/clock.svg
Normal file
6
website/client/assets/svg/clock.svg
Normal 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 |
3
website/client/assets/svg/lock.svg
Normal file
3
website/client/assets/svg/lock.svg
Normal 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 |
3
website/client/assets/svg/pin.svg
Normal file
3
website/client/assets/svg/pin.svg
Normal 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 |
@@ -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')}")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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];
|
||||
}),
|
||||
|
||||
20
website/client/components/shops/index.vue
Normal file
20
website/client/components/shops/index.vue
Normal 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>
|
||||
56
website/client/components/shops/market/balanceInfo.vue
Normal file
56
website/client/components/shops/market/balanceInfo.vue
Normal 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>
|
||||
195
website/client/components/shops/market/buyModal.vue
Normal file
195
website/client/components/shops/market/buyModal.vue
Normal 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>
|
||||
@@ -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>
|
||||
667
website/client/components/shops/market/index.vue
Normal file
667
website/client/components/shops/market/index.vue
Normal 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>
|
||||
184
website/client/components/shops/market/sellModal.vue
Normal file
184
website/client/components/shops/market/sellModal.vue
Normal 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>
|
||||
142
website/client/components/shops/shopItem.vue
Normal file
142
website/client/components/shops/shopItem.vue
Normal 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>
|
||||
@@ -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;
|
||||
65
website/client/components/ui/drawerHeaderTabs.vue
Normal file
65
website/client/components/ui/drawerHeaderTabs.vue
Normal 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>
|
||||
71
website/client/components/ui/itemRows.vue
Normal file
71
website/client/components/ui/itemRows.vue
Normal 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>
|
||||
@@ -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 },
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
53
website/client/store/actions/shops.js
Normal file
53
website/client/store/actions/shops.js
Normal 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));
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
3
website/client/store/getters/shops.js
Normal file
3
website/client/store/getters/shops.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export function market (store) {
|
||||
return store.state.shops.market;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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!"
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
10
website/common/script/content/shop-featuredItems.js
Normal file
10
website/common/script/content/shop-featuredItems.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const featuredItems = {
|
||||
market: [
|
||||
'head_armoire_vikingHelm',
|
||||
'weapon_special_1',
|
||||
'shield_special_0',
|
||||
'armor_warrior_5',
|
||||
],
|
||||
};
|
||||
|
||||
export default featuredItems;
|
||||
@@ -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')),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user