diff --git a/website/client/app.vue b/website/client/app.vue index a4299b6639a6..cae0f46a1222 100644 --- a/website/client/app.vue +++ b/website/client/app.vue @@ -9,7 +9,7 @@ .container-fluid app-header buyModal( - :item="selectedItemToBuy", + :item="selectedItemToBuy || {}", :withPin="true", @change="resetItemToBuy($event)", @buyPressed="customPurchase($event)", @@ -17,9 +17,8 @@ ) selectMembersModal( - :item="selectedCardToBuy", + :item="selectedSpellToBuy || {}", :group="user.party", - @change="resetCardToBuy($event)", @memberSelected="memberSelected($event)", ) @@ -75,6 +74,7 @@ import * as Analytics from 'client/libs/analytics'; import BuyModal from './components/shops/buyModal.vue'; import SelectMembersModal from 'client/components/selectMembersModal.vue'; import notifications from 'client/mixins/notifications'; +import { setup as setupPayments } from 'client/libs/payments'; export default { mixins: [notifications], @@ -91,7 +91,7 @@ export default { data () { return { selectedItemToBuy: null, - selectedCardToBuy: null, + selectedSpellToBuy: null, sound: { oggSource: '', @@ -127,6 +127,12 @@ export default { this.$root.$on('buyModal::showItem', (item) => { this.selectedItemToBuy = item; + this.$root.$emit('show::modal', 'buy-modal'); + }); + + this.$root.$on('selectMembersModal::showItem', (item) => { + this.selectedSpellToBuy = item; + this.$root.$emit('show::modal', 'select-member-modal'); }); // @TODO split up this file, it's too big @@ -205,6 +211,12 @@ export default { 'preferences.timezoneOffset': this.browserTimezoneOffset, }); } + + this.$nextTick(() => { + // Load external scripts after the app has been rendered + setupPayments(); + Analytics.load(); + }); }).catch((err) => { console.error('Impossible to fetch user. Clean up localStorage and refresh.', err); // eslint-disable-line no-console }); @@ -265,11 +277,6 @@ export default { this.selectedItemToBuy = null; } }, - resetCardToBuy ($event) { - if (!$event) { - this.selectedCardToBuy = null; - } - }, itemSelected (item) { this.selectedItemToBuy = item; }, @@ -285,15 +292,20 @@ export default { customPurchase (item) { if (item.purchaseType === 'card') { if (this.user.party._id) { - this.selectedCardToBuy = item; + this.selectedSpellToBuy = item; + + this.$root.$emit('hide::modal', 'buy-modal'); + this.$root.$emit('show::modal', 'select-member-modal'); } else { this.error(this.$t('errorNotInParty')); } } }, memberSelected (member) { - this.$store.dispatch('user:castSpell', {key: this.selectedCardToBuy.key, targetId: member.id}); - this.selectedCardToBuy = null; + this.$store.dispatch('user:castSpell', {key: this.selectedSpellToBuy.key, targetId: member.id}); + this.selectedSpellToBuy = null; + + this.$root.$emit('hide::modal', 'select-member-modal'); }, hideLoadingScreen () { const loadingScreen = document.getElementById('loading-screen'); diff --git a/website/client/components/appMenu.vue b/website/client/components/appMenu.vue index a7fd89a0871e..dc04ad49dc5e 100644 --- a/website/client/components/appMenu.vue +++ b/website/client/components/appMenu.vue @@ -364,6 +364,7 @@ export default { this.$root.$emit('show::modal', 'avatar-modal'); }, showProfile (startingPage) { + this.$store.state.profileUser = this.user; this.$store.state.profileOptions.startingPage = startingPage; this.$root.$emit('show::modal', 'profile'); }, diff --git a/website/client/components/chat/chatMessages.vue b/website/client/components/chat/chatMessages.vue index 4287f76ce19f..9202a308c551 100644 --- a/website/client/components/chat/chatMessages.vue +++ b/website/client/components/chat/chatMessages.vue @@ -40,7 +40,7 @@ span.action(v-if='user.contributor.admin || (msg.uuid !== user._id && user.flags.communityGuidelinesAccepted)', @click='report(msg)') .svg-icon(v-html="icons.report") | {{$t('report')}} - span.action(v-if='msg.uuid === user._id', @click='remove(msg, index)') + span.action(v-if='msg.uuid === user._id || inbox', @click='remove(msg, index)') .svg-icon(v-html="icons.delete") | {{$t('delete')}} span.action.float-right(v-if='likeCount(msg) > 0') @@ -375,6 +375,12 @@ export default { }, async remove (message, index) { this.chat.splice(index, 1); + + if (this.inbox) { + axios.delete(`/api/v3/user/messages/${message.id}`); + return; + } + await this.$store.dispatch('chat:deleteChat', { groupId: this.groupId, chatId: message.id, diff --git a/website/client/components/creatorIntro.vue b/website/client/components/creatorIntro.vue index 0666e4a2b08b..1732ddd3d92a 100644 --- a/website/client/components/creatorIntro.vue +++ b/website/client/components/creatorIntro.vue @@ -128,13 +128,8 @@ b-modal#avatar-modal(title="", :hide-header='true', :hide-footer='true', :class= strong(v-once) {{$t('flower')}} .row(v-if='activeSubPage === "glasses"') .col-12.customize-options - .eyewear_special_blackTopFrame.option(@click='equip("eyewear_special_blackTopFrame")', :class='{active: user.preferences.costume ? user.items.gear.costume.eyewear === "eyewear_special_blackTopFrame" : user.items.gear.equipped.eyewear === "eyewear_special_blackTopFrame"}') - .eyewear_special_blueTopFrame.option(@click='equip("eyewear_special_blueTopFrame")', :class='{active: user.preferences.costume ? user.items.gear.costume.eyewear === "eyewear_special_blueTopFrame" : user.items.gear.equipped.eyewear === "eyewear_special_blueTopFrame"}') - .eyewear_special_greenTopFrame.option(@click='equip("eyewear_special_greenTopFrame")', :class='{active: user.preferences.costume ? user.items.gear.costume.eyewear === "eyewear_special_greenTopFrame" : user.items.gear.equipped.eyewear === "eyewear_special_greenTopFrame"}') - .eyewear_special_pinkTopFrame.option(@click='equip("eyewear_special_pinkTopFrame")', :class='{active: user.preferences.costume ? user.items.gear.costume.eyewear === "eyewear_special_pinkTopFrame" : user.items.gear.equipped.eyewear === "eyewear_special_pinkTopFrame"}') - .eyewear_special_redTopFrame.option(@click='equip("eyewear_special_redTopFrame")', :class='{active: user.preferences.costume ? user.items.gear.costume.eyewear === "eyewear_special_redTopFrame" : user.items.gear.equipped.eyewear === "eyewear_special_redTopFrame"}') - .eyewear_special_whiteTopFrame.option(@click='equip("eyewear_special_whiteTopFrame")', :class='{active: user.preferences.costume ? user.items.gear.costume.eyewear === "eyewear_special_whiteTopFrame" : user.items.gear.equipped.eyewear === "eyewear_special_whiteTopFrame"}') - .eyewear_special_yellowTopFrame.option(@click='equip("eyewear_special_yellowTopFrame")', :class='{active: user.preferences.costume ? user.items.gear.costume.eyewear === "eyewear_special_yellowTopFrame" : user.items.gear.equipped.eyewear === "eyewear_special_yellowTopFrame"}') + .option(v-for='option in eyewear', :class='{active: option.active}') + .sprite.customize-option(:class="`eyewear_special_${option.key}`", @click='option.click') #wheelchairs.row(v-if='activeSubPage === "wheelchair"') .col-12.customize-options.weelchairs .option(@click='set({"preferences.chair": "none"})', :class='{active: user.preferences.chair === "none"}') @@ -872,6 +867,20 @@ export default { }, computed: { ...mapState({user: 'user.data'}), + eyewear () { + let keys = ['blackTopFrame', 'blueTopFrame', 'greenTopFrame', 'pinkTopFrame', 'redTopFrame', 'whiteTopFrame', 'yellowTopFrame']; + let options = keys.map(key => { + let newKey = `eyewear_special_${key}`; + let option = {}; + option.key = key; + option.active = this.user.preferences.costume ? this.user.items.gear.costume.eyewear === newKey : this.user.items.gear.equipped.eyewear === newKey; + option.click = () => { + return this.equip(newKey); + }; + return option; + }); + return options; + }, animalEarsUnlockString () { let animalItemKeys = this.animalEarsKeys.map(key => { return `items.gear.owned.headAccessory_special_${key}`; diff --git a/website/client/components/groups/startQuestModal.vue b/website/client/components/groups/startQuestModal.vue index 108a7d7833d8..e55844ae213e 100644 --- a/website/client/components/groups/startQuestModal.vue +++ b/website/client/components/groups/startQuestModal.vue @@ -12,7 +12,7 @@ div(v-if='questData') questDialogContent(:item="questData") div.text-center - button.btn.btn-primary(@click='questInit()', :disabled="Boolean(group.quest)") {{$t('inviteToPartyOrQuest')}} + button.btn.btn-primary(@click='questInit()', :disabled="!Boolean(selectedQuest)") {{$t('inviteToPartyOrQuest')}} div.text-center p {{$t('inviteInformation')}} .side-panel(v-if='questData') diff --git a/website/client/components/inventory/items/index.vue b/website/client/components/inventory/items/index.vue index c70991337ab6..8f89a4b82b44 100644 --- a/website/client/components/inventory/items/index.vue +++ b/website/client/components/inventory/items/index.vue @@ -139,13 +139,6 @@ div.popover div.popover-content {{ $t('clickOnEggToHatch', {potionName: currentDraggingPotion.text }) }} - selectMembersModal( - :item="selectedSpell", - :group="user.party", - @change="resetSpell($event)", - @memberSelected="memberSelected($event)", - ) - startQuestModal( :group="user.party" ) @@ -192,7 +185,6 @@ import ItemRows from 'client/components/ui/itemRows'; import CountBadge from 'client/components/ui/countBadge'; import cardsModal from './cards-modal'; -import SelectMembersModal from 'client/components/selectMembersModal'; import HatchedPetDialog from '../stable/hatchedPetDialog'; import startQuestModal from '../../groups/startQuestModal'; @@ -234,7 +226,6 @@ export default { bDropdownItem, HatchedPetDialog, CountBadge, - SelectMembersModal, startQuestModal, cardsModal, }, @@ -252,7 +243,6 @@ export default { currentDraggingPotion: null, potionClickMode: false, hatchedPet: null, - selectedSpell: null, cardOptions: { cardType: '', messageOptions: 0, @@ -275,7 +265,7 @@ export default { this.groups.forEach(group => { const groupKey = group.key; - group.quantity = 0; // reset the count + group.quantity = 0; // resetf the count let itemsArray = itemsByType[groupKey] = []; const contentItems = this.content[groupKey]; @@ -321,8 +311,8 @@ export default { } for (let type in this.content.cardTypes) { - if (this.user.items.special[type] > 0) { - let card = this.user.items.special[`${this.cardType}Received`]; + let card = this.user.items.special[`${type}Received`] || []; + if (this.user.items.special[type] > 0 || card.length > 0) { specialArray.push({ type: 'card', key: type, @@ -435,7 +425,7 @@ export default { item.quantity--; this.$forceUpdate(); } else { - this.selectedSpell = item; + this.$root.$emit('selectMembersModal::showItem', item); } } else if (groupKey === 'quests') { this.$root.$emit('show::modal', 'start-quest-modal'); @@ -452,17 +442,6 @@ export default { lastMouseMoveEvent = $event; } }, - - resetSpell ($event) { - if (!$event) { - this.selectedSpell = null; - } - }, - - memberSelected (member) { - this.$store.dispatch('user:castSpell', {key: this.selectedSpell.key, targetId: member.id}); - this.selectedSpell = null; - }, }, }; diff --git a/website/client/components/payments/amazonModal.vue b/website/client/components/payments/amazonModal.vue index d7eea171d49a..58814b755474 100644 --- a/website/client/components/payments/amazonModal.vue +++ b/website/client/components/payments/amazonModal.vue @@ -19,7 +19,6 @@ diff --git a/website/client/components/static/header.vue b/website/client/components/static/header.vue index 298406bb2943..d003629a4ecc 100644 --- a/website/client/components/static/header.vue +++ b/website/client/components/static/header.vue @@ -157,8 +157,18 @@ export default { }), }; }, + computed: { + isUserLoggedIn () { + return this.$store.state.isUserLoggedIn; + }, + }, methods: { playButtonClick () { + if (this.isUserLoggedIn) { + this.$router.push('/'); + return; + } + // @TODO duplicate of code in home.vue Analytics.track({ hitType: 'event', diff --git a/website/client/components/static/home.vue b/website/client/components/static/home.vue index 5b772bba75b6..8da8dfe3e2f3 100644 --- a/website/client/components/static/home.vue +++ b/website/client/components/static/home.vue @@ -125,7 +125,7 @@ #purple-footer { background-color: #271b3d; - footer, footer a { + footer, footer a, footer h3 { background: transparent; color: #d5c8ff; } @@ -451,7 +451,7 @@ .svg-icon { vertical-align: bottom; - color: #fff; + color: #d5c8ff; display: inline-block; margin-right: 1em; } @@ -517,6 +517,7 @@ position: absolute; height: 500px; width: 100%; + opacity: .5; } } diff --git a/website/client/components/tasks/taskModal.vue b/website/client/components/tasks/taskModal.vue index ed00b9ae100b..05a1439a6e5b 100644 --- a/website/client/components/tasks/taskModal.vue +++ b/website/client/components/tasks/taskModal.vue @@ -30,10 +30,12 @@ .option(v-if="['daily', 'todo'].indexOf(task.type) > -1") label(v-once) {{ $t('checklist') }} br - .inline-edit-input-group.checklist-group.input-group(v-for="(item, $index) in task.checklist") - input.inline-edit-input.checklist-item.form-control(type="text", v-model="item.text") - span.input-group-btn(@click="removeChecklistItem($index)") - .svg-icon.destroy-icon(v-html="icons.destroy") + | {{checklist}} + div(v-sortable='', @onsort='sortedChecklist') + .inline-edit-input-group.checklist-group.input-group(v-for="(item, $index) in checklist") + input.inline-edit-input.checklist-item.form-control(type="text", v-model="item.text") + span.input-group-btn(@click="removeChecklistItem($index)") + .svg-icon.destroy-icon(v-html="icons.destroy") input.inline-edit-input.checklist-item.form-control(type="text", :placeholder="$t('newChecklistItem')", @keydown.enter="addChecklistItem($event)", v-model="newChecklistItem") .d-flex.justify-content-center(v-if="task.type === 'habit'") .option-item(:class="optionClass(task.up === true)", @click="task.up = !task.up") @@ -401,6 +403,8 @@ import { mapGetters, mapActions, mapState } from 'client/libs/store'; import bDropdown from 'bootstrap-vue/lib/components/dropdown'; import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item'; import toggleSwitch from 'client/components/ui/toggleSwitch'; +import sortable from 'client/directives/sortable.directive'; +import clone from 'lodash/clone'; import Datepicker from 'vuejs-datepicker'; import moment from 'moment'; import uuid from 'uuid'; @@ -423,6 +427,9 @@ export default { Datepicker, toggleSwitch, }, + directives: { + sortable, + }, props: ['task', 'purpose', 'challengeId', 'groupId'], // purpose is either create or edit, task is the task created or edited data () { return { @@ -444,6 +451,7 @@ export default { members: [], memberNamesById: {}, assignedMembers: [], + checklist: [], }; }, watch: { @@ -464,6 +472,9 @@ export default { this.assignedMembers = []; if (this.task.group && this.task.group.assignedUsers) this.assignedMembers = this.task.group.assignedUsers; } + + // @TODO: This whole component is mutating a prop and that causes issues. We need to not copy the prop similar to group modals + if (this.task) this.checklist = clone(this.task.checklist); }, }, computed: { @@ -534,6 +545,13 @@ export default { }, methods: { ...mapActions({saveTask: 'tasks:save', destroyTask: 'tasks:destroy', createTask: 'tasks:create'}), + sortedChecklist (data) { + let sorting = clone(this.checklist); + let movingItem = sorting[data.oldIndex]; + sorting.splice(data.oldIndex, 1); + sorting.splice(data.newIndex, 0, movingItem); + this.task.checklist = sorting; + }, optionClass (activeCondition) { if (activeCondition) { return [`${this.cssClass}-color`, 'option-item-selected']; @@ -547,11 +565,13 @@ export default { text: this.newChecklistItem, completed: false, }); + this.checklist = clone(this.task.checklist); this.newChecklistItem = null; if (e) e.preventDefault(); }, removeChecklistItem (i) { this.task.checklist.splice(i, 1); + this.checklist = clone(this.task.checklist); }, weekdaysMin (dayNumber) { return moment.weekdaysMin(dayNumber); diff --git a/website/client/components/userMenu/inbox.vue b/website/client/components/userMenu/inbox.vue index f643cf30501f..23413994a869 100644 --- a/website/client/components/userMenu/inbox.vue +++ b/website/client/components/userMenu/inbox.vue @@ -11,6 +11,8 @@ // @TODO: Implement this after we fix username bug // .col-2.offset-1 // button.btn.btn-secondary(@click='toggleClick()') + + .col-4.offset-4 + .svg-icon.close(v-html="icons.svgClose", @click='close()') // .col-8.to-form(v-if='displayCreate') // strong To: // b-form-input @@ -39,7 +41,7 @@ // @TODO: Implement new message header here when we fix the above .new-message-row(v-if='selectedConversation') - input(v-model='newMessage') + textarea(v-model='newMessage') button.btn.btn-secondary(@click='sendPrivateMessage()') Send @@ -51,6 +53,11 @@ margin-top: 1em; } + .close { + margin-top: .5em; + width: 15px; + } + h2 { margin-top: .5em; } @@ -107,12 +114,16 @@ width: 100%; padding: 1em; - input { + textarea { + height: 80%; display: inline-block; + vertical-align: bottom; width: 80%; } button { + vertical-align: bottom; + display: inline-block; box-shadow: none; margin-left: 1em; } @@ -155,6 +166,7 @@ import bFormInput from 'bootstrap-vue/lib/components/form-input'; import messageIcon from 'assets/svg/message.svg'; import chatMessage from '../chat/chatMessages'; +import svgClose from 'assets/svg/close.svg'; export default { mixins: [styleHelper], @@ -167,6 +179,7 @@ export default { return { icons: Object.freeze({ messageIcon, + svgClose, }), displayCreate: true, selectedConversation: '', @@ -201,6 +214,7 @@ export default { timestamp: message.timestamp, user: message.user, uuid: message.uuid, + id: message.id, }; if (message.sent) { @@ -256,6 +270,8 @@ export default { }); }, sendPrivateMessage () { + if (!this.newMessage) return; + let convoFound = this.conversations.find((conversation) => { return conversation.key === this.selectedConversation; }); @@ -284,6 +300,9 @@ export default { chatscroll.scrollTop = chatscroll.scrollHeight; }); }, + close () { + this.$root.$emit('hide::modal', 'inbox-modal'); + }, }, }; diff --git a/website/client/components/userMenu/profile.vue b/website/client/components/userMenu/profile.vue index af0a6567a87f..696507aa9acb 100644 --- a/website/client/components/userMenu/profile.vue +++ b/website/client/components/userMenu/profile.vue @@ -47,7 +47,10 @@ div h2 {{ $t('info') }} div strong {{ $t('joined') }}: - | {{user.auth.timestamps.created}} + | {{userJoinedDate}} + div + strong {{ $t('lastLoggedIn') }}: + | {{userLastLoggedIn}} div strong {{ $t('totalLogins') }}: span {{ $t('totalCheckins', {count: user.loginIncentives}) }} @@ -100,7 +103,7 @@ div h4.popover-content-title {{ achievement.title }} div.popover-content-text(v-html="achievement.text") .achievement(:class='achievement.icon + "2x"', v-if='achievement.earned') - .counter.badge.badge-info.stack-count(v-if='achievement.optionalCount') {{achievement.optionalCount}} + .counter.badge.badge-info.stack-count(v-if='achievement.optionalCount') {{achievement.optionalCount}} .achievement.achievement-unearned(class='achievement-unearned2x', v-if='!achievement.earned') hr.col-12 .row @@ -126,7 +129,7 @@ div div(:class="`shop_${equippedItems.eyewear}`") h3 {{$t('eyewear')}} .col-4.item-wrapper - .box(:class='{white: equippedItems.head}') + .box(:class='{white: equippedItems.head && equippedItems.head.indexOf("base_0") === -1}') div(:class="`shop_${equippedItems.head}`") h3 {{$t('headGear')}} .col-4.item-wrapper @@ -138,7 +141,7 @@ div div(:class="`shop_${equippedItems.backAccessory}`") h3 {{$t('backAccess')}} .col-4.item-wrapper - .box(:class='{white: equippedItems.armor}') + .box(:class='{white: equippedItems.armor && equippedItems.armor.indexOf("base_0") === -1}') div(:class="`shop_${equippedItems.armor}`") h3 {{$t('armor')}} .col-4.item-wrapper @@ -146,12 +149,12 @@ div div(:class="`shop_${equippedItems.bodyAccessory}`") h3 {{$t('bodyAccess')}} .col-4.item-wrapper - .box(:class='{white: equippedItems.weapon}') + .box(:class='{white: equippedItems.weapon && equippedItems.weapon.indexOf("base_0") === -1}') div(:class="`shop_${equippedItems.weapon}`") h3 {{$t('mainHand')}} .col-4.item-wrapper .col-4.item-wrapper - .box(:class='{white: equippedItems.shield}') + .box(:class='{white: equippedItems.shield && equippedItems.shield.indexOf("base_0") === -1}') div(:class="`shop_${equippedItems.shield}`") h3 {{$t('offHand')}} .col-6 @@ -162,7 +165,7 @@ div div(:class="`shop_${costumeItems.eyewear}`") h3 {{$t('eyewear')}} .col-4.item-wrapper - .box(:class='{white: costumeItems.head}') + .box(:class='{white: costumeItems.head && costumeItems.head.indexOf("base_0") === -1}') div(:class="`shop_${costumeItems.head}`") h3 {{$t('headGear')}} .col-4.item-wrapper @@ -174,7 +177,7 @@ div div(:class="`shop_${costumeItems.backAccessory}`") h3 {{$t('backAccess')}} .col-4.item-wrapper - .box(:class='{white: costumeItems.armor}') + .box(:class='{white: costumeItems.armor && costumeItems.armor.indexOf("base_0") === -1}') div(:class="`shop_${costumeItems.armor}`") h3 {{$t('armor')}} .col-4.item-wrapper @@ -182,7 +185,7 @@ div div(:class="`shop_${costumeItems.bodyAccessory}`") h3 {{$t('bodyAccess')}} .col-4.item-wrapper - .box(:class='{white: costumeItems.weapon}') + .box(:class='{white: costumeItems.weapon && costumeItems.weapon.indexOf("base_0") === -1}') div(:class="`shop_${costumeItems.weapon}`") h3 {{$t('mainHand')}} .col-4.item-wrapper @@ -190,7 +193,7 @@ div div(:class="user.preferences.background") h3 {{$t('background')}} .col-4.item-wrapper - .box(:class='{white: costumeItems.shield}') + .box(:class='{white: costumeItems.shield && costumeItems.shield.indexOf("base_0") === -1}') div(:class="`shop_${costumeItems.shield}`") h3 {{$t('offHand')}} .row.pet-mount-row @@ -200,7 +203,7 @@ div .row.col-12 .col-4 .box(:class='{white: user.items.currentPet}') - div(:class="user.items.currentPet") + .pet(:class="`Pet-${user.items.currentPet}`") .col-8 div | {{ formatAnimal(user.items.currentPet, 'pet') }} @@ -216,7 +219,7 @@ div .row.col-12 .col-4 .box(:class='{white: user.items.currentMount}') - div(:class="user.items.currentMount") + .mount(:class="`Mount-${user.items.currentMount}`") .col-8 div | {{ formatAnimal(user.items.currentMount, 'mount') }} @@ -322,6 +325,10 @@ div margin-bottom: 2em; } + .pet, .mount { + margin-top: -1.6em; + } + .header { h1 { color: #4f2a93; @@ -399,9 +406,10 @@ div color: #fff; background-color: #ff944c; box-shadow: 0 1px 1px 0 rgba(26, 24, 29, 0.12); - width: 24px; - height: 24px; - border-radius: 50%; + min-width: 24px; + min-height: 24px; + border-radius: 2em; + padding: .5em; } .achievement-icon { @@ -537,6 +545,7 @@ div - - - diff --git a/website/client/libs/analytics.js b/website/client/libs/analytics.js index b42673f7fedd..4862d868994d 100644 --- a/website/client/libs/analytics.js +++ b/website/client/libs/analytics.js @@ -5,6 +5,10 @@ import includes from 'lodash/includes'; import getStore from 'client/store'; import Vue from 'vue'; +const IS_PRODUCTION = process.env.NODE_ENV === 'production'; // eslint-disable-line no-process-env +const AMPLITUDE_KEY = process.env.AMPLITUDE_KEY; // eslint-disable-line no-process-env +const GA_ID = process.env.GA_ID; // eslint-disable-line no-process-env + let REQUIRED_FIELDS = ['hitType', 'eventCategory', 'eventAction']; let ALLOWED_HIT_TYPES = [ 'pageview', @@ -93,12 +97,7 @@ export function updateUser (properties) { }); } - export function setup () { - const IS_PRODUCTION = process.env.NODE_ENV === 'production'; // eslint-disable-line no-process-env - const AMPLITUDE_KEY = process.env.AMPLITUDE_KEY; // eslint-disable-line no-process-env - const GA_ID = process.env.GA_ID; // eslint-disable-line no-process-env - // Setup queues until the real scripts are loaded /* eslint-disable */ @@ -119,25 +118,24 @@ export function setup () { }, window['ga'].l = 1 * new Date(); ga('create', GA_ID); /* eslint-enable */ +} +export function load () { // Load real scripts - if (!IS_PRODUCTION) return; - Vue.nextTick(() => { - // Amplitude - const amplitudeScript = document.createElement('script'); - let firstScript = document.getElementsByTagName('script')[0]; - amplitudeScript.type = 'text/javascript'; - amplitudeScript.async = true; - amplitudeScript.src = 'https://d24n15hnbwhuhn.cloudfront.net/libs/amplitude-2.2.0-min.gz.js'; - firstScript.parentNode.insertBefore(amplitudeScript, firstScript); - - // Google Analytics - const gaScript = document.createElement('script'); - firstScript = document.getElementsByTagName('script')[0]; - gaScript.async = 1; - gaScript.src = '//www.google-analytics.com/analytics.js'; - firstScript.parentNode.insertBefore(gaScript, firstScript); - }); + // Amplitude + const amplitudeScript = document.createElement('script'); + let firstScript = document.getElementsByTagName('script')[0]; + amplitudeScript.type = 'text/javascript'; + amplitudeScript.async = true; + amplitudeScript.src = 'https://d24n15hnbwhuhn.cloudfront.net/libs/amplitude-2.2.0-min.gz.js'; + firstScript.parentNode.insertBefore(amplitudeScript, firstScript); + + // Google Analytics + const gaScript = document.createElement('script'); + firstScript = document.getElementsByTagName('script')[0]; + gaScript.async = 1; + gaScript.src = '//www.google-analytics.com/analytics.js'; + firstScript.parentNode.insertBefore(gaScript, firstScript); } \ No newline at end of file diff --git a/website/client/libs/payments.js b/website/client/libs/payments.js new file mode 100644 index 000000000000..1ebfe8cdaad5 --- /dev/null +++ b/website/client/libs/payments.js @@ -0,0 +1,32 @@ +import getStore from 'client/store'; + +const AMAZON_PAYMENTS = process.env.AMAZON_PAYMENTS; // eslint-disable-line +const NODE_ENV = process.env.NODE_ENV; // eslint-disable-line + +export function setup () { + const store = getStore(); + + // Set Amazon Payments as ready in the store, + // Added here to make sure the listener is registered before the script can be executed + window.onAmazonLoginReady = () => { + store.state.isAmazonReady = true; + window.amazon.Login.setClientId(AMAZON_PAYMENTS.CLIENT_ID); + }; + + // Load the scripts + + // Amazon Payments + const amazonScript = document.createElement('script'); + let firstScript = document.getElementsByTagName('script')[0]; + amazonScript.type = 'text/javascript'; + amazonScript.async = true; + amazonScript.src = `https://static-na.payments-amazon.com/OffAmazonPayments/us/${(NODE_ENV === 'production' ? '' : 'sandbox/')}js/Widgets.js`; + firstScript.parentNode.insertBefore(amazonScript, firstScript); + + // Stripe + const stripeScript = document.createElement('script'); + firstScript = document.getElementsByTagName('script')[0]; + stripeScript.async = true; + stripeScript.src = '//checkout.stripe.com/v2/checkout.js'; + firstScript.parentNode.insertBefore(stripeScript, firstScript); +} \ No newline at end of file diff --git a/website/client/main.js b/website/client/main.js index ef40655bc226..ad9b6e579050 100644 --- a/website/client/main.js +++ b/website/client/main.js @@ -4,7 +4,9 @@ require('babel-polyfill'); import Vue from 'vue'; import AppComponent from './app'; -import { setup as setupAnalytics } from 'client/libs/analytics'; +import { + setup as setupAnalytics, +} from 'client/libs/analytics'; import router from './router'; import getStore from './store'; import StoreModule from './libs/store'; @@ -27,11 +29,12 @@ Vue.config.productionTip = IS_PRODUCTION; Vue.use(i18n, {i18nData: window && window['habitica-i18n']}); Vue.use(StoreModule); -setupAnalytics(); +setupAnalytics(); // just create queues for analytics, no scripts loaded at this time +const store = getStore(); export default new Vue({ el: '#app', router, - store: getStore(), + store, render: h => h(AppComponent), }); diff --git a/website/client/mixins/payments.js b/website/client/mixins/payments.js index f855f6c3e519..a574e78cc21a 100644 --- a/website/client/mixins/payments.js +++ b/website/client/mixins/payments.js @@ -5,10 +5,13 @@ import subscriptionBlocks from '../../common/script/content/subscriptionBlocks'; import { mapState } from 'client/libs/store'; import notificationsMixin from 'client/mixins/notifications'; -let StripeCheckout = window.StripeCheckout; +let StripeCheckout; export default { mixins: [notificationsMixin], + created () { + StripeCheckout = window.StripeCheckout; + }, computed: { ...mapState(['credentials']), paypalCheckoutLink () { diff --git a/website/client/store/actions/quests.js b/website/client/store/actions/quests.js index c953fce4ef6d..be621184b731 100644 --- a/website/client/store/actions/quests.js +++ b/website/client/store/actions/quests.js @@ -5,10 +5,20 @@ import * as Analytics from 'client/libs/analytics'; // } export async function sendAction (store, payload) { - Analytics.updateUser({ - partyID: store.state.party.data._id, - partySize: store.state.party.data.memberCount, - }); + // @TODO: Maybe move this to server + let partyData = { + partyID: store.state.user.data.party._id, + partySize: store.state.party.members.data.length, + }; + + if (store.state.party) { + partyData = { + partyID: store.state.party.data._id, + partySize: store.state.party.data.memberCount, + }; + } + + Analytics.updateUser(partyData); let response = await axios.post(`/api/v3/groups/${payload.groupId}/${payload.action}`); diff --git a/website/client/store/actions/shops.js b/website/client/store/actions/shops.js index 9c24e866fb56..59fb6ed86730 100644 --- a/website/client/store/actions/shops.js +++ b/website/client/store/actions/shops.js @@ -72,7 +72,6 @@ export function genericPurchase (store, params) { switch (params.pinType) { case 'mystery_set': return purchaseMysterySet(store, params); - case 'potion': case 'armoire': // eslint-disable-line let buyResult = buyArmoire(store.state.user.data); @@ -88,6 +87,7 @@ export function genericPurchase (store, params) { axios.post('/api/v3/user/buy-armoire'); return; + case 'potion': case 'marketGear': return buyItem(store, params); case 'background': diff --git a/website/client/store/index.js b/website/client/store/index.js index ca3c07154e58..ba56c2111209 100644 --- a/website/client/store/index.js +++ b/website/client/store/index.js @@ -55,6 +55,7 @@ export default function () { title: 'Habitica', isUserLoggedIn, isUserLoaded: false, // Means the user and the user's tasks are ready + isAmazonReady: false, // Whether the Amazon Payments lib can be used user: asyncResourceFactory(), credentials: AUTH_SETTINGS ? { API_ID: AUTH_SETTINGS.auth.apiId, @@ -95,9 +96,9 @@ export default function () { cloning: false, tasksToClone: {}, }, - editingGroup: {}, // TODO move to local state + editingGroup: {}, // @TODO move to local state // content data, frozen to prevent Vue from modifying it since it's static and never changes - // TODO apply freezing to the entire codebase (the server) and not only to the client side? + // @TODO apply freezing to the entire codebase (the server) and not only to the client side? // NOTE this takes about 10-15ms on a fast computer content: deepFreeze(content), constants: deepFreeze({...commonConstants, DAY_MAPPING}), diff --git a/website/common/script/libs/getItemInfo.js b/website/common/script/libs/getItemInfo.js index 1470c54597c6..f10067d9ba27 100644 --- a/website/common/script/libs/getItemInfo.js +++ b/website/common/script/libs/getItemInfo.js @@ -115,7 +115,7 @@ module.exports = function getItemInfo (user, type, item, officialPinnedItems, la notes: item.notes(language), value: item.value, currency: 'gems', - class: item.class, + class: `quest_bundle_${item.key}`, purchaseType: 'bundles', path: `bundles.${item.key}`, pinType: 'bundles', diff --git a/website/common/script/ops/pinnedGearUtils.js b/website/common/script/ops/pinnedGearUtils.js index 095d8a46931e..599c9430363a 100644 --- a/website/common/script/ops/pinnedGearUtils.js +++ b/website/common/script/ops/pinnedGearUtils.js @@ -45,14 +45,12 @@ function addPinnedGear (user, type, path) { } function addPinnedGearByClass (user) { - if (user.flags.classSelected) { - let newPinnedItems = selectGearToPin(user); + let newPinnedItems = selectGearToPin(user); - for (let item of newPinnedItems) { - let itemInfo = getItemInfo(user, 'marketGear', item); + for (let item of newPinnedItems) { + let itemInfo = getItemInfo(user, 'marketGear', item); - addPinnedGear(user, itemInfo.pinType, itemInfo.path); - } + addPinnedGear(user, itemInfo.pinType, itemInfo.path); } } @@ -70,14 +68,12 @@ function removeItemByPath (user, path) { } function removePinnedGearByClass (user) { - if (user.flags.classSelected) { - let currentPinnedItems = selectGearToPin(user); + let currentPinnedItems = selectGearToPin(user); - for (let item of currentPinnedItems) { - let itemInfo = getItemInfo(user, 'marketGear', item); + for (let item of currentPinnedItems) { + let itemInfo = getItemInfo(user, 'marketGear', item); - removeItemByPath(user, itemInfo.path); - } + removeItemByPath(user, itemInfo.path); } }