Spaces:
Running
Running
feat: add refresh button to reload latest data from Supabase
Browse files- src/views/LiveView.vue +55 -0
src/views/LiveView.vue
CHANGED
|
@@ -6,6 +6,14 @@
|
|
| 6 |
<AssetTabs v-model="asset" :ordered-assets="orderedAssets" />
|
| 7 |
</div>
|
| 8 |
<div class="toolbar__right">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
<div class="mode">
|
| 10 |
<button class="mode__btn" :class="{ 'is-active': mode==='usd' }" @click="mode='usd'">$</button>
|
| 11 |
<button class="mode__btn" :class="{ 'is-active': mode==='pct' }" @click="mode='pct'">%</button>
|
|
@@ -124,6 +132,7 @@ const ASSET_CUTOFF = { BTC: '2025-08-01' }
|
|
| 124 |
const mode = ref('usd')
|
| 125 |
const asset = ref('BTC')
|
| 126 |
const rowsRef = ref([])
|
|
|
|
| 127 |
let allDecisions = []
|
| 128 |
const cards = shallowRef([])
|
| 129 |
|
|
@@ -146,6 +155,23 @@ onMounted(async () => {
|
|
| 146 |
}
|
| 147 |
})
|
| 148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
/* ---------- helpers ---------- */
|
| 150 |
const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
|
| 151 |
const signedMoney = (n) => `${n >= 0 ? '+' : '−'}${fmtUSD(Math.abs(n))}`
|
|
@@ -335,6 +361,35 @@ watch(
|
|
| 335 |
|
| 336 |
/* toolbar */
|
| 337 |
.toolbar { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 0 10px; background: #ffffff; color: #0f172a; border-bottom: 1px solid #E7ECF3; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
.mode__btn { height: 30px; min-width: 40px; padding: 0 10px; border-radius: 10px; border: 1px solid #D7DDE7; background: #ffffff; font-weight: 700; color: #0f172a; }
|
| 339 |
.mode__btn.is-active { background: #0f172a; color: #ffffff; border-color: #0f172a; }
|
| 340 |
|
|
|
|
| 6 |
<AssetTabs v-model="asset" :ordered-assets="orderedAssets" />
|
| 7 |
</div>
|
| 8 |
<div class="toolbar__right">
|
| 9 |
+
<button
|
| 10 |
+
class="refresh-btn"
|
| 11 |
+
@click="refreshData"
|
| 12 |
+
:disabled="refreshing"
|
| 13 |
+
title="Refresh data from database"
|
| 14 |
+
>
|
| 15 |
+
<i class="pi pi-refresh" :class="{ 'spinning': refreshing }"></i>
|
| 16 |
+
</button>
|
| 17 |
<div class="mode">
|
| 18 |
<button class="mode__btn" :class="{ 'is-active': mode==='usd' }" @click="mode='usd'">$</button>
|
| 19 |
<button class="mode__btn" :class="{ 'is-active': mode==='pct' }" @click="mode='pct'">%</button>
|
|
|
|
| 132 |
const mode = ref('usd')
|
| 133 |
const asset = ref('BTC')
|
| 134 |
const rowsRef = ref([])
|
| 135 |
+
const refreshing = ref(false)
|
| 136 |
let allDecisions = []
|
| 137 |
const cards = shallowRef([])
|
| 138 |
|
|
|
|
| 155 |
}
|
| 156 |
})
|
| 157 |
|
| 158 |
+
/* ---------- refresh data ---------- */
|
| 159 |
+
async function refreshData() {
|
| 160 |
+
if (refreshing.value) return
|
| 161 |
+
refreshing.value = true
|
| 162 |
+
try {
|
| 163 |
+
console.log('[Live] Force refreshing data from Supabase...')
|
| 164 |
+
await dataService.forceRefresh()
|
| 165 |
+
rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
|
| 166 |
+
allDecisions = getAllDecisions() || []
|
| 167 |
+
console.log('[Live] Data refreshed successfully')
|
| 168 |
+
} catch (e) {
|
| 169 |
+
console.error('[Live] Error refreshing data:', e)
|
| 170 |
+
} finally {
|
| 171 |
+
refreshing.value = false
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
/* ---------- helpers ---------- */
|
| 176 |
const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
|
| 177 |
const signedMoney = (n) => `${n >= 0 ? '+' : '−'}${fmtUSD(Math.abs(n))}`
|
|
|
|
| 361 |
|
| 362 |
/* toolbar */
|
| 363 |
.toolbar { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 0 10px; background: #ffffff; color: #0f172a; border-bottom: 1px solid #E7ECF3; }
|
| 364 |
+
.toolbar__right { display: flex; align-items: center; gap: 12px; }
|
| 365 |
+
.refresh-btn {
|
| 366 |
+
height: 30px;
|
| 367 |
+
width: 30px;
|
| 368 |
+
border-radius: 10px;
|
| 369 |
+
border: 1px solid #D7DDE7;
|
| 370 |
+
background: #ffffff;
|
| 371 |
+
color: #0f172a;
|
| 372 |
+
cursor: pointer;
|
| 373 |
+
display: flex;
|
| 374 |
+
align-items: center;
|
| 375 |
+
justify-content: center;
|
| 376 |
+
transition: all 0.2s ease;
|
| 377 |
+
}
|
| 378 |
+
.refresh-btn:hover:not(:disabled) {
|
| 379 |
+
background: #f8f9fa;
|
| 380 |
+
border-color: #0f172a;
|
| 381 |
+
}
|
| 382 |
+
.refresh-btn:disabled {
|
| 383 |
+
opacity: 0.5;
|
| 384 |
+
cursor: not-allowed;
|
| 385 |
+
}
|
| 386 |
+
.refresh-btn i.spinning {
|
| 387 |
+
animation: spin 1s linear infinite;
|
| 388 |
+
}
|
| 389 |
+
@keyframes spin {
|
| 390 |
+
from { transform: rotate(0deg); }
|
| 391 |
+
to { transform: rotate(360deg); }
|
| 392 |
+
}
|
| 393 |
.mode__btn { height: 30px; min-width: 40px; padding: 0 10px; border-radius: 10px; border: 1px solid #D7DDE7; background: #ffffff; font-weight: 700; color: #0f172a; }
|
| 394 |
.mode__btn.is-active { background: #0f172a; color: #ffffff; border-color: #0f172a; }
|
| 395 |
|