diff --git a/public/brand-assets/telos-zero/telos-zero-dark-gradient-o.png b/public/brand-assets/telos-zero/telos-zero-dark-gradient-o.png new file mode 100644 index 00000000..eadb1863 Binary files /dev/null and b/public/brand-assets/telos-zero/telos-zero-dark-gradient-o.png differ diff --git a/public/brand-assets/telos-zero/telos-zero-dark-gradient.png b/public/brand-assets/telos-zero/telos-zero-dark-gradient.png new file mode 100644 index 00000000..c7e416ca Binary files /dev/null and b/public/brand-assets/telos-zero/telos-zero-dark-gradient.png differ diff --git a/public/brand-assets/telos-zero/telos-zero-light-gradient.png b/public/brand-assets/telos-zero/telos-zero-light-gradient.png new file mode 100644 index 00000000..1d03205f Binary files /dev/null and b/public/brand-assets/telos-zero/telos-zero-light-gradient.png differ diff --git a/public/brand-assets/telos-zero/telos-zero-light.png b/public/brand-assets/telos-zero/telos-zero-light.png new file mode 100644 index 00000000..83b387d2 Binary files /dev/null and b/public/brand-assets/telos-zero/telos-zero-light.png differ diff --git a/public/brand-assets/telos/telos-dark-gradient.svg b/public/brand-assets/telos/telos-dark-gradient.svg new file mode 100644 index 00000000..009f71a7 --- /dev/null +++ b/public/brand-assets/telos/telos-dark-gradient.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/brand-assets/telos/telos-icon-256.png b/public/brand-assets/telos/telos-icon-256.png new file mode 100644 index 00000000..c9021a28 Binary files /dev/null and b/public/brand-assets/telos/telos-icon-256.png differ diff --git a/public/brand-assets/telos/telos-icon.svg b/public/brand-assets/telos/telos-icon.svg new file mode 100644 index 00000000..0a37cea2 --- /dev/null +++ b/public/brand-assets/telos/telos-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/brand-assets/telos/telos-light-gradient.svg b/public/brand-assets/telos/telos-light-gradient.svg new file mode 100644 index 00000000..aced79b5 --- /dev/null +++ b/public/brand-assets/telos/telos-light-gradient.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/brand-assets/telos/telos-light.svg b/public/brand-assets/telos/telos-light.svg new file mode 100644 index 00000000..35f06b3c --- /dev/null +++ b/public/brand-assets/telos/telos-light.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/chains/telos-testnet/favicon.png b/public/chains/telos-testnet/favicon.png index 2be9c2c7..c9021a28 100644 Binary files a/public/chains/telos-testnet/favicon.png and b/public/chains/telos-testnet/favicon.png differ diff --git a/public/chains/telos/favicon.png b/public/chains/telos/favicon.png index 2be9c2c7..c9021a28 100644 Binary files a/public/chains/telos/favicon.png and b/public/chains/telos/favicon.png differ diff --git a/public/fonts/silka-bold-webfont.woff2 b/public/fonts/silka-bold-webfont.woff2 new file mode 100644 index 00000000..471efcd7 Binary files /dev/null and b/public/fonts/silka-bold-webfont.woff2 differ diff --git a/public/fonts/silka-light-webfont.ttf b/public/fonts/silka-light-webfont.ttf new file mode 100644 index 00000000..facaa55a Binary files /dev/null and b/public/fonts/silka-light-webfont.ttf differ diff --git a/public/fonts/silka-medium-webfont.woff2 b/public/fonts/silka-medium-webfont.woff2 new file mode 100644 index 00000000..c1501348 Binary files /dev/null and b/public/fonts/silka-medium-webfont.woff2 differ diff --git a/public/fonts/silka-regular-webfont.woff2 b/public/fonts/silka-regular-webfont.woff2 new file mode 100644 index 00000000..6fa50eb4 Binary files /dev/null and b/public/fonts/silka-regular-webfont.woff2 differ diff --git a/public/fonts/silka-semibold-webfont.ttf b/public/fonts/silka-semibold-webfont.ttf new file mode 100644 index 00000000..bd8169ec Binary files /dev/null and b/public/fonts/silka-semibold-webfont.ttf differ diff --git a/public/icons/android-chrome-192x192.png b/public/icons/android-chrome-192x192.png index c3e56c4f..aa325e64 100644 Binary files a/public/icons/android-chrome-192x192.png and b/public/icons/android-chrome-192x192.png differ diff --git a/public/icons/android-chrome-512x512.png b/public/icons/android-chrome-512x512.png index 57d67062..11671ab8 100644 Binary files a/public/icons/android-chrome-512x512.png and b/public/icons/android-chrome-512x512.png differ diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png index 9fe69f6f..9b1ae548 100644 Binary files a/public/icons/apple-touch-icon.png and b/public/icons/apple-touch-icon.png differ diff --git a/public/icons/favicon-16x16.png b/public/icons/favicon-16x16.png index 2ba6897c..9551e584 100644 Binary files a/public/icons/favicon-16x16.png and b/public/icons/favicon-16x16.png differ diff --git a/public/icons/favicon-32x32.png b/public/icons/favicon-32x32.png index 210d27a7..f852a81a 100644 Binary files a/public/icons/favicon-32x32.png and b/public/icons/favicon-32x32.png differ diff --git a/public/icons/site.webmanifest b/public/icons/site.webmanifest index 45dc8a20..2b948973 100644 --- a/public/icons/site.webmanifest +++ b/public/icons/site.webmanifest @@ -1 +1 @@ -{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file +{"name":"Telos Block Explorer","short_name":"Telos Explorer","icons":[{"src":"/icons/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/icons/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#2C2B2F","background_color":"#FFFFFF","display":"standalone"} diff --git a/quasar.conf.js b/quasar.conf.js index a22292dd..e6525cf7 100644 --- a/quasar.conf.js +++ b/quasar.conf.js @@ -164,36 +164,21 @@ module.exports = configure(function (ctx) { }, manifest: { - name: '\'OBE\'', - short_name: '\'OBE\'', - description: '..', + name: 'Telos Block Explorer', + short_name: 'Telos Explorer', + description: 'Open block explorer for Telos and AntelopeIO networks.', display: 'standalone', orientation: 'portrait', - background_color: '#ffffff', - theme_color: '#027be3', + background_color: '#FFFFFF', + theme_color: '#2C2B2F', icons: [ { - src: 'icons/icon-128x128.png', - sizes: '128x128', - type: 'image/png' - }, - { - src: 'icons/icon-192x192.png', + src: 'icons/android-chrome-192x192.png', sizes: '192x192', type: 'image/png' }, { - src: 'icons/icon-256x256.png', - sizes: '256x256', - type: 'image/png' - }, - { - src: 'icons/icon-384x384.png', - sizes: '384x384', - type: 'image/png' - }, - { - src: 'icons/icon-512x512.png', + src: 'icons/android-chrome-512x512.png', sizes: '512x512', type: 'image/png' } diff --git a/src/api/price.ts b/src/api/price.ts index 997929fd..e452f0b8 100644 --- a/src/api/price.ts +++ b/src/api/price.ts @@ -1,37 +1,78 @@ import axios from 'axios'; -import { PriceStats, PriceHistory } from 'src/types'; +import { DateTuple, PriceStats, PriceHistory } from 'src/types'; import { PriceChartData } from 'src/types/PriceChartData'; +const HOURS_23_MS = 23 * 60 * 60 * 1000; +const DAY_MS = 24 * 60 * 60 * 1000; + +interface CoinPaprikaTicker { + last_updated: string; + quotes: { + USD: { + price: number; + volume_24h: number; + market_cap: number; + percent_change_24h: number; + }; + }; +} + +interface CoinPaprikaHistoryPoint { + timestamp: string; + price: number; +} + +interface TelosMarketData { + collected: number; + price: number; + market_cap: string; + volume: string; +} + export const getCoingeckoUsdPrice = async ( tokenId: string, ): Promise => { - const stats: PriceStats = await axios.get( - getCoingeckoExchangeStatsUrl(tokenId), - ); + try { + const stats: PriceStats = await axios.get( + getCoingeckoExchangeStatsUrl(tokenId), + ); - return stats.data[tokenId].usd; + return stats.data[tokenId].usd; + } catch (error) { + if (tokenId === 'telos') { + return getTelosFallbackUsdPrice(); + } + throw error; + } }; export const getCoingeckoPriceChartData = async ( tokenId: string, ): Promise => { - const exchangeStatsUrl = getCoingeckoExchangeStatsUrl(tokenId); - const priceHistoryUrl = `https://api.coingecko.com/api/v3/coins/${tokenId}/market_chart?vs_currency=USD&days=1`; // &interval=hourly` - restore if when enterprise plan enabled; + try { + const exchangeStatsUrl = getCoingeckoExchangeStatsUrl(tokenId); + const priceHistoryUrl = `https://api.coingecko.com/api/v3/coins/${tokenId}/market_chart?vs_currency=USD&days=1`; // &interval=hourly` - restore if when enterprise plan enabled; - const [priceStats, priceHistory]: [PriceStats, PriceHistory] = - await Promise.all([ - axios.get(exchangeStatsUrl), - axios.get(priceHistoryUrl), - ]); + const [priceStats, priceHistory]: [PriceStats, PriceHistory] = + await Promise.all([ + axios.get(exchangeStatsUrl), + axios.get(priceHistoryUrl), + ]); - return { - lastUpdated: priceStats.data[tokenId].last_updated_at, - tokenPrice: priceStats.data[tokenId].usd, - dayChange: priceStats.data[tokenId].usd_24h_change, - dayVolume: priceStats.data[tokenId].usd_24h_vol, - marketCap: priceStats.data[tokenId].usd_market_cap, - prices: priceHistory.data.prices, - }; + return { + lastUpdated: priceStats.data[tokenId].last_updated_at, + tokenPrice: priceStats.data[tokenId].usd, + dayChange: priceStats.data[tokenId].usd_24h_change, + dayVolume: priceStats.data[tokenId].usd_24h_vol, + marketCap: priceStats.data[tokenId].usd_market_cap, + prices: normalizePriceSeries(priceHistory.data.prices, priceStats.data[tokenId].usd), + }; + } catch (error) { + if (tokenId === 'telos') { + return getCoinPaprikaTelosPriceChartData(); + } + throw error; + } }; // eslint-disable-next-line @typescript-eslint/require-await @@ -45,3 +86,77 @@ export const getEmptyPriceChartData = async (): Promise => ({ }); const getCoingeckoExchangeStatsUrl = (tokenId: string): string => `https://api.coingecko.com/api/v3/simple/price?ids=${tokenId}&vs_currencies=USD&include_market_cap=true&include_24hr_vol=true&include_24hr_change=true&include_last_updated_at=true`; + +const getCoinPaprikaTelosPriceChartData = async (): Promise => { + try { + const start = new Date(Date.now() - HOURS_23_MS).toISOString(); + const [ticker, history] = await Promise.all([ + axios.get('https://api.coinpaprika.com/v1/tickers/tlos-telos'), + axios.get( + `https://api.coinpaprika.com/v1/tickers/tlos-telos/historical?start=${encodeURIComponent(start)}&interval=1h`, + ), + ]); + const usd = ticker.data.quotes.USD; + const prices = history.data + .map(point => [Date.parse(point.timestamp), point.price] as DateTuple) + .filter(([timestamp, price]) => Number.isFinite(Number(timestamp)) && Number.isFinite(price)); + + return { + lastUpdated: Date.parse(ticker.data.last_updated) / 1000, + tokenPrice: usd.price, + dayChange: usd.percent_change_24h, + dayVolume: usd.volume_24h, + marketCap: usd.market_cap, + prices: normalizePriceSeries(prices, usd.price), + }; + } catch (error) { + return getTelosMarketDataFallback(); + } +}; + +const getTelosMarketDataFallback = async (): Promise => { + const marketData = await getTelosMarketData(); + + return { + lastUpdated: marketData.collected / 1000, + tokenPrice: marketData.price, + dayChange: 0, + dayVolume: Number(marketData.volume), + marketCap: Number(marketData.market_cap), + prices: normalizePriceSeries([], marketData.price), + }; +}; + +const getTelosFallbackUsdPrice = async (): Promise => { + try { + const marketData = await getTelosMarketData(); + return marketData.price; + } catch (error) { + const ticker = await axios.get('https://api.coinpaprika.com/v1/tickers/tlos-telos'); + return ticker.data.quotes.USD.price; + } +}; + +const getTelosMarketData = async (): Promise => { + const response = await axios.get( + 'https://api.telos.net/v1/evm/tokens/marketdata?symbols=TLOS', + ); + + return response.data[0]; +}; + +const normalizePriceSeries = (prices: DateTuple[], currentPrice: number): DateTuple[] => { + if (prices.length >= 2) { + return prices; + } + + if (!Number.isFinite(currentPrice) || currentPrice <= 0) { + return []; + } + + const now = Date.now(); + return [ + [now - DAY_MS, currentPrice], + [now, currentPrice], + ]; +}; diff --git a/src/assets/obe_worldmap_telos.svg b/src/assets/obe_worldmap_telos.svg index 464aaa9c..17754fe5 100644 --- a/src/assets/obe_worldmap_telos.svg +++ b/src/assets/obe_worldmap_telos.svg @@ -6,24 +6,24 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/src/components/AccountCard.vue b/src/components/AccountCard.vue index d5bb35dd..d1e10ab4 100644 --- a/src/components/AccountCard.vue +++ b/src/components/AccountCard.vue @@ -549,7 +549,7 @@ export default defineComponent({ :disable="tokensLoading || isLoading" :label="tokensLoading ? 'Loading...' : 'Send'" color="primary" - class="full-width" + class="full-width account-action-button" @click="openSendDialog = true" /> @@ -557,7 +557,7 @@ export default defineComponent({ @@ -566,7 +566,7 @@ export default defineComponent({ @@ -718,6 +718,21 @@ $medium:750px .resource margin-right: 2rem +.account-action-button + background-color: rgba(255, 255, 255, 0.1) !important + background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.14) 0%, rgba(255, 255, 255, 0.08) 100%) !important + border: 1px solid rgba(0, 242, 254, 0.38) + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.2), inset 0 0 0 1px rgba(255, 255, 255, 0.04) + color: #FFFFFF !important + font-weight: 600 + min-height: 44px + &::before + border-color: transparent !important + &:hover, &:focus + background-color: rgba(0, 242, 254, 0.12) !important + border-color: rgba(196, 113, 245, 0.5) + box-shadow: 0 10px 22px rgba(0, 0, 0, 0.22), 0 0 18px rgba(0, 242, 254, 0.1) + .text-right font-weight: bold &.total-loading-spinner @@ -737,6 +752,10 @@ $medium:750px a cursor: pointer text-decoration: underline + color: var(--q-info, #00F2FE) + text-underline-offset: 2px + &:hover, &:focus + color: var(--q-accent, #C471F5) .total-amount color: white @@ -776,10 +795,16 @@ $medium:750px a cursor: pointer text-decoration: underline - color: white + color: var(--q-info, #00F2FE) font-size: 16px font-weight: normal + text-underline-offset: 2px + &:hover, &:focus + color: var(--q-accent, #C471F5) .tx-link text-decoration: none !important + color: var(--q-info, #00F2FE) + &:hover, &:focus + color: var(--q-accent, #C471F5) diff --git a/src/components/CoinSelectorDialog.vue b/src/components/CoinSelectorDialog.vue index 5fe1cbc9..7cf31b2c 100644 --- a/src/components/CoinSelectorDialog.vue +++ b/src/components/CoinSelectorDialog.vue @@ -116,14 +116,14 @@ export default defineComponent({ cursor: pointer .addBtn - color: white - background-image: linear-gradient(to right, $deep-purple-2 20%, $light-blue-6 80% ) + color: var(--q-dark) + background: var(--q-color-primary-gradient) &:hover - background-image: linear-gradient(to left, $deep-purple-2 20%, $light-blue-6 80%) + box-shadow: 0 8px 20px rgba(79, 172, 254, 0.22) .dialogCard - background: radial-gradient(circle at 48% 100%, rgba(108, 35, 255, 1) 0%, rgba(84, 0, 253, 1) 20%, rgba(2, 27, 100, 1) 92%) + background: var(--q-color-secondary-gradient) color: white // Hide scrollbar for Chrome, Safari and Opera diff --git a/src/components/Footer.vue b/src/components/Footer.vue index 224523c2..31714c2f 100644 --- a/src/components/Footer.vue +++ b/src/components/Footer.vue @@ -31,7 +31,11 @@ export default { .footer-background background: var(--q-color-footer-background) backdrop-filter: blur(14px) + border-top: 1px solid var(--q-color-header-border) .no-dec text-decoration: none color: white + font-size: 12px + font-weight: 600 + letter-spacing: 0 diff --git a/src/components/Header.vue b/src/components/Header.vue index 703a55dc..b58fd1df 100644 --- a/src/components/Header.vue +++ b/src/components/Header.vue @@ -22,7 +22,7 @@ export default defineComponent({ const headerSettings = computed((): HeaderSettings => ConfigManager.get().getCurrentChain().getUiCustomization().headerSettings); const account = computed(() => accountStore.accountName); - const isLarge = computed((): boolean => $q.screen.gt.md); + const isLarge = computed((): boolean => $q.screen.gt.xs); const showMultichainSelector = computed(() => process.env.SHOW_MULTICHAIN_SELECTOR === 'true'); const isTestnet = ref(getChain().isTestnet()); @@ -53,7 +53,7 @@ export default defineComponent({ diff --git a/src/components/PermissionCard.vue b/src/components/PermissionCard.vue index b3540be8..9881eba6 100644 --- a/src/components/PermissionCard.vue +++ b/src/components/PermissionCard.vue @@ -111,9 +111,9 @@ export default defineComponent({ margin-left: 50px .permission-card - background: #8A65D41A + background: rgba(79, 172, 254, 0.12) .permission-name-section - background: #8A65D41A + background: rgba(79, 172, 254, 0.12) min-width: 8rem flex-shrink: 0 @@ -124,7 +124,7 @@ export default defineComponent({ .permission-action-section min-width: 10rem - background: #945eff63 + background: rgba(196, 113, 245, 0.32) .owner-permission .permission-card diff --git a/src/components/PriceChart.vue b/src/components/PriceChart.vue index dbfdfa66..eec931e1 100644 --- a/src/components/PriceChart.vue +++ b/src/components/PriceChart.vue @@ -6,7 +6,6 @@ import exportingInit from 'highcharts/modules/exporting'; import { DateTuple } from 'src/types'; import { getChain } from 'src/config/ConfigManager'; import { PriceChartData } from 'src/types/PriceChartData'; -import { getCssVar } from 'quasar'; const chain = getChain(); @@ -25,9 +24,17 @@ export default defineComponent({ // uncomment to fill area chart: { type: 'area', + backgroundColor: 'transparent', + spacing: [16, 18, 10, 10], }, title: { text: 'Past 24h', + style: { + color: '#2C2B2F', + fontFamily: 'Silka, Arial, sans-serif', + fontSize: '22px', + fontWeight: '600', + }, }, xAxis: { dateTimeLabelFormats: { @@ -35,14 +42,44 @@ export default defineComponent({ millisecond: '%A, %b %e, %l %p', }, type: 'datetime', + gridLineColor: 'rgba(79, 172, 254, 0.12)', + lineColor: 'rgba(44, 43, 47, 0.28)', + tickColor: 'rgba(44, 43, 47, 0.28)', + labels: { + style: { + color: '#404142', + fontFamily: 'Silka, Arial, sans-serif', + }, + }, }, yAxis: { title: { text: 'Price', + style: { + color: '#404142', + fontFamily: 'Silka, Arial, sans-serif', + }, + }, + gridLineColor: 'rgba(79, 172, 254, 0.18)', + labels: { + style: { + color: '#404142', + fontFamily: 'Silka, Arial, sans-serif', + }, }, }, legend: { enabled: true, + itemStyle: { + color: '#2C2B2F', + fontFamily: 'Silka, Arial, sans-serif', + fontWeight: '600', + }, + }, + credits: { + style: { + color: '#404142', + }, }, plotOptions: { area: { @@ -55,14 +92,17 @@ export default defineComponent({ y2: 1, }, stops: [ - // [0, '#571AFF'], - [0, getCssVar('color-graph-shadow')], + // [0, getCssVar('primary')], + [0, 'rgba(79, 172, 254, 0.24)'], + [1, 'rgba(196, 113, 245, 0.04)'], ], }, marker: { - radius: 0.5, + radius: 2, + lineColor: '#FFFFFF', + lineWidth: 1, }, - lineWidth: 2, + lineWidth: 2.5, states: { hover: { lineWidth: 4, @@ -74,7 +114,7 @@ export default defineComponent({ series: [ { name: chain.getSystemToken().symbol, - color: getCssVar('primary'), + color: '#2C2B2F', data: [] as DateTuple[], }, ], @@ -90,15 +130,34 @@ export default defineComponent({ const rank = ref(''); const dayVolume = ref(''); const dayChange = ref(''); + const priceDataError = ref(false); const fetchPriceChartData = async () => { - const data: PriceChartData = await chain.getPriceData(); - lastUpdated.value = data.lastUpdated; - tokenPrice.value = formatCurrencyValue(data.tokenPrice); - dayChange.value = formatPercentage(data.dayChange); - dayVolume.value = formatCurrencyValue(data.dayVolume); - marketCap.value = formatCurrencyValue(data.marketCap); - chartOptions.value.series[0].data = data.prices; + try { + const data: PriceChartData = await chain.getPriceData(); + lastUpdated.value = data.lastUpdated; + tokenPrice.value = formatCurrencyValue(data.tokenPrice); + dayChange.value = formatPercentage(data.dayChange); + dayVolume.value = formatCurrencyValue(data.dayVolume); + marketCap.value = formatCurrencyValue(data.marketCap); + priceDataError.value = data.prices.length === 0; + chartOptions.value = { + ...chartOptions.value, + series: [ + { + ...chartOptions.value.series[0], + data: data.prices, + }, + ], + }; + } catch (error) { + console.error(error); + priceDataError.value = true; + tokenPrice.value = '--'; + dayChange.value = '--'; + dayVolume.value = '--'; + marketCap.value = '--'; + } }; const formatPercentage = (val: number): string => `${val.toFixed(2)} %`; const formatCurrencyValue = (val: number): string => val < 1 ? `$${val.toFixed(3)}` : val < ONE_MILLION @@ -119,6 +178,7 @@ export default defineComponent({ rank, dayVolume, dayChange, + priceDataError, fetchPriceChartData, formatPercentage, formatCurrencyValue, @@ -130,14 +190,15 @@ export default defineComponent({