diff --git a/app/components/home/home.tsx b/app/components/home/home.tsx index 48929f1..a4cf68f 100644 --- a/app/components/home/home.tsx +++ b/app/components/home/home.tsx @@ -1,5 +1,7 @@ +import * as _ from 'lodash'; import { inject, observer } from 'mobx-react'; import React from 'react'; +import { IItem, IItemPrice } from '../../model'; import { AppStore } from '../../stores/app.store'; import { Navbar } from '../navbar/navbar'; import { PriceListItem } from '../price-list-item/price-list-item'; @@ -19,15 +21,29 @@ export class Home extends React.Component { this.state = {}; } + onPriceUpdate = (id: string, itemPrice: IItemPrice) => { + this.props.appStore!.updateItemPrice(id, itemPrice); + }; + + renderItemList = () => { + const { activeLeague, stashItems } = this.props.appStore!; + const orderedItems = _.orderBy( + stashItems, + (i: IItem) => { + return i.itemPrice && i.itemPrice.recommendedPrice ? parseFloat(i.itemPrice.recommendedPrice) : 0; + }, + ['desc'], + ); + return orderedItems.map((v, k) => { + return ; + }); + }; + render() { return (
-
- {this.props.appStore!.stashItems.map((v, k) => { - return ; - })} -
+
{this.renderItemList()}
); } diff --git a/app/components/index.ts b/app/components/index.ts index 5c708b8..d70e8e7 100644 --- a/app/components/index.ts +++ b/app/components/index.ts @@ -2,5 +2,4 @@ export * from './home/home'; export * from './login-webview/login-webview'; export * from './navbar/navbar'; export * from './price-list-item/price-list-item'; -export * from './price-list/price-list'; export * from './stash-tab/stash-tab'; diff --git a/app/components/navbar/navbar.tsx b/app/components/navbar/navbar.tsx index 20fa2e3..82ff2a0 100644 --- a/app/components/navbar/navbar.tsx +++ b/app/components/navbar/navbar.tsx @@ -62,6 +62,7 @@ export class Navbar extends React.Component {
{this.renderLeagueSelector()} {this.renderStashTabSelector()} +
); } diff --git a/app/components/price-list-item/price-list-item.tsx b/app/components/price-list-item/price-list-item.tsx index 8e23543..d9a17f6 100644 --- a/app/components/price-list-item/price-list-item.tsx +++ b/app/components/price-list-item/price-list-item.tsx @@ -1,12 +1,15 @@ import React from 'react'; +import { IItemPrice } from '../../model'; import { FrameType } from '../../model/frame-type'; import { IItem } from '../../model/item'; import { RarityColor } from '../../model/rarity-color'; -import { ItemTextService } from '../../services'; +import { ItemTextService, PoeService } from '../../services'; import './price-list-item.scss'; interface IProps { - data: IItem; + item: IItem; + league: string; + onPriceUpdate: (id: string, price: IItemPrice) => void; } interface IState { @@ -19,17 +22,36 @@ export class PriceListItem extends React.Component { this.state = {}; } - componentDidMount() {} + componentDidMount() { + PoeService.priceCheck(this.props.item, this.props.league).then((data: any) => { + this.props.onPriceUpdate(this.props.item.id, data); + }); + } + + getItemPriceText = (p: IItemPrice) => { + let s = ''; + s += p.recommendedPrice ? `Recommended Price: ${p.median_price}\n` : ''; + s += p.max_price ? `Max Price: ${p.max_price}\n` : ''; + s += p.median_price ? `Median Price: ${p.median_price}\n` : ''; + + return s; + }; render() { - const { icon, priceInfo, stackSize, typeLine, frameType } = this.props.data; + const { icon, itemPrice, stackSize, typeLine, frameType } = this.props.item; const name = ItemTextService.filterName(typeLine); - const fullText = ItemTextService.parseItem(this.props.data); + const fullText = ItemTextService.parseItem(this.props.item); const rarityColor = RarityColor[(FrameType[frameType] || '').toLowerCase()]; + const itemPriceElement = itemPrice ? ( + + {itemPrice.recommendedPrice} {itemPrice.currency_rec} + + ) : null; + return ( -
console.log(this.props.data)}> +
console.log(this.props.item)}>
@@ -38,10 +60,10 @@ export class PriceListItem extends React.Component {
{name}
-
{stackSize && Stack Size: {stackSize}}
-
-
- {priceInfo ? `${priceInfo.min || priceInfo.min_price} ${priceInfo.currency || priceInfo.currency_rec}` : ''} +
+ {stackSize && Stack Size: {stackSize}} +
{itemPriceElement}
+
diff --git a/app/components/price-list/price-list.tsx b/app/components/price-list/price-list.tsx index af8346a..64c3522 100644 --- a/app/components/price-list/price-list.tsx +++ b/app/components/price-list/price-list.tsx @@ -1,14 +1,14 @@ -import React from 'react'; -import { PriceListItem } from '../price-list-item/price-list-item'; +// import React from 'react'; +// import { PriceListItem } from '../price-list-item/price-list-item'; -interface IProps { - data: any[]; -} +// interface IProps { +// data: any[]; +// } -export class PriceList extends React.Component { - constructor(props: IProps) { - super(props); - } +// export class PriceList extends React.Component { +// constructor(props: IProps) { +// super(props); +// } - render = () => this.props.data.map((value, index) => ); -} +// render = () => this.props.data.map((value, index) => ); +// } diff --git a/app/http/http.ts b/app/http/http.ts index d7537c1..929c7fb 100644 --- a/app/http/http.ts +++ b/app/http/http.ts @@ -1,7 +1,3 @@ import axios from 'axios'; export const http = axios.create(); - -export const setHeader = (header: string, value: string) => { - http.defaults.headers[header] = value; -}; diff --git a/app/model/index.ts b/app/model/index.ts index 72fd670..c6c87f8 100644 --- a/app/model/index.ts +++ b/app/model/index.ts @@ -1 +1,6 @@ +export * from './frame-type'; +export * from './item'; +export * from './item-price'; export * from './league'; +export * from './rarity-color'; +export * from './stash-tab'; diff --git a/app/model/item-price.ts b/app/model/item-price.ts new file mode 100644 index 0000000..d3c3af0 --- /dev/null +++ b/app/model/item-price.ts @@ -0,0 +1,14 @@ +export interface IItemPrice { + currency_rec: string; + data: any; + error: number; + item_base: string; + max_price: string; + median_price: string; + min_price: string; + name: string; + pred_explanation: any[]; + recommendedPrice: string; + resultsCounted: number; + status: number; +} diff --git a/app/model/item.ts b/app/model/item.ts index 5ea6ab2..08f7f00 100644 --- a/app/model/item.ts +++ b/app/model/item.ts @@ -1,6 +1,10 @@ +import { IItemPrice } from './item-price'; + export interface IItem { category: any[]; + corrupted: boolean; descrText: string; + elder: boolean; explicitMods: string[]; frameType: number; h: number; @@ -8,10 +12,15 @@ export interface IItem { id: string; identified: boolean; ilvl: number; + implicitMods: string[]; inventoryId: string; + itemPrice?: IItemPrice; league: string; name: string; properties: any[]; + requirements: any[]; + shaper: boolean; + sockets: any[]; stackSize?: number; typeLine: string; verified: boolean; diff --git a/app/services/item-text.service.ts b/app/services/item-text.service.ts index 3d68f82..b1a36db 100644 --- a/app/services/item-text.service.ts +++ b/app/services/item-text.service.ts @@ -1,9 +1,10 @@ import * as _ from 'lodash'; import { FrameType } from '../model/frame-type'; +import { IItem } from '../model/item'; const lineBreak = '--------\n'; -const parseItem = (item: any): string => { +const parseItem = (item: IItem): string => { let name = ''; name += `Rarity: ${FrameType[item.frameType]}\n`; diff --git a/app/services/poe.service.ts b/app/services/poe.service.ts index beb2e1a..f231d2b 100644 --- a/app/services/poe.service.ts +++ b/app/services/poe.service.ts @@ -1,8 +1,12 @@ -import PQueue from 'p-queue'; +import * as _ from 'lodash'; import { POE_HOME, POE_LEAGUE_LIST_URL, POE_STASH_ITEMS_URL } from '../constants'; import { http } from '../http'; +import { IItem } from '../model/item'; +import { PromiseQueue } from '../util'; +import { ItemTextService } from './item-text.service'; +import { StorageService } from './storage.service'; -const queue = new PQueue({ concurrency: 1 }); +const queue = new PromiseQueue(); const getStash = async (username: string, league: string, tabIndex: number | string): Promise => { const res = await http.get( @@ -11,32 +15,51 @@ const getStash = async (username: string, league: string, tabIndex: number | str return res.data; }; -const priceCheck = async (item: any): Promise => { +/** + * Clears the request queue. Used when switching leagues/stashes + */ +const clearRequestQueue = () => { + queue.clearQueue(); +}; + +const priceCheck = async (item: IItem, league: string): Promise => { + const itemCache = StorageService.getItemCache(item.id); + // return cached item if it exists - otherwise fetch from Poe Prices + if (itemCache) { + return Promise.resolve(itemCache); + } + return new Promise(resolve => { queue.add(() => { return http - .get(`https://poeprices.info/api?l=Incursion&i=${encodeURI(btoa(item))}`, { + .get(`https://poeprices.info/api?l=${league}&i=${encodeURI(btoa(ItemTextService.parseItem(item)))}`, { headers: { 'Cache-Control': 'max-age=600', }, }) - .then(resolve); - }); + .then(data => { + if (_.get(data, 'data.error') === 0) { + StorageService.storeItemCache(item.id, data.data); + } + return resolve(data.data); + }); + }, 1000); }); }; -const getUsername = async (): Promise => { - const res = await http.get(POE_HOME); +const getUsername = async (headers: any): Promise => { + const res = await http.get(POE_HOME, { headers }); const username = res.data.match(/\/account\/view-profile\/(.*?)\"/); return username[1]; }; -const getLeagues = async (): Promise => { - const res = await http.get(POE_LEAGUE_LIST_URL); +const getLeagues = async (headers: any): Promise => { + const res = await http.get(POE_LEAGUE_LIST_URL, { headers }); return res.data; }; export const PoeService = { + clearRequestQueue, getLeagues, getStash, getUsername, diff --git a/app/services/storage.service.ts b/app/services/storage.service.ts index eeeddfd..b32fb29 100644 --- a/app/services/storage.service.ts +++ b/app/services/storage.service.ts @@ -1,4 +1,7 @@ import Store from 'electron-store'; +import { DateTime } from 'luxon'; +import { IItemPrice } from '../model'; +import { IItem } from '../model/item'; const store = new Store(); const storeUsername = (username: string) => { @@ -17,9 +20,30 @@ const getUsername = (): string | undefined => { return store.get('user') || undefined; }; +const getItemCache = (id: string): IItem | undefined => { + const storedVal = store.get(id); + if (!storedVal) { + return undefined; + } + const val = JSON.parse(storedVal); + if (DateTime.fromISO(val.timestamp) < DateTime.local().plus({ minutes: 10 })) { + return val.item; + } + store.delete(id); + return undefined; +}; + +const storeItemCache = (id: string, item: IItemPrice) => { + const timestamp = DateTime.local(); + const val = JSON.stringify({ timestamp, item }); + store.set(id, val); +}; + export const StorageService = { + getItemCache, getUsername, getSessionID, + storeItemCache, storeUsername, storeSessionID, }; diff --git a/app/stores/app.store.ts b/app/stores/app.store.ts index 0f25156..c67d768 100644 --- a/app/stores/app.store.ts +++ b/app/stores/app.store.ts @@ -1,7 +1,7 @@ import Store from 'electron-store'; +import * as _ from 'lodash'; import { action, computed, observable } from 'mobx'; -import { setHeader } from '../http'; -import { ILeague } from '../model'; +import { IItemPrice, ILeague } from '../model'; import { IItem } from '../model/item'; import { IStashTab } from '../model/stash-tab'; import { PoeService } from '../services'; @@ -14,7 +14,7 @@ export class AppStore { @observable public sessionID?: string; @observable - public activeLeague: ILeague; + public activeLeague?: ILeague; @observable public selectedTabIndex: number = 0; @observable @@ -46,7 +46,7 @@ export class AppStore { @action private async loadLeagues() { try { - const leagues = await PoeService.getLeagues(); + const leagues = await PoeService.getLeagues(this.sessionCookieHeader); if (!leagues) { return; } @@ -67,19 +67,29 @@ export class AppStore { @action public resetState() { - delete this.username; - delete this.sessionID; - setHeader('Cookie', ''); + this.username = undefined; + this.sessionID = undefined; + this.activeLeague = undefined; + this.selectedTabIndex = 0; + this.stashTabs = []; + this.stashItems = []; + this.leagues = []; store.clear(); } @action public setSessionID(sessionID: string) { - setHeader('Cookie', `POESESSID=${sessionID}`); this.sessionID = sessionID; StorageService.storeSessionID(sessionID); } + @computed + public get sessionCookieHeader(): { Cookie: string } { + return { + Cookie: `POESESSID=${this.sessionID}`, + }; + } + @action public setUsername(username: string) { this.username = username; @@ -101,14 +111,27 @@ export class AppStore { @action public async performLogin(sessionID: string) { this.setSessionID(sessionID); - const username = await PoeService.getUsername(); + const username = await PoeService.getUsername(this.sessionCookieHeader); this.setUsername(username); + this.loadLeagues(); } @action public async loadItems(tabIndex: number | string) { - const data = await PoeService.getStash(this.username!, this.activeLeague.id, tabIndex); + PoeService.clearRequestQueue(); + const data = await PoeService.getStash(this.username!, this.activeLeague!.id, tabIndex); this.stashTabs = data.tabs; this.stashItems = data.items; } + + /** + * Updates the price of an item in the list - updates based on id + */ + @action + public updateItemPrice(id: string, itemPrice: IItemPrice) { + const item = _.find(this.stashItems, i => i.id === id); + if (item) { + item.itemPrice = itemPrice; + } + } } diff --git a/app/util/index.ts b/app/util/index.ts new file mode 100644 index 0000000..0d8f812 --- /dev/null +++ b/app/util/index.ts @@ -0,0 +1 @@ +export * from './promise-queue'; diff --git a/app/util/promise-queue.ts b/app/util/promise-queue.ts new file mode 100644 index 0000000..51dc623 --- /dev/null +++ b/app/util/promise-queue.ts @@ -0,0 +1,43 @@ +/** + * Allow promises to be queued up and executed in order. + */ +export class PromiseQueue { + private queue: any[] = []; + private locked: boolean = false; + + /** Add new promise to queue */ + add(p: () => Promise, delay?: number) { + this._add(p, false, delay); + } + + /** Add new promise to the from of the queue */ + addToBeginning(p: () => Promise, delay?: number) { + this._add(p, true, delay); + } + + /** Clear the queue and reset state. */ + clearQueue() { + this.queue = []; + } + + private delay = (ms: number) => () => new Promise(resolve => setTimeout(resolve, ms)); + + _add = (p: () => Promise, beginning: boolean = false, delay?: number) => { + const newPromise = delay ? [p, this.delay(delay)] : [p]; + beginning ? this.queue.unshift(newPromise) : this.queue.push(newPromise); + if (!this.locked) { + this.next(); + } + }; + + private next() { + this.locked = true; + const p = this.queue.shift(); + const finished = () => { + this.queue.length > 0 ? this.next() : (this.locked = false); + }; + Promise.all(p.map((v: any) => v())) + .then(finished) + .catch(finished); + } +} diff --git a/package-lock.json b/package-lock.json index 40c88da..8e152b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -97,16 +97,16 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.116.tgz", "integrity": "sha512-lRnAtKnxMXcYYXqOiotTmJd74uawNWuPnsnPrrO7HiFuE3npE2iQhfABatbYDyxTNqZNuXzcKGhw37R7RjBFLg==" }, + "@types/luxon": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-1.2.2.tgz", + "integrity": "sha512-JziyQbl0YIE36lVLDMLhkEhZ1h3Do/+H6x908tXRhzPFrcGAyh7mJ44rhDff+R230RaeIUKeWnhoB8lH5SdsPA==" + }, "@types/node": { "version": "7.0.69", "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.69.tgz", "integrity": "sha512-S5NC8HV6HnRipg8nC0j30TPl7ktXjRTKqgyINLNe8K/64UJUI8Lq0sRopXC0hProsV2F5ibj8IqPkl1xpGggrw==" }, - "@types/p-queue": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@types/p-queue/-/p-queue-2.3.1.tgz", - "integrity": "sha512-JyO7uMAtkcMMULmsTQ4t/lCC8nxirTtweGG1xAFNNIAoC1RemmeIxq8PiKghuEy99XdbS6Lwx4zpbXUjfeSSAA==" - }, "@types/prop-types": { "version": "15.5.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.5.5.tgz", @@ -8325,6 +8325,11 @@ "yallist": "2.1.2" } }, + "luxon": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.3.3.tgz", + "integrity": "sha512-3CM0jpS3mbHwWoPYprX1/Zsd5esni0LkhMfSiSY6xQ3/M3pnct3OPWbWkQdEEl9MO9593k6PvDn1DhxCkpuZEw==" + }, "macos-alias": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/macos-alias/-/macos-alias-0.2.11.tgz", @@ -9390,11 +9395,6 @@ "p-limit": "1.3.0" } }, - "p-queue": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-2.4.2.tgz", - "integrity": "sha512-n8/y+yDJwBjoLQe1GSJbbaYQLTI7QHNZI2+rpmCDbe++WLf9HC3gf6iqj5yfPAV71W4UF3ql5W1+UBPXoXTxng==" - }, "p-try": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", diff --git a/package.json b/package.json index 565a6e4..55d71cd 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "dependencies": { "@types/electron-store": "^1.3.0", "@types/lodash": "^4.14.116", - "@types/p-queue": "^2.3.1", + "@types/luxon": "^1.2.2", "@types/react": "^16.4.9", "@types/react-dom": "^16.0.7", "autoprefixer": "^9.1.1", @@ -68,12 +68,12 @@ "font-awesome": "^4.7.0", "html-webpack-plugin": "^3.2.0", "lodash": "^4.17.10", + "luxon": "^1.3.3", "mobx": "^5.1.0", "mobx-react": "^5.2.5", "node-sass": "^4.9.3", "normalize.css": "^8.0.0", "open-color": "^1.6.3", - "p-queue": "^2.4.2", "postcss-loader": "^3.0.0", "react": "^16.4.2", "react-dom": "^16.4.2",