Jimin Huang commited on
Commit
9d1fe98
·
1 Parent(s): 5cec81c

Change settings

Browse files
Files changed (2) hide show
  1. src/components/AssetTabs.vue +32 -41
  2. src/lib/prices.ts +0 -87
src/components/AssetTabs.vue CHANGED
@@ -1,62 +1,53 @@
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>
 
 
1
  <template>
2
+ <div class="flex gap-2 flex-wrap">
3
+ <button
4
+ v-for="code in orderedAssets" :key="code"
5
+ class="px-3 py-1 rounded-full border flex items-center gap-2"
6
+ :class="code===modelValue ? 'bg-black text-white' : 'bg-white'"
7
+ @click="$emit('update:modelValue', code)"
8
+ >
9
+ <img :src="iconFor(code)" alt="" class="asset-icon" decoding="async" loading="lazy" @error="hide($event)" />
10
+ <span>{{ code }}</span>
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
+ preferredOrder: {
22
+ type: Array,
23
+ default: () => ['BTC','ETH','SOL','BNB','DOGE','XRP','AAPL','MSFT','BMRN','MRNA','TSLA']
24
+ }
25
+ })
26
  const emit = defineEmits(['update:modelValue'])
27
 
28
+ // same pattern as your AssetsFilter.vue
29
+ const iconFor = (assetCode) =>
30
+ new URL(`../assets/images/assets_images/${assetCode}.png`, import.meta.url).href
31
+ const hide = (e) => { e.target.style.display = 'none' }
32
 
33
  const available = computed(() => {
34
+ const rows = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
35
+ return Array.from(new Set(rows.map(r => r.asset)))
36
  })
37
+
38
  const orderedAssets = computed(() => {
39
  const present = new Set(available.value)
40
+ const primary = props.preferredOrder.filter(a => present.has(a))
41
+ const extras = [...present].filter(a => !props.preferredOrder.includes(a)).sort()
42
  const list = [...primary, ...extras]
43
+ if (!list.includes(props.modelValue) && list.length) emit('update:modelValue', list[0])
44
  return list
45
  })
 
 
 
 
 
 
 
 
 
46
  </script>
47
 
48
  <style scoped>
49
+ .asset-icon{
50
+ width: 18px; height: 18px; flex: 0 0 18px;
51
+ object-fit: contain; display: inline-block; vertical-align: middle;
 
52
  }
 
 
 
 
 
53
  </style>
src/lib/prices.ts DELETED
@@ -1,87 +0,0 @@
1
- // src/lib/prices.ts
2
- type PriceMap = Record<string, number>
3
-
4
- // Map asset code -> provider + symbol
5
- const MAP: Record<string, { p: 'binance'|'yahoo'; 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 (Yahoo uses plain tickers)
15
- AAPL:{ p: 'yahoo', s: 'AAPL' },
16
- MSFT:{ p: 'yahoo', s: 'MSFT' },
17
- BMRN:{ p: 'yahoo', s: 'BMRN' },
18
- MRNA:{ p: 'yahoo', s: 'MRNA' },
19
- TSLA:{ p: 'yahoo', s: 'TSLA' },
20
- };
21
-
22
- let latest: PriceMap = {};
23
- const subs = new Set<(m: PriceMap)=>void>();
24
- const push = () => subs.forEach(fn => fn({ ...latest }));
25
-
26
- export function subscribePrices(fn: (m: PriceMap)=>void) {
27
- subs.add(fn); fn({ ...latest });
28
- return () => subs.delete(fn);
29
- }
30
-
31
- async function fetchBinance(symbol: string) {
32
- const r = await fetch(`https://api.binance.com/api/v3/ticker/price?symbol=${encodeURIComponent(symbol)}`);
33
- if (!r.ok) throw new Error(`binance ${symbol} ${r.status}`);
34
- const j = await r.json(); const n = Number(j?.price);
35
- return Number.isFinite(n) ? n : undefined;
36
- }
37
-
38
- async function fetchYahoo(symbolsCsv: string) {
39
- const url = `https://query1.finance.yahoo.com/v7/finance/quote?symbols=${encodeURIComponent(symbolsCsv)}`
40
- const r = await fetch(url);
41
- if (!r.ok) throw new Error(`yahoo ${symbolsCsv} ${r.status}`);
42
- const j = await r.json();
43
- const out: Record<string, number> = {};
44
- for (const it of (j?.quoteResponse?.result ?? [])) {
45
- const sym = it?.symbol;
46
- const px = Number(it?.regularMarketPrice ?? it?.postMarketPrice ?? it?.preMarketPrice);
47
- if (sym && Number.isFinite(px)) out[sym] = px;
48
- }
49
- return out;
50
- }
51
-
52
- export async function pollPrices(codes: string[], intervalMs = 30000) {
53
- const uniq = [...new Set(codes)].filter(c => MAP[c]);
54
-
55
- // split by provider for efficient requests
56
- const binanceSyms = uniq.filter(c => MAP[c].p === 'binance').map(c => MAP[c].s);
57
- const yahooSyms = uniq.filter(c => MAP[c].p === 'yahoo').map(c => MAP[c].s);
58
-
59
- async function tick() {
60
- try {
61
- // Binance: per-symbol (cheap for a handful)
62
- await Promise.all(binanceSyms.map(async s => {
63
- const px = await fetchBinance(s);
64
- const code = Object.entries(MAP).find(([k,v]) => v.s === s)?.[0];
65
- if (code && px !== undefined) latest[code] = px;
66
- }));
67
-
68
- // Yahoo: batch in one call
69
- if (yahooSyms.length) {
70
- const batch = await fetchYahoo(yahooSyms.join(','));
71
- for (const [sym, px] of Object.entries(batch)) {
72
- const code = Object.entries(MAP).find(([k,v]) => v.s === sym)?.[0];
73
- if (code && px !== undefined) latest[code] = px;
74
- }
75
- }
76
- } catch { /* ignore transient errors */ }
77
- push();
78
- }
79
-
80
- await tick();
81
- setInterval(tick, intervalMs);
82
- }
83
-
84
- export const fmtUSD = (n?: number) =>
85
- typeof n === 'number'
86
- ? n.toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
87
- : '—';