Spaces:
Running
Running
Jimin Huang
commited on
Commit
·
84b00d4
1
Parent(s):
787af51
Change settings
Browse files- src/components/AssetTabs.vue +40 -38
- src/lib/prices.ts +65 -0
src/components/AssetTabs.vue
CHANGED
|
@@ -1,60 +1,62 @@
|
|
|
|
|
| 1 |
<template>
|
| 2 |
-
<div class="
|
| 3 |
-
<button
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
<
|
| 11 |
</button>
|
| 12 |
</div>
|
| 13 |
</template>
|
| 14 |
|
| 15 |
-
<script setup>
|
| 16 |
-
import { computed } from 'vue'
|
| 17 |
import { dataService } from '../lib/dataService'
|
|
|
|
| 18 |
|
| 19 |
-
const props = defineProps({
|
| 20 |
-
modelValue: { type: String, default: '' },
|
| 21 |
-
// keep whatever order you want to prefer (others will append)
|
| 22 |
-
preferredOrder: {
|
| 23 |
-
type: Array,
|
| 24 |
-
default: () => ['BTC','ETH','SOL','BNB','DOGE','XRP','AAPL','MSFT','BMRN','MRNA','TSLA']
|
| 25 |
-
}
|
| 26 |
-
})
|
| 27 |
const emit = defineEmits(['update:modelValue'])
|
| 28 |
|
| 29 |
-
|
| 30 |
-
const
|
| 31 |
-
new URL(`../assets/images/assets_images/${assetCode}.png`, import.meta.url).href
|
| 32 |
-
const hide = (e) => { e.target.style.display = 'none' }
|
| 33 |
|
| 34 |
-
// Only assets that exist in current data
|
| 35 |
const available = computed(() => {
|
| 36 |
-
const rows = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
|
| 37 |
-
return Array.from(new Set(rows.map(r
|
| 38 |
})
|
| 39 |
-
|
| 40 |
-
// Deterministic ordering (without touching AssetsFilter.vue)
|
| 41 |
const orderedAssets = computed(() => {
|
| 42 |
const present = new Set(available.value)
|
| 43 |
-
const primary =
|
| 44 |
-
const extras = [...present].filter(a => !
|
| 45 |
const list = [...primary, ...extras]
|
| 46 |
-
if (!list.includes(props.modelValue
|
| 47 |
return list
|
| 48 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
</script>
|
| 50 |
|
| 51 |
<style scoped>
|
| 52 |
-
.
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
object-fit: contain;
|
| 57 |
-
display: inline-block;
|
| 58 |
-
vertical-align: middle;
|
| 59 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
</style>
|
|
|
|
| 1 |
+
<!-- src/components/AssetTabsPrice.vue -->
|
| 2 |
<template>
|
| 3 |
+
<div class="tabs">
|
| 4 |
+
<button v-for="code in orderedAssets" :key="code"
|
| 5 |
+
class="tab" :class="code===modelValue ? 'tab--active' : ''"
|
| 6 |
+
@click="$emit('update:modelValue', code)">
|
| 7 |
+
<div class="row">
|
| 8 |
+
<img :src="iconFor(code)" class="icon" @error="hide($event)" alt="" />
|
| 9 |
+
<span class="code">{{ code }}</span>
|
| 10 |
+
</div>
|
| 11 |
+
<div class="price">{{ fmtUSD(prices[code]) }}</div>
|
| 12 |
</button>
|
| 13 |
</div>
|
| 14 |
</template>
|
| 15 |
|
| 16 |
+
<script setup lang="ts">
|
| 17 |
+
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
| 18 |
import { dataService } from '../lib/dataService'
|
| 19 |
+
import { pollPrices, subscribePrices, fmtUSD } from '../lib/prices'
|
| 20 |
|
| 21 |
+
const props = defineProps({ modelValue: String })
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
const emit = defineEmits(['update:modelValue'])
|
| 23 |
|
| 24 |
+
const iconFor = (c:string) => new URL(`../assets/images/assets_images/${c}.png`, import.meta.url).href
|
| 25 |
+
const hide = (e:Event)=>((e.target as HTMLImageElement).style.display='none')
|
|
|
|
|
|
|
| 26 |
|
|
|
|
| 27 |
const available = computed(() => {
|
| 28 |
+
const rows = Array.isArray((dataService as any).tableRows) ? (dataService as any).tableRows : []
|
| 29 |
+
return Array.from(new Set(rows.map((r:any)=>r.asset)))
|
| 30 |
})
|
| 31 |
+
const preferred = ['BTC','ETH','SOL','BNB','DOGE','XRP','AAPL','MSFT','BMRN','MRNA','TSLA']
|
|
|
|
| 32 |
const orderedAssets = computed(() => {
|
| 33 |
const present = new Set(available.value)
|
| 34 |
+
const primary = preferred.filter(a => present.has(a))
|
| 35 |
+
const extras = [...present].filter(a => !preferred.includes(a)).sort()
|
| 36 |
const list = [...primary, ...extras]
|
| 37 |
+
if (list.length && !list.includes(props.modelValue || '')) emit('update:modelValue', list[0])
|
| 38 |
return list
|
| 39 |
})
|
| 40 |
+
|
| 41 |
+
const prices = ref<Record<string, number>>({})
|
| 42 |
+
let unsub: null | (()=>void) = null
|
| 43 |
+
onMounted(async () => {
|
| 44 |
+
unsub = subscribePrices(m => (prices.value = m))
|
| 45 |
+
await pollPrices(orderedAssets.value) // starts polling; cheap even if called again
|
| 46 |
+
})
|
| 47 |
+
onBeforeUnmount(()=>unsub?.())
|
| 48 |
+
watch(orderedAssets, (arr)=>pollPrices(arr))
|
| 49 |
</script>
|
| 50 |
|
| 51 |
<style scoped>
|
| 52 |
+
.tabs{ display:flex; flex-wrap:wrap; gap:8px }
|
| 53 |
+
.tab{
|
| 54 |
+
background:#fff; border:1px solid #1f2937; border-radius:9999px;
|
| 55 |
+
padding:8px 12px; min-width:110px; display:flex; flex-direction:column; align-items:center; gap:2px
|
|
|
|
|
|
|
|
|
|
| 56 |
}
|
| 57 |
+
.tab--active{ background:#111827; color:#fff; border-color:#111827 }
|
| 58 |
+
.row{ display:flex; align-items:center; gap:6px; line-height:1 }
|
| 59 |
+
.icon{ width:18px; height:18px; flex:0 0 18px; object-fit:contain }
|
| 60 |
+
.code{ font-weight:700; letter-spacing:.02em }
|
| 61 |
+
.price{ font-variant-numeric:tabular-nums; font-weight:800; line-height:1.05; margin-top:2px }
|
| 62 |
</style>
|
src/lib/prices.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// src/lib/prices.ts
|
| 2 |
+
type PriceMap = Record<string, number>
|
| 3 |
+
|
| 4 |
+
// Map your asset code -> provider + symbol
|
| 5 |
+
const MAP: Record<string, { p: 'binance'|'stooq'; s: string }> = {
|
| 6 |
+
// crypto (USDT pairs)
|
| 7 |
+
BTC: { p: 'binance', s: 'BTCUSDT' },
|
| 8 |
+
ETH: { p: 'binance', s: 'ETHUSDT' },
|
| 9 |
+
SOL: { p: 'binance', s: 'SOLUSDT' },
|
| 10 |
+
BNB: { p: 'binance', s: 'BNBUSDT' },
|
| 11 |
+
DOGE:{ p: 'binance', s: 'DOGEUSDT' },
|
| 12 |
+
XRP: { p: 'binance', s: 'XRPUSDT' },
|
| 13 |
+
|
| 14 |
+
// equities (stooq wants .us)
|
| 15 |
+
AAPL:{ p: 'stooq', s: 'aapl.us' },
|
| 16 |
+
MSFT:{ p: 'stooq', s: 'msft.us' },
|
| 17 |
+
BMRN:{ p: 'stooq', s: 'bmrn.us' },
|
| 18 |
+
MRNA:{ p: 'stooq', s: 'mrna.us' },
|
| 19 |
+
TSLA:{ p: 'stooq', s: 'tsla.us' },
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
// --- simple cache + pub/sub
|
| 23 |
+
let latest: PriceMap = {};
|
| 24 |
+
const subs = new Set<(m: PriceMap)=>void>();
|
| 25 |
+
const push = () => subs.forEach(fn => fn({ ...latest }));
|
| 26 |
+
|
| 27 |
+
export function subscribePrices(fn: (m: PriceMap)=>void) {
|
| 28 |
+
subs.add(fn); fn({ ...latest });
|
| 29 |
+
return () => subs.delete(fn);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
async function fetchBinance(symbol: string) {
|
| 33 |
+
const r = await fetch(`https://api.binance.com/api/v3/ticker/price?symbol=${encodeURIComponent(symbol)}`);
|
| 34 |
+
const j = await r.json(); const n = Number(j?.price);
|
| 35 |
+
return Number.isFinite(n) ? n : undefined;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
async function fetchStooq(symbol: string) {
|
| 39 |
+
const url = `https://stooq.com/q/l/?s=${encodeURIComponent(symbol)}&f=sd2t2ohlcv&h&e=csv`;
|
| 40 |
+
const r = await fetch(url);
|
| 41 |
+
const txt = await r.text(); // first line is header; second line has data, column 'Close'
|
| 42 |
+
const line = txt.split('\n')[1] || '';
|
| 43 |
+
const cols = line.split(','); // Symbol,Date,Time,Open,High,Low,Close,Volume
|
| 44 |
+
const n = Number(cols[6]);
|
| 45 |
+
return Number.isFinite(n) ? n : undefined;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
export async function pollPrices(codes: string[], intervalMs = 30000) {
|
| 49 |
+
const uniq = [...new Set(codes)].filter(c => MAP[c]);
|
| 50 |
+
async function tick() {
|
| 51 |
+
await Promise.all(uniq.map(async code => {
|
| 52 |
+
const { p, s } = MAP[code];
|
| 53 |
+
const px = p === 'binance' ? await fetchBinance(s) : await fetchStooq(s);
|
| 54 |
+
if (px !== undefined) latest[code] = px;
|
| 55 |
+
}));
|
| 56 |
+
push();
|
| 57 |
+
}
|
| 58 |
+
await tick();
|
| 59 |
+
setInterval(tick, intervalMs);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
export const fmtUSD = (n?: number) =>
|
| 63 |
+
typeof n === 'number'
|
| 64 |
+
? n.toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
|
| 65 |
+
: '—';
|