You've already forked tf2wikipricing
bump version to 0.7.0
Reviewed-on: http://charon.local/git/xen/tf2wikipricing/pulls/18
This commit is contained in:
17
__tests__/currency.test.ts
Normal file
17
__tests__/currency.test.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { describe, expect, test, jest, mock, beforeEach } from "bun:test";
|
||||||
|
import { convertTF2PriceToUSD } from '../src/content/utils/currency'
|
||||||
|
|
||||||
|
mock.module('../src/content/config', () => ({
|
||||||
|
conversion_ref_usd: 0.05 // Mock conversion rate
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
describe('Currency Service', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
});
|
||||||
|
|
||||||
|
test('convertTF2PriceToUSD returns correct price', () => {
|
||||||
|
expect(convertTF2PriceToUSD(1, 10, 50)).toBe(3);
|
||||||
|
})
|
||||||
|
})
|
||||||
94
__tests__/exchangeRateService.test.ts
Normal file
94
__tests__/exchangeRateService.test.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { describe, expect, it, jest, mock, beforeEach, beforeAll } from "bun:test";
|
||||||
|
import { prepareExchangeRates, wipeExchangeRates } from '../src/content/exchangeRateService.ts'
|
||||||
|
import { getStorageValue, setStorageValue } from '../src/content/storage'
|
||||||
|
import { storage_exchangerates, storage_exchangerates_next, storage_exchangerates_update } from "../src/content/config";
|
||||||
|
import { logDebug, logError } from '../src/content/utils/log';
|
||||||
|
declare function GM_fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>
|
||||||
|
|
||||||
|
// Mock the storage functions
|
||||||
|
mock.module('../src/content/storage', () => ({
|
||||||
|
setStorageValue: mock(async (key: string, value: any) => {}),
|
||||||
|
getStorageValue: mock(async (key: string, defaultValue: any) => defaultValue)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the log functions
|
||||||
|
mock.module('../src/content/utils/log', () => ({
|
||||||
|
logDebug: mock(() => {}),
|
||||||
|
log: mock(() => {}),
|
||||||
|
logError: mock(() => {})
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('wipeExchangeRates', () => {
|
||||||
|
it('should clear exchange rate storage and set update timestamps', async () => {
|
||||||
|
await wipeExchangeRates();
|
||||||
|
|
||||||
|
expect(setStorageValue).toHaveBeenCalledWith(storage_exchangerates, null);
|
||||||
|
expect(setStorageValue).toHaveBeenCalledWith(storage_exchangerates_update, expect.any(String));
|
||||||
|
expect(setStorageValue).toHaveBeenCalledWith(storage_exchangerates_next, expect.any(String));
|
||||||
|
expect(logDebug).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prepareExchangeRates', () => {
|
||||||
|
const mockRates = { USD: 1, EUR: 0.85, GBP: 0.73 };
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
jest
|
||||||
|
.spyOn(global, 'GM_fetch')
|
||||||
|
.mockImplementation(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
status: 500
|
||||||
|
} as Response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return stored rates if they are up to date', async () => {
|
||||||
|
(getStorageValue as jest.Mock).mockImplementation(async (key) => {
|
||||||
|
if (key === storage_exchangerates) return mockRates;
|
||||||
|
if (key === storage_exchangerates_update) return new Date().toISOString();
|
||||||
|
if (key === storage_exchangerates_next) return new Date(Date.now() + 100000).toISOString();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const rates = await prepareExchangeRates();
|
||||||
|
expect(rates).toEqual(mockRates);
|
||||||
|
expect(GM_fetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch new rates when they are expired', async () => {
|
||||||
|
(getStorageValue as jest.Mock).mockImplementation(async (key) => {
|
||||||
|
if (key === storage_exchangerates_update) return new Date(Date.now() - 100000).toISOString();
|
||||||
|
if (key === storage_exchangerates_next) return new Date(Date.now() - 50000).toISOString();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
(GM_fetch as jest.Mock).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
rates: mockRates,
|
||||||
|
time_next_update_utc: new Date(Date.now() + 100000).toISOString()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const rates = await prepareExchangeRates();
|
||||||
|
expect(rates).toEqual(mockRates);
|
||||||
|
expect(GM_fetch).toHaveBeenCalled();
|
||||||
|
expect(setStorageValue).toHaveBeenCalledWith(storage_exchangerates, mockRates);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle fetch errors gracefully', async () => {
|
||||||
|
(getStorageValue as jest.Mock).mockResolvedValue(null);
|
||||||
|
(GM_fetch as jest.Mock).mockResolvedValueOnce({
|
||||||
|
ok: false,
|
||||||
|
status: 500
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const rates = await prepareExchangeRates();
|
||||||
|
expect(rates).toBeNull();
|
||||||
|
expect(logError).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -16,34 +16,34 @@ describe('formatPrice', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('formats price with keys and metal', () => {
|
test('formats price with keys and metal', () => {
|
||||||
expect(formatPrice(2, 10, 50)).toBe('2.2 keys (US$5.50)');
|
expect(formatPrice(2, 10, 50)).toBe('2.2 keys');
|
||||||
expect($T).toHaveBeenCalledWith('%@ keys');
|
expect($T).toHaveBeenCalledWith('%@ keys');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('formats price with metal only', () => {
|
test('formats price with metal only', () => {
|
||||||
expect(formatPrice(0, 15.75, 50)).toBe('15.75 ref (US$0.79)');
|
expect(formatPrice(0, 15.75, 50)).toBe('15.75 ref');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('formats price with metal only and whole number', () => {
|
test('formats price with metal only and whole number', () => {
|
||||||
expect(formatPrice(0, 3, 50)).toBe('3 ref (US$0.16)');
|
expect(formatPrice(0, 3, 50)).toBe('3 ref');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('uses singular key form', () => {
|
test('uses singular key form', () => {
|
||||||
expect(formatPrice(1, 0, 50)).toBe('1 key (US$2.50)');
|
expect(formatPrice(1, 0, 50)).toBe('1 key');
|
||||||
expect($T).toHaveBeenCalledWith('%@ key');
|
expect($T).toHaveBeenCalledWith('%@ key');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rounds USD up to nearest cent', () => {
|
test('rounds USD up to nearest cent', () => {
|
||||||
expect(formatPrice(3, 7.33, 35)).toBe('3.21 keys (US$5.62)'); // (3 * 35 + 7.33) * 0.05 = 5.6165 → 5.62
|
expect(formatPrice(3, 7.33, 35)).toBe('3.21 keys'); // (3 * 35 + 7.33) * 0.05 = 5.6165 → 5.62
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handles different locale formatting', () => {
|
test('handles different locale formatting', () => {
|
||||||
expect(formatPrice(2, 10, 50, 'de')).toMatch(/2,2 keys \(US\$5,50\)/);
|
expect(formatPrice(2, 10, 50, 'de')).toMatch(/2,2 keys/);
|
||||||
expect(formatPrice(0, 15.75, 50, 'de')).toMatch(/15,75 ref \(US\$0,79\)/);
|
expect(formatPrice(0, 15.75, 50, 'de')).toMatch(/15,75 ref/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handles zero values', () => {
|
test('handles zero values', () => {
|
||||||
expect(formatPrice(0, 0, 50)).toBe('0 ref (US$0.00)');
|
expect(formatPrice(0, 0, 50)).toBe('0 ref');
|
||||||
expect(formatPrice(0, 0, 50, 'de')).toMatch(/0 ref \(US\$0,00\)/);
|
expect(formatPrice(0, 0, 50, 'de')).toMatch(/0 ref/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tf2wikipricing",
|
"name": "tf2wikipricing",
|
||||||
"version": "0.6.0",
|
"version": "0.7.0",
|
||||||
"description": "Adds item pricing to the Team Fortress 2 wiki",
|
"description": "Adds item pricing to the Team Fortress 2 wiki",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@happy-dom/global-registrator": "^17.4.4",
|
"@happy-dom/global-registrator": "^17.4.4",
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ export const storage_lastUpdateTime = 'tf2wikipricing_lastUpdate';
|
|||||||
export const storage_schema = 'tf2wikipricing_schema';
|
export const storage_schema = 'tf2wikipricing_schema';
|
||||||
export const storage_version = 'tf2wikipricing_version';
|
export const storage_version = 'tf2wikipricing_version';
|
||||||
export const storage_priceprefix = 'tf2wikipricing_sku_';
|
export const storage_priceprefix = 'tf2wikipricing_sku_';
|
||||||
|
export const storage_exchangerates = 'tf2wikipricing_exchangerates';
|
||||||
|
export const storage_exchangerates_update = 'tf2wikipricing_exchangerates_update';
|
||||||
|
export const storage_exchangerates_next = 'tf2wikipricing_exchangerates_next';
|
||||||
export const conversion_ref_usd = 0.0265;
|
export const conversion_ref_usd = 0.0265;
|
||||||
export const defindex_key = 5021;
|
export const defindex_key = 5021;
|
||||||
export const defindex_metal_refined = 5002;
|
export const defindex_metal_refined = 5002;
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import { fetchPrice, fetchKeyPrice, ItemPriceData } from './priceService'
|
|||||||
import { createPriceRow, createStoreButton } from './uiRenderer'
|
import { createPriceRow, createStoreButton } from './uiRenderer'
|
||||||
import { findFirstElement, findFirstChildElement } from './utils/dom'
|
import { findFirstElement, findFirstChildElement } from './utils/dom'
|
||||||
import { extractPageTitleFromURL } from './utils/url';
|
import { extractPageTitleFromURL } from './utils/url';
|
||||||
|
import { ExchangeRates, prepareExchangeRates } from './exchangeRateService';
|
||||||
var itemSchema: ItemSchema | null;
|
var itemSchema: ItemSchema | null;
|
||||||
|
var exchangeRates: ExchangeRates | null;
|
||||||
|
|
||||||
var locale: string = 'en'
|
var locale: string = 'en'
|
||||||
|
|
||||||
@@ -220,7 +222,7 @@ async function inject() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const qualityName = itemQualities[quality as unknown as keyof typeof itemQualities].toString()
|
const qualityName = itemQualities[quality as unknown as keyof typeof itemQualities].toString()
|
||||||
const priceRow = createPriceRow(qualityName, data, keyPrice, locale)
|
const priceRow = createPriceRow(qualityName, data, keyPrice, exchangeRates, locale)
|
||||||
|
|
||||||
priceRows.push({order: quality == 6 ? -1 : quality, row: priceRow, category: PriceRowCategory.None})
|
priceRows.push({order: quality == 6 ? -1 : quality, row: priceRow, category: PriceRowCategory.None})
|
||||||
})
|
})
|
||||||
@@ -237,7 +239,7 @@ async function inject() {
|
|||||||
log(`Australium ${itemName} is unpriced or unavailable, skipping...`)
|
log(`Australium ${itemName} is unpriced or unavailable, skipping...`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const priceRow = createPriceRow($T("Australium"), data, keyPrice, locale, "https://wiki.teamfortress.com/wiki/Australium_weapons")
|
const priceRow = createPriceRow($T("Australium"), data, keyPrice, exchangeRates, locale, "https://wiki.teamfortress.com/wiki/Australium_weapons")
|
||||||
|
|
||||||
priceRows.push({order: 99, row: priceRow, category: PriceRowCategory.None})
|
priceRows.push({order: 99, row: priceRow, category: PriceRowCategory.None})
|
||||||
resolve()
|
resolve()
|
||||||
@@ -269,7 +271,7 @@ async function inject() {
|
|||||||
log(`Festive ${itemName} is unpriced or unavailable, skipping...`)
|
log(`Festive ${itemName} is unpriced or unavailable, skipping...`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const priceRow = createPriceRow($T("Unique"), data, keyPrice, locale)
|
const priceRow = createPriceRow($T("Unique"), data, keyPrice, exchangeRates, locale)
|
||||||
|
|
||||||
priceRows.push({order: -1, row: priceRow, category: PriceRowCategory.Festive})
|
priceRows.push({order: -1, row: priceRow, category: PriceRowCategory.Festive})
|
||||||
resolve()
|
resolve()
|
||||||
@@ -285,7 +287,7 @@ async function inject() {
|
|||||||
log(`Strange Festive ${itemName} is unpriced or unavailable, skipping...`)
|
log(`Strange Festive ${itemName} is unpriced or unavailable, skipping...`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const priceRow = createPriceRow($T("Strange"), data, keyPrice, locale)
|
const priceRow = createPriceRow($T("Strange"), data, keyPrice, exchangeRates, locale)
|
||||||
|
|
||||||
priceRows.push({order: 11, row: priceRow, category: PriceRowCategory.Festive})
|
priceRows.push({order: 11, row: priceRow, category: PriceRowCategory.Festive})
|
||||||
resolve()
|
resolve()
|
||||||
@@ -329,7 +331,7 @@ async function inject() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const priceRow = createPriceRow($T(`kt-${tier}`), data, keyPrice, locale, "https://wiki.teamfortress.com/wiki/Killstreak_Kit")
|
const priceRow = createPriceRow($T(`kt-${tier}`), data, keyPrice, exchangeRates, locale, "https://wiki.teamfortress.com/wiki/Killstreak_Kit")
|
||||||
|
|
||||||
priceRows.push({order: tier, row: priceRow, category: PriceRowCategory.KillstreakKit})
|
priceRows.push({order: tier, row: priceRow, category: PriceRowCategory.KillstreakKit})
|
||||||
resolve()
|
resolve()
|
||||||
@@ -376,7 +378,7 @@ async function inject() {
|
|||||||
log(`${itemName} is unpriced or unavailable, skipping...`)
|
log(`${itemName} is unpriced or unavailable, skipping...`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const priceRow = createPriceRow($T(variantName), data, keyPrice, locale, "https://wiki.teamfortress.com/wiki/Botkiller_weapons")
|
const priceRow = createPriceRow($T(variantName), data, keyPrice, exchangeRates, locale, "https://wiki.teamfortress.com/wiki/Botkiller_weapons")
|
||||||
|
|
||||||
// FIXME: order should be by release
|
// FIXME: order should be by release
|
||||||
// Silver Mk.I, Gold Mk.II, Rust, Blood, Carbonado, Diamond, Silver Mk.II, Gold Mk.II
|
// Silver Mk.I, Gold Mk.II, Rust, Blood, Carbonado, Diamond, Silver Mk.II, Gold Mk.II
|
||||||
@@ -425,7 +427,8 @@ async function inject() {
|
|||||||
label.style.fontSize = "85%";
|
label.style.fontSize = "85%";
|
||||||
const updateText = $T("Updated %@.", locale).replace('%@', updateTime.toLocaleString(locale, { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZoneName: 'short' }))
|
const updateText = $T("Updated %@.", locale).replace('%@', updateTime.toLocaleString(locale, { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZoneName: 'short' }))
|
||||||
const attributionText = $T("Trade prices sourced from %@. Currency conversions are approximate.", locale).replace('%@', '<a rel="nofollow" class="external text" href="https://prices.tf">prices.tf</a>');
|
const attributionText = $T("Trade prices sourced from %@. Currency conversions are approximate.", locale).replace('%@', '<a rel="nofollow" class="external text" href="https://prices.tf">prices.tf</a>');
|
||||||
label.innerHTML = `${updateText}<br>${attributionText}`;
|
const exchangeRateAttribution = `<a rel="nofollow" class="external text" href="https://www.exchangerate-api.com">Rates By Exchange Rate API</a>.`;
|
||||||
|
label.innerHTML = `${updateText}<br>${attributionText}<br>${exchangeRateAttribution}`;
|
||||||
row.appendChild(label);
|
row.appendChild(label);
|
||||||
|
|
||||||
priceProgressRow.insertAdjacentElement('afterend', row);
|
priceProgressRow.insertAdjacentElement('afterend', row);
|
||||||
@@ -440,15 +443,22 @@ function addStyles() {
|
|||||||
style.innerHTML = styleCss;
|
style.innerHTML = styleCss;
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareSchema().then(function (schema) {
|
prepareSchema()
|
||||||
|
.then(schema => {
|
||||||
itemSchema = schema;
|
itemSchema = schema;
|
||||||
if (!itemSchema) {
|
if (!itemSchema) {
|
||||||
logError("No item schema ready, exiting.");
|
|
||||||
wipeSchema(); // FIXME: ugly hack. requires additional page reload. if prepareSchema returns null, we should handle it properly
|
wipeSchema(); // FIXME: ugly hack. requires additional page reload. if prepareSchema returns null, we should handle it properly
|
||||||
return;
|
throw new Error("No item schema ready");
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.then(prepareExchangeRates)
|
||||||
|
.then(rates => exchangeRates = rates)
|
||||||
|
.then(() => {
|
||||||
locale = extractLocaleFromURL(document.URL)
|
locale = extractLocaleFromURL(document.URL)
|
||||||
addStyles();
|
addStyles();
|
||||||
inject();
|
inject();
|
||||||
// TODO: Purge expired price data
|
// TODO: Purge expired price data
|
||||||
});
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logError(error);
|
||||||
|
})
|
||||||
55
src/content/exchangeRateService.ts
Normal file
55
src/content/exchangeRateService.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { getStorageValue, setStorageValue } from './storage'
|
||||||
|
import { logDebug, log, logError } from './utils/log'
|
||||||
|
import { storage_exchangerates, storage_exchangerates_next, storage_exchangerates_update } from './config'
|
||||||
|
declare function GM_fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>
|
||||||
|
import './GM_fetch'
|
||||||
|
|
||||||
|
export interface ExchangeRates {
|
||||||
|
[key: string]: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function wipeExchangeRates(): Promise<void> {
|
||||||
|
await setStorageValue(storage_exchangerates, null)
|
||||||
|
await setStorageValue(storage_exchangerates_update, new Date().toISOString())
|
||||||
|
await setStorageValue(storage_exchangerates_next, new Date().toISOString())
|
||||||
|
logDebug(`Exchange rates wiped`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prepareExchangeRates(): Promise<ExchangeRates> {
|
||||||
|
var needsUpdate: Boolean = false
|
||||||
|
var rates: ExchangeRates | null = null
|
||||||
|
|
||||||
|
rates = await getStorageValue(storage_exchangerates, null);
|
||||||
|
const update = await getStorageValue(storage_exchangerates_update, null)
|
||||||
|
const nextUpdate = await getStorageValue(storage_exchangerates_next, null)
|
||||||
|
if (update && nextUpdate) {
|
||||||
|
const lastUpdateTime = new Date(update);
|
||||||
|
const nextUpdateTime = new Date(nextUpdate);
|
||||||
|
log(`Exchange rates updated at ${lastUpdateTime}`);
|
||||||
|
if (rates == null || Object.keys(rates).length === 0 || lastUpdateTime.getTime() > nextUpdateTime.getTime()) {
|
||||||
|
needsUpdate = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
needsUpdate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if(needsUpdate) {
|
||||||
|
log("Exchange rates out of Date. Rebuilding...");
|
||||||
|
const url = "https://open.er-api.com/v6/latest/USD"
|
||||||
|
const response = await GM_fetch(url);
|
||||||
|
if (response.ok) {
|
||||||
|
await setStorageValue(storage_exchangerates_update, new Date().toISOString())
|
||||||
|
var json = await response.json()
|
||||||
|
if(json != null){
|
||||||
|
rates = json['rates']
|
||||||
|
await setStorageValue(storage_exchangerates, rates)
|
||||||
|
await setStorageValue(storage_exchangerates_next, json['time_next_update_utc'])
|
||||||
|
}
|
||||||
|
logDebug(`Exchange rates updated at ${new Date()}`)
|
||||||
|
} else {
|
||||||
|
logError(`Failed to fetch exchange rates. Status code: ${response.status}`, response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rates
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { ExchangeRates } from "./exchangeRateService";
|
||||||
import { ItemPriceData } from "./priceService";
|
import { ItemPriceData } from "./priceService";
|
||||||
|
import { convertTF2PriceToUSD, defaultCurrencyForPageLocale, convertUSD } from "./utils/currency";
|
||||||
import { formatPrice } from "./utils/formatting";
|
import { formatPrice } from "./utils/formatting";
|
||||||
import { $T } from "./utils/localization";
|
import { $T } from "./utils/localization";
|
||||||
|
import { logError } from "./utils/log";
|
||||||
|
|
||||||
export function createPriceRow(qualityName: string, data: ItemPriceData, keyPrice: ItemPriceData, locale: string, wikiPage: string = null): HTMLTableRowElement {
|
export function createPriceRow(qualityName: string, data: ItemPriceData, keyPrice: ItemPriceData, rates: ExchangeRates, locale: string, wikiPage: string = null): HTMLTableRowElement {
|
||||||
const priceRow = document.createElement("tr");
|
const priceRow = document.createElement("tr");
|
||||||
|
|
||||||
const priceLabel = document.createElement("td");
|
const priceLabel = document.createElement("td");
|
||||||
@@ -19,8 +22,33 @@ export function createPriceRow(qualityName: string, data: ItemPriceData, keyPric
|
|||||||
|
|
||||||
const priceData = document.createElement("td");
|
const priceData = document.createElement("td");
|
||||||
const priceLink = document.createElement("span");
|
const priceLink = document.createElement("span");
|
||||||
const priceString = data ? formatPrice(data.keys, data.metal, keyPrice.metal, locale).trim() : $T('Data unavailable')
|
var priceString: string = ''
|
||||||
priceLink.innerHTML = priceString // + `<br>$${data.scmPrice}`
|
|
||||||
|
if(data) {
|
||||||
|
const gamePrice = formatPrice(data.keys, data.metal, keyPrice.metal, locale).trim()
|
||||||
|
const realPriceUSD = convertTF2PriceToUSD(data.keys, data.metal, keyPrice.metal)
|
||||||
|
|
||||||
|
const USDFormatter = new Intl.NumberFormat(locale, { style: "currency", currency: 'USD' })
|
||||||
|
var realPriceString = USDFormatter.format(realPriceUSD)
|
||||||
|
const currency = defaultCurrencyForPageLocale(locale) ?? 'USD'
|
||||||
|
if(currency !== 'USD') {
|
||||||
|
try {
|
||||||
|
const realPrice = convertUSD(realPriceUSD, currency, rates)
|
||||||
|
const currencyFormatter = new Intl.NumberFormat(locale, { style: "currency", currency: currency })
|
||||||
|
realPriceString = currencyFormatter.format(realPrice)
|
||||||
|
} catch (e) {
|
||||||
|
logError(`Failed to convert USD ${realPriceUSD} to ${currency}`, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
priceString += [
|
||||||
|
gamePrice,
|
||||||
|
'(' + realPriceString + ')'
|
||||||
|
].join(' ').trim()
|
||||||
|
} else {
|
||||||
|
priceString += $T('Data unavailable')
|
||||||
|
}
|
||||||
|
priceLink.innerHTML = priceString
|
||||||
priceData.appendChild(priceLink);
|
priceData.appendChild(priceLink);
|
||||||
priceRow.appendChild(priceData);
|
priceRow.appendChild(priceData);
|
||||||
return priceRow;
|
return priceRow;
|
||||||
|
|||||||
54
src/content/utils/currency.ts
Normal file
54
src/content/utils/currency.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { conversion_ref_usd } from '../config';
|
||||||
|
import { ExchangeRates } from '../exchangeRateService';
|
||||||
|
|
||||||
|
export function defaultCurrencyForPageLocale(locale: string): string | null {
|
||||||
|
switch (locale) {
|
||||||
|
case 'cs':
|
||||||
|
return 'CZK';
|
||||||
|
case 'da':
|
||||||
|
return 'DKK';
|
||||||
|
case 'de':
|
||||||
|
case 'fi':
|
||||||
|
case 'fr':
|
||||||
|
case 'it':
|
||||||
|
case 'nl':
|
||||||
|
case 'hu':
|
||||||
|
case 'pt':
|
||||||
|
return 'EUR';
|
||||||
|
case 'ja':
|
||||||
|
return 'JPY';
|
||||||
|
case 'ko':
|
||||||
|
return 'KRW';
|
||||||
|
case 'pl':
|
||||||
|
return 'PLN';
|
||||||
|
case 'pt-br':
|
||||||
|
return 'BRL';
|
||||||
|
case 'ro':
|
||||||
|
return 'RON';
|
||||||
|
case 'ru':
|
||||||
|
return 'RUB';
|
||||||
|
case 'sv':
|
||||||
|
return 'SEK';
|
||||||
|
case 'tr':
|
||||||
|
return 'TRY';
|
||||||
|
case 'zh-hans':
|
||||||
|
case 'zh-hant':
|
||||||
|
return 'CNY';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertTF2PriceToUSD(keys: number, metal: number, keyPrice: number): number {
|
||||||
|
const pureMetal = (keys * keyPrice) + metal;
|
||||||
|
|
||||||
|
// Round price up to nearest cent
|
||||||
|
return Math.ceil(pureMetal * conversion_ref_usd * 100) / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertUSD(usd: number, currency: string, rates: ExchangeRates): number {
|
||||||
|
const cur = currency.toUpperCase()
|
||||||
|
if(cur === 'USD') return usd
|
||||||
|
|
||||||
|
return usd * rates[cur]
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ function toFixed(num: number, fixed: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function formatPrice(keys: number, metal: number, keyPrice: number, locale: string = 'en') {
|
export function formatPrice(keys: number, metal: number, keyPrice: number, locale: string = 'en') {
|
||||||
const pureMetal = (keys * keyPrice) + metal;
|
|
||||||
const formattedKeys = +(keys + (metal / keyPrice)).toFixed(2)
|
const formattedKeys = +(keys + (metal / keyPrice)).toFixed(2)
|
||||||
|
|
||||||
var output: string = ''
|
var output: string = ''
|
||||||
@@ -16,13 +15,6 @@ export function formatPrice(keys: number, metal: number, keyPrice: number, local
|
|||||||
} else {
|
} else {
|
||||||
output += $T("%@ ref").replace('%@', (+toFixed(metal, 2)).toLocaleString(locale))
|
output += $T("%@ ref").replace('%@', (+toFixed(metal, 2)).toLocaleString(locale))
|
||||||
}
|
}
|
||||||
const currencyFormatter = new Intl.NumberFormat(locale, {
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Round price up to nearest cent
|
|
||||||
const price = Math.ceil(pureMetal * conversion_ref_usd * 100) / 100
|
|
||||||
output += ` (US$${currencyFormatter.format(price)})`
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user