diff --git a/src/index.html b/src/index.html index 91314a3..76f5fe2 100644 --- a/src/index.html +++ b/src/index.html @@ -7,7 +7,10 @@ @@favicons - + @@socialMediaTags @@ -40,7 +43,11 @@

-:-

- + -:- - + @@ -71,8 +80,13 @@

-:-

+
+ + +
+

- Bitcoin price is + Crypto price is ...

@@ -114,6 +128,7 @@

Open Source | API +

@@ -136,10 +151,11 @@

+ - + diff --git a/src/js/api.js b/src/js/api.js index c55fc68..aa03b9f 100644 --- a/src/js/api.js +++ b/src/js/api.js @@ -8,14 +8,11 @@ class API { get(_endpoint) { App.Loader.init(); - return axios.get(this.apiAdapter.baseURL + _endpoint) - .then( r => r.data ) - .then (r => { - App.Message.clear(); - App.Loader.destroy(); - - return r; - }); + return this.apiAdapter.getBitcoinRatesForPeriod(_endpoint).then((r) => { + App.Message.clear(); + App.Loader.destroy(); + return r; + }); } mapData(_r, _period) { @@ -49,8 +46,8 @@ class API { getBitcoinRatesNow() { return this.apiAdapter.getBitcoinRatesNow(); } -}; +} // window.App.API = new API(App.apiFakeAdapter); //window.App.API = new API(App.apiGoranAdapter); -window.App.API = new API(App.apiCecoAdapter); +window.App.API = new API(App.apiBoyoAdapter); diff --git a/src/js/apiBoyoAdapter.js b/src/js/apiBoyoAdapter.js new file mode 100644 index 0000000..b88af06 --- /dev/null +++ b/src/js/apiBoyoAdapter.js @@ -0,0 +1,28 @@ +window.App.apiBoyoAdapter = { + mapData: function (response, dateLabelFormat) { + return response + .map((_rec) => ({ + value: _rec.value, + timestamp: dayjs + .utc(_rec.timestamp * 1000) + .local() + .format(dateLabelFormat), + })) + .reverse(); + }, + + getCryptoRatesForPeriod: function (period, cryptoType) { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage( + { type: 'getCryptoPrice', period: period, cryptoType: cryptoType }, + (response) => { + if (response && !response.error) { + resolve(response.data); + } else { + reject(response.error || `Failed to retrieve ${cryptoType} price data`); + } + } + ); + }); + }, +}; diff --git a/src/js/apiCecoAdapter.js b/src/js/apiCecoAdapter.js index 2b7534b..1135b48 100644 --- a/src/js/apiCecoAdapter.js +++ b/src/js/apiCecoAdapter.js @@ -1,12 +1,6 @@ window.App = window.App || {}; window.App.apiCecoAdapter = { - baseURL: 'https://api.crypto-tab.com/v1/', - - get: function (_endpoint) { - return App.API.get(_endpoint); - }, - mapData: function (response, dateLabelFormat) { return response .map((_rec) => ({ @@ -19,44 +13,43 @@ window.App.apiCecoAdapter = { .reverse(); }, - _createDateAsUTC: function (date) { - return new Date( - Date.UTC( - date.getFullYear(), - date.getMonth(), - date.getDate(), - date.getHours(), - date.getMinutes(), - date.getSeconds() - ) - ); + getBitcoinRatesForPeriod: function (period) { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage({ type: 'getBitcoinPrice', period: period }, (response) => { + if (response && !response.error) { + resolve(response.data); + } else { + reject(response.error || 'Failed to retrieve Bitcoin price data'); + } + }); + }); }, getBitcoinRatesForAll: function () { - return this.get('bitcoin/all'); + return this.getBitcoinRatesForPeriod('ALL'); }, getBitcoinRatesForOneYear: function () { - return this.get('bitcoin/year'); + return this.getBitcoinRatesForPeriod('ONE_YEAR'); }, getBitcoinRatesForOneMonth: function () { - return this.get('bitcoin/month'); + return this.getBitcoinRatesForPeriod('ONE_MONTH'); }, getBitcoinRatesForOneWeek: function () { - return this.get('bitcoin/week'); + return this.getBitcoinRatesForPeriod('ONE_WEEK'); }, getBitcoinRatesForOneDay: function () { - return this.get('bitcoin/day'); + return this.getBitcoinRatesForPeriod('ONE_DAY'); }, getBitcoinRatesForOneHour: function () { - return this.get('bitcoin/hour'); + return this.getBitcoinRatesForPeriod('ONE_HOUR'); }, getBitcoinRatesNow: function () { - return this.get('bitcoin/now'); + return this.getBitcoinRatesForPeriod('NOW'); }, }; diff --git a/src/js/background.js b/src/js/background.js new file mode 100644 index 0000000..639873b --- /dev/null +++ b/src/js/background.js @@ -0,0 +1,34 @@ +let cryptoPriceData = { + bitcoin: {}, + ethereum: {}, +}; + +async function fetchCryptoPrice(period, cryptoType) { + const endpoints = { + ALL: `${cryptoType}/all`, + ONE_YEAR: `${cryptoType}/year`, + ONE_MONTH: `${cryptoType}/month`, + ONE_WEEK: `${cryptoType}/week`, + ONE_DAY: `${cryptoType}/day`, + ONE_HOUR: `${cryptoType}/hour`, + NOW: `${cryptoType}/now`, + }; + + try { + const response = await fetch(`http://localhost:3000/v1/${endpoints[period]}`); // TODO: Update to use the deployed API (maybe use a config file to store the base URL) + const data = await response.json(); + cryptoPriceData[cryptoType][period] = data; + } catch (error) { + console.error(`Error fetching ${cryptoType} price for ${period}:`, error); + } +} + +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.type === 'getCryptoPrice') { + const { period, cryptoType } = request; + fetchCryptoPrice(period, cryptoType).then(() => { + sendResponse({ data: cryptoPriceData[cryptoType][period], cached: false }); + }); + return true; // Indicates that the response will be sent asynchronously + } +}); diff --git a/src/js/bitcoin.js b/src/js/bitcoin.js deleted file mode 100644 index de9801d..0000000 --- a/src/js/bitcoin.js +++ /dev/null @@ -1,259 +0,0 @@ -dayjs.extend(window.dayjs_plugin_utc); -dayjs.extend(window.dayjs_plugin_localizedFormat); -dayjs.extend(window.dayjs_plugin_relativeTime); -dayjs.extend(window.dayjs_plugin_calendar); - -window.App = window.App || {}; - -window.App.Bitcoin = { - PERIODS: { - ONE_HOUR: 'ONE_HOUR', - ONE_DAY: 'ONE_DAY', - ONE_WEEK: 'ONE_WEEK', - ONE_MONTH: 'ONE_MONTH', - ONE_YEAR: 'ONE_YEAR', - ALL: 'ALL', - }, - isLocalChartDataOld: false, - isLocalNowDataOld: false, - - $chart: document.getElementById('chart'), - chart: null, - - $dataPeriods: document.querySelectorAll('.js-period'), - initEvents() { - const self = this; - - [...self.$dataPeriods].forEach((el) => { - el.addEventListener('click', function () { - self.$dataPeriods.forEach((p) => p.classList.remove('active')); - this.classList.add('active'); - - const period = this.dataset.period; - self.repositories[period] - .getData() - .then((_data) => self.chart.init(_data)) - .catch((error) => { - self.handleChartRejection(period, error); - }); - - App.Settings.set('period', this.dataset.period); - - self.setPriceChange(); - }); - }); - - App.Settings.get().then(({ period }) => { - const selectedTab = period ? Object.keys(this.PERIODS).indexOf(period) : 1; - - self.$dataPeriods[selectedTab].click(); - }); - }, - - getBitcoinData(period) { - switch (period) { - case 'ALL': - return App.API.getBitcoinRatesForAll(); - case 'ONE_YEAR': - return App.API.getBitcoinRatesForOneYear(); - case 'ONE_MONTH': - return App.API.getBitcoinRatesForOneMonth(); - case 'ONE_WEEK': - return App.API.getBitcoinRatesForOneWeek(); - case 'ONE_DAY': - return App.API.getBitcoinRatesForOneDay(); - case 'ONE_HOUR': - return App.API.getBitcoinRatesForOneHour(); - } - }, - - getLabelFormat(period) { - switch (period) { - case 'ALL': - return 'YYYY'; - case 'ONE_YEAR': - return 'MMM YYYY'; - case 'ONE_MONTH': - return 'Do MMM'; - case 'ONE_WEEK': - return 'dddd'; - case 'ONE_DAY': - return 'HH:mm'; - case 'ONE_HOUR': - return 'HH:mm'; - } - }, - - handleChartRejection(_period, _error) { - this.isLocalChartDataOld = true; - - this.repositories[_period].getDataUpToDateStatus().then((_res) => { - App.Loader.destroy(); - - if (_res.localData === null) { - App.Message.fireError("That's extremely sad. " + _error); - this.chart.destroy(); - } else if (_res.localData.length) { - App.Message.clear(); - - this.chart.init(_res.localData); - this.setLastUpdated(true); - } - }); - }, - handleNowRejection() { - this.isLocalNowDataOld = true; - - App.Loader.destroy(); - }, - - repositories: {}, - initRepositories() { - const storageSetting = - App.ENV.platform === 'EXTENSION' ? 'BROWSER_STORAGE' : 'LOCAL_STORAGE'; - - Object.keys(this.PERIODS).forEach( - (period) => - (this.repositories[period] = new SuperRepo({ - storage: storageSetting, - name: 'bitcoin-' + period, - outOfDateAfter: 15 * 60 * 1000, - mapData: (r) => App.API.mapData(r, this.getLabelFormat(period)), - request: () => - this.getBitcoinData(period) - .then((res) => { - this.isLocalChartDataOld = false; - return res; - }) - .catch((jqXHR, textStatus, errorThrown) => { - this.handleChartRejection(period, jqXHR); - }), - })) - ); - - this.repositories['NOW'] = new SuperRepo({ - storage: storageSetting, - name: 'bitcoin-NOW', - outOfDateAfter: 3 * 60 * 1000, - mapData: (data) => { - const { value, changePercent } = data[0]; - const { dayAgo, weekAgo, monthAgo } = changePercent; - - return { - price: value, - changePercent: { dayAgo, weekAgo, monthAgo }, - }; - }, - request: () => - App.API.getBitcoinRatesNow() - .then((res) => { - this.isLocalNowDataOld = false; - return res; - }) - .catch(() => { - this.handleNowRejection(); - }), - }); - }, - - $priceNow: document.querySelector('#price-now'), - setPriceNow(_price) { - this.$priceNow.textContent = App.Utils.formatPrice(Math.round(_price)); - }, - - $change: document.querySelector('#change'), - async setPriceChange() { - let { localData } = await this.repositories['NOW'].getDataUpToDateStatus(); - if (!localData) { - return; - } - - this.setPriceNow(localData.price); - - const { dayAgo, weekAgo, monthAgo } = localData.changePercent; - let settings = await App.Settings.get(); - - let changePercent; - let periodLabel; - switch (settings.period) { - case this.PERIODS.ONE_DAY: - default: { - changePercent = dayAgo; - periodLabel = 'since yesterday'; - break; - } - case this.PERIODS.ONE_WEEK: { - changePercent = weekAgo; - periodLabel = 'since last week'; - break; - } - case this.PERIODS.ONE_MONTH: { - changePercent = monthAgo; - periodLabel = 'since last month'; - break; - } - case this.PERIODS.ONE_HOUR: - case this.PERIODS.ONE_YEAR: - case this.PERIODS.ALL: { - this.$change.innerHTML = ''; - return; - } - } - - const getSignedPercentage = (_number) => { - const isChangePisitive = _number >= 0; - const isChangeZero = _number === 0; - - return isChangePisitive && !isChangeZero ? `+${_number}%` : `${_number}%`; - }; - const getVisualClass = (_number) => { - const isChangePisitive = _number >= 0; - const isChangeZero = _number === 0; - - return isChangeZero ? '' : isChangePisitive ? 'positive' : 'negative'; - }; - - this.$change.innerHTML = ` (${getSignedPercentage(changePercent)} - ${periodLabel})`; - }, - - $lastUpdated: document.querySelector('#last-updated'), - setLastUpdated() { - this.repositories['NOW'].getDataUpToDateStatus().then((info) => { - const prettyLastUpdatedTime = dayjs(info.lastFetched).fromNow(); - this.$lastUpdated.innerHTML = - this.isLocalChartDataOld || this.isLocalNowDataOld - ? `${prettyLastUpdatedTime}. - Data request failed. Refresh the page to try again.` - : `${prettyLastUpdatedTime}.`; - - this.$lastUpdated.setAttribute('data-tooltip', dayjs(info.lastFetched).calendar()); - }); - }, - - displayPriceNow() { - this.repositories['NOW'] - .getData() - .then((_data) => { - this.setPriceChange(); - this.setLastUpdated(); - }) - .catch(() => { - this.handleNowRejection(); - this.setPriceChange(); - this.setLastUpdated(); - }); - - // Track timeframe changes - setInterval(this.setLastUpdated.bind(this), 30 * 1000); - }, - - init() { - this.chart = new App.Chart(this.$chart); - - this.initRepositories(); - this.displayPriceNow(); - - this.initEvents(); - }, -}; diff --git a/src/js/crypto.js b/src/js/crypto.js new file mode 100644 index 0000000..d5c957e --- /dev/null +++ b/src/js/crypto.js @@ -0,0 +1,310 @@ +dayjs.extend(window.dayjs_plugin_utc); +dayjs.extend(window.dayjs_plugin_localizedFormat); +dayjs.extend(window.dayjs_plugin_relativeTime); +dayjs.extend(window.dayjs_plugin_calendar); + +window.App = window.App || {}; + +window.App.Crypto = { + PERIODS: { + ONE_HOUR: 'ONE_HOUR', + ONE_DAY: 'ONE_DAY', + ONE_WEEK: 'ONE_WEEK', + ONE_MONTH: 'ONE_MONTH', + ONE_YEAR: 'ONE_YEAR', + ALL: 'ALL', + }, + isLocalChartDataOld: false, + isLocalNowDataOld: false, + + $chart: document.getElementById('chart'), + chart: null, + + $dataPeriods: document.querySelectorAll('.js-period'), + $cryptoToggle: document.querySelectorAll('input[name="crypto"]'), + $cryptoTypeLabel: document.getElementById('crypto-type'), + currentCrypto: '', + + initEvents() { + const self = this; + + [...self.$dataPeriods].forEach((el) => { + el.addEventListener('click', function () { + self.$dataPeriods.forEach((p) => p.classList.remove('active')); + this.classList.add('active'); + + const period = this.dataset.period; + self.getCryptoData(period, self.currentCrypto) + .then((_data) => self.chart.init(_data)) + .catch((error) => { + self.handleChartRejection(period, self.currentCrypto, error); + }); + + App.Settings.set('period', this.dataset.period); + + self.setPriceChange(self.currentCrypto); + }); + }); + + [...self.$cryptoToggle].forEach((el) => { + el.addEventListener('change', function () { + if (this.checked) { + self.currentCrypto = this.value; + self.updateCryptoTypeLabel(self.currentCrypto); + const period = document.querySelector('.js-period.active').dataset.period; + self.getCryptoData(period, self.currentCrypto) + .then((_data) => self.chart.init(_data)) + .catch((error) => { + self.handleChartRejection(period, self.currentCrypto, error); + }); + + App.Settings.set('cryptoType', self.currentCrypto); + + self.setPriceChange(self.currentCrypto); + self.setLastUpdated(); + self.displayPriceNow(); + } + }); + }); + + App.Settings.get().then(({ period, cryptoType }) => { + self.currentCrypto = + cryptoType || document.querySelector('input[name="crypto"]:checked').value; + const selectedTab = period ? Object.keys(this.PERIODS).indexOf(period) : 1; + document.querySelector(`input[name="crypto"][value="${self.currentCrypto}"]`).checked = + true; + self.initRepositories(); + self.$dataPeriods[selectedTab].click(); + }); + }, + + updateCryptoTypeLabel(cryptoType) { + const label = cryptoType.charAt(0).toUpperCase() + cryptoType.slice(1); + this.$cryptoTypeLabel.textContent = label; + }, + + getCryptoData(period, cryptoType) { + return new Promise((resolve, reject) => { + this.repositories[cryptoType][period] + .getData() + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(error || `Failed to retrieve ${cryptoType} price data`); + }); + }); + }, + + getLabelFormat(period) { + switch (period) { + case 'ALL': + return 'YYYY'; + case 'ONE_YEAR': + return 'MMM YYYY'; + case 'ONE_MONTH': + return 'D MMM'; + case 'ONE_WEEK': + return 'dddd'; + case 'ONE_DAY': + return 'HH:mm'; + case 'ONE_HOUR': + return 'HH:mm'; + } + }, + + handleChartRejection(_period, _cryptoType, _error) { + this.isLocalChartDataOld = true; + + this.repositories[_cryptoType][_period].getDataUpToDateStatus().then((_res) => { + App.Loader.destroy(); + + if (_res.localData === null) { + App.Message.fireError(`That's extremely sad. ${_error}`); + this.chart.destroy(); + } else if (_res.localData.length) { + App.Message.clear(); + this.chart.init(_res.localData); + this.setLastUpdated(); + } + }); + }, + handleNowRejection() { + this.isLocalNowDataOld = true; + App.Loader.destroy(); + }, + + repositories: {}, + initRepositories() { + const storageSetting = + App.ENV.platform === 'EXTENSION' ? 'BROWSER_STORAGE' : 'LOCAL_STORAGE'; + + ['bitcoin', 'ethereum'].forEach((cryptoType) => { + this.repositories[cryptoType] = {}; + Object.keys(this.PERIODS).forEach((period) => { + this.repositories[cryptoType][period] = new SuperRepo({ + storage: storageSetting, + name: `${cryptoType}-${period}`, + outOfDateAfter: 15 * 60 * 1000, // 15 minutes + mapData: (r) => App.API.mapData(r, this.getLabelFormat(period)), + request: () => + this.getCryptoDataFromBackground(period, cryptoType) + .then((res) => { + this.isLocalChartDataOld = false; + return res; + }) + .catch((jqXHR, textStatus, errorThrown) => { + this.handleChartRejection(period, cryptoType, jqXHR); + }), + }); + }); + + this.repositories[cryptoType]['NOW'] = new SuperRepo({ + storage: storageSetting, + name: `${cryptoType}-NOW`, + outOfDateAfter: 3 * 60 * 1000, // 3 minutes + mapData: (data) => { + const { value, changePercent } = data[0]; + const { dayAgo, weekAgo, monthAgo } = changePercent; + + return { + price: value, + changePercent: { dayAgo, weekAgo, monthAgo }, + }; + }, + request: () => + this.getCryptoDataFromBackground('NOW', cryptoType) + .then((res) => { + this.isLocalNowDataOld = false; + return res; + }) + .catch(() => { + this.handleNowRejection(); + }), + }); + }); + }, + + getCryptoDataFromBackground(period, cryptoType) { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage( + { type: 'getCryptoPrice', period: period, cryptoType: cryptoType }, + (response) => { + if (response && !response.error) { + resolve(response.data); + } else { + reject(response.error || `Failed to retrieve ${cryptoType} price data`); + } + } + ); + }); + }, + + $priceNow: document.querySelector('#price-now'), + setPriceNow(_price) { + this.$priceNow.textContent = App.Utils.formatPrice(Math.round(_price)); + }, + + $change: document.querySelector('#change'), + async setPriceChange(cryptoType) { + let { localData } = await this.repositories[cryptoType]['NOW'].getDataUpToDateStatus(); + if (!localData) { + return; + } + + this.setPriceNow(localData.price); + + const { dayAgo, weekAgo, monthAgo } = localData.changePercent; + let settings = await App.Settings.get(); + + let changePercent; + let periodLabel; + switch (settings.period) { + case this.PERIODS.ONE_DAY: + default: { + changePercent = dayAgo; + periodLabel = 'since yesterday'; + break; + } + case this.PERIODS.ONE_WEEK: { + changePercent = weekAgo; + periodLabel = 'since last week'; + break; + } + case this.PERIODS.ONE_MONTH: { + changePercent = monthAgo; + periodLabel = 'since last month'; + break; + } + case this.PERIODS.ONE_HOUR: + case this.PERIODS.ONE_YEAR: + case this.PERIODS.ALL: { + this.$change.innerHTML = ''; + return; + } + } + + const getSignedPercentage = (_number) => { + const isChangePositive = _number >= 0; + const isChangeZero = _number === 0; + + return isChangePositive && !isChangeZero ? `+${_number}%` : `${_number}%`; + }; + const getVisualClass = (_number) => { + const isChangePositive = _number >= 0; + const isChangeZero = _number === 0; + + return isChangeZero ? '' : isChangePositive ? 'positive' : 'negative'; + }; + + this.$change.innerHTML = ` (${getSignedPercentage(changePercent)} + ${periodLabel})`; + }, + + $lastUpdated: document.querySelector('#last-updated'), + setLastUpdated() { + const cryptoType = this.currentCrypto; + this.repositories[cryptoType]['NOW'].getDataUpToDateStatus().then((info) => { + const prettyLastUpdatedTime = dayjs(info.lastFetched).fromNow(); + this.$lastUpdated.innerHTML = + this.isLocalChartDataOld || this.isLocalNowDataOld + ? `${prettyLastUpdatedTime}. + Data request failed. Refresh the page to try again.` + : `${prettyLastUpdatedTime}.`; + + this.$lastUpdated.setAttribute('data-tooltip', dayjs(info.lastFetched).calendar()); + }); + }, + + displayPriceNow() { + const cryptoType = this.currentCrypto; + this.repositories[cryptoType]['NOW'] + .getData() + .then((_data) => { + this.setPriceChange(cryptoType); + this.setLastUpdated(); + }) + .catch(() => { + this.handleNowRejection(); + this.setPriceChange(cryptoType); + this.setLastUpdated(); + }); + + // Track timeframe changes + setInterval(this.setLastUpdated.bind(this), 30 * 1000); + }, + + init() { + this.chart = new App.Chart(this.$chart); + + this.initRepositories(); + this.initEvents(); + + App.Settings.get().then(({ cryptoType }) => { + this.currentCrypto = + cryptoType || document.querySelector('input[name="crypto"]:checked').value; + this.updateCryptoTypeLabel(this.currentCrypto); + this.displayPriceNow(); + }); + }, +}; diff --git a/src/js/script.js b/src/js/script.js index 1dc6cb5..7e37ac4 100644 --- a/src/js/script.js +++ b/src/js/script.js @@ -1,10 +1,10 @@ -window.App.Bitcoin.init(); +window.App.Crypto.init(); window.onload = () => { const { platform } = App.ENV; // Display platform specific DOM elements - [...document.querySelectorAll(`[data-platform="${platform}"]`)].forEach( el => { + [...document.querySelectorAll(`[data-platform="${platform}"]`)].forEach((el) => { el.classList.remove('hidden'); }); diff --git a/src/manifest.json b/src/manifest.json index 83b481a..871a611 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -15,5 +15,8 @@ "host_permissions": [ "https://bitcoin-price-api.devlabs-projects.info/**/*", "https://api.crypto-tab.com/**/*" - ] + ], + "background": { + "service_worker": "js/background.js" + } }