Jimin Huang commited on
Commit
84b00d4
·
1 Parent(s): 787af51

Change settings

Browse files
Files changed (2) hide show
  1. src/components/AssetTabs.vue +40 -38
  2. src/lib/prices.ts +65 -0
src/components/AssetTabs.vue CHANGED
@@ -1,60 +1,62 @@
 
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" @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
- // 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
- // Same pattern as AssetsFilter.vue
30
- const iconFor = (assetCode) =>
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 => r.asset)))
38
  })
39
-
40
- // Deterministic ordering (without touching AssetsFilter.vue)
41
  const orderedAssets = computed(() => {
42
  const present = new Set(available.value)
43
- const primary = props.preferredOrder.filter(a => present.has(a))
44
- const extras = [...present].filter(a => !props.preferredOrder.includes(a)).sort()
45
  const list = [...primary, ...extras]
46
- if (!list.includes(props.modelValue) && list.length) emit('update:modelValue', list[0])
47
  return list
48
  })
 
 
 
 
 
 
 
 
 
49
  </script>
50
 
51
  <style scoped>
52
- .asset-icon{
53
- width: 20px !important; /* set to 16/20/24 to match your filter */
54
- height: 20px !important;
55
- flex: 0 0 20px;
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
+ : '—';