harb/landing/src/views/HolderDashboardView.vue
openhands fad6486152 fix: Post-purchase holder dashboard on landing page (#150)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 15:02:22 +00:00

307 lines
8.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="holder-dashboard">
<!-- Header -->
<div class="holder-dashboard__header">
<div class="holder-dashboard__addr-block">
<span class="holder-dashboard__label">Wallet</span>
<div class="holder-dashboard__addr-row">
<span class="holder-dashboard__addr">{{ truncatedAddress }}</span>
<button class="copy-btn" :class="{ copied }" @click="copyAddress" title="Copy address">
{{ copied ? '✓' : '⎘' }}
</button>
</div>
</div>
<div class="holder-dashboard__balance-block">
<span class="holder-dashboard__balance-num">{{ formatKrk(balanceKrk) }}</span>
<span class="holder-dashboard__balance-sym">KRK</span>
</div>
</div>
<!-- Loading / Error -->
<div v-if="loading && balanceKrk === 0" class="holder-dashboard__loading">
<div class="spinner"></div>
<p>Loading wallet data</p>
</div>
<div v-if="error" class="holder-dashboard__error"> {{ error }}</div>
<!-- P&L Card -->
<div
v-if="avgCostBasis > 0"
class="pnl-card"
:class="{ positive: unrealizedPnlEth >= 0, negative: unrealizedPnlEth < 0 }"
>
<div class="pnl-card__label">Unrealized P&amp;L</div>
<div class="pnl-card__value">
{{ unrealizedPnlEth >= 0 ? '+' : '' }}{{ fmtEthUsd(Math.abs(unrealizedPnlEth)) }}
</div>
<div class="pnl-card__value-secondary" v-if="ethUsdPrice">
{{ unrealizedPnlEth >= 0 ? '+' : '' }}{{ formatEthCompact(Math.abs(unrealizedPnlEth)) }}
</div>
<div class="pnl-card__percent">
{{ unrealizedPnlPercent >= 0 ? '+' : '' }}{{ unrealizedPnlPercent.toFixed(1) }}%
</div>
<div class="pnl-card__detail">
Avg cost: {{ fmtEthUsd(avgCostBasis) }}/KRK · Current: {{ fmtEthUsd(currentPriceEth) }}/KRK
</div>
</div>
<!-- Value Cards -->
<div class="value-cards">
<div class="value-card">
<div class="value-card__label">KRK Balance</div>
<div class="value-card__value">{{ formatKrk(balanceKrk) }}</div>
<div class="value-card__unit">KRK</div>
</div>
<div class="value-card">
<div class="value-card__label">ETH Backing</div>
<div class="value-card__value">{{ fmtEthUsd(ethBacking) }}</div>
<div class="value-card__unit" v-if="ethUsdPrice">{{ formatEthCompact(ethBacking) }}</div>
<div class="value-card__unit" v-else>ETH</div>
</div>
</div>
<!-- Transaction History (buy/sell only) -->
<TransactionHistory
:address="addressParam"
:graphql-url="graphqlUrl"
:type-filter="['buy', 'sell']"
/>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useHolderDashboard, useEthPrice, formatUsd, formatEthCompact, TransactionHistory } from '@harb/ui-shared';
const route = useRoute();
const addressParam = computed(() => String(route.params.address ?? ''));
const graphqlUrl = `${window.location.origin}/api/graphql`;
const { loading, error, balanceKrk, avgCostBasis, currentPriceEth, unrealizedPnlEth, unrealizedPnlPercent, ethBacking } =
useHolderDashboard(addressParam, graphqlUrl);
const { ethUsdPrice } = useEthPrice();
const copied = ref(false);
const truncatedAddress = computed(() => {
const a = addressParam.value;
if (!a || a.length < 10) return a;
return `${a.slice(0, 6)}${a.slice(-4)}`;
});
function copyAddress() {
navigator.clipboard.writeText(addressParam.value).then(() => {
copied.value = true;
setTimeout(() => (copied.value = false), 1500);
});
}
function formatKrk(val: number): string {
if (!Number.isFinite(val)) return '0.00';
return val.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 4 });
}
/** USD primary; ETH fallback when price not yet loaded */
function fmtEthUsd(val: number): string {
if (!Number.isFinite(val)) return '—';
if (ethUsdPrice.value !== null) return formatUsd(val * ethUsdPrice.value);
return formatEthCompact(val);
}
</script>
<style lang="sass" scoped>
.holder-dashboard
display: flex
flex-direction: column
gap: 28px
padding: 16px
max-width: 1200px
margin: 0 auto
@media (min-width: 992px)
padding: 48px
// ─── Header ───────────────────────────────────────────────────────────────────
.holder-dashboard__header
background: linear-gradient(135deg, rgba(117, 80, 174, 0.15) 0%, rgba(117, 80, 174, 0.05) 100%)
border: 1px solid rgba(117, 80, 174, 0.3)
border-radius: 20px
padding: 28px 32px
display: flex
flex-direction: column
gap: 16px
@media (min-width: 768px)
flex-direction: row
align-items: center
justify-content: space-between
.holder-dashboard__label
font-size: 12px
text-transform: uppercase
letter-spacing: 1.5px
color: #7550AE
margin-bottom: 6px
display: block
.holder-dashboard__addr-row
display: flex
align-items: center
gap: 10px
.holder-dashboard__addr
font-size: 16px
color: #ffffff
font-family: monospace
word-break: break-all
.holder-dashboard__balance-block
display: flex
align-items: baseline
gap: 8px
.holder-dashboard__balance-num
font-size: 36px
color: #7550AE
@media (min-width: 768px)
font-size: 44px
.holder-dashboard__balance-sym
font-size: 18px
color: #9A9898
.copy-btn
background: rgba(117, 80, 174, 0.2)
border: 1px solid rgba(117, 80, 174, 0.4)
border-radius: 6px
color: #ffffff
cursor: pointer
font-size: 16px
padding: 4px 10px
transition: background 0.2s
&:hover
background: rgba(117, 80, 174, 0.4)
&.copied
color: #4ADE80
border-color: #4ADE80
// ─── Loading / Error ──────────────────────────────────────────────────────────
.holder-dashboard__loading
display: flex
align-items: center
gap: 12px
color: #9A9898
padding: 16px
.holder-dashboard__error
background: rgba(248, 113, 113, 0.1)
border: 1px solid rgba(248, 113, 113, 0.3)
border-radius: 12px
padding: 16px
color: #F87171
.spinner
width: 20px
height: 20px
border: 2px solid rgba(117, 80, 174, 0.3)
border-top-color: #7550AE
border-radius: 50%
animation: spin 0.8s linear infinite
@keyframes spin
to
transform: rotate(360deg)
// ─── P&L Card ─────────────────────────────────────────────────────────────────
.pnl-card
background: rgba(255,255,255,0.06)
border: 1px solid rgba(255,255,255,0.15)
border-radius: 16px
padding: 1.5rem
text-align: center
&.positive
border-color: rgba(16, 185, 129, 0.4)
background: rgba(16, 185, 129, 0.08)
&.negative
border-color: rgba(239, 68, 68, 0.4)
background: rgba(239, 68, 68, 0.08)
&__label
font-size: 0.85rem
color: #9A9898
margin-bottom: 0.5rem
&__value
font-size: 1.8rem
font-weight: 700
.positive &
color: #10B981
.negative &
color: #EF4444
&__value-secondary
font-size: 0.85rem
margin-top: 0.15rem
opacity: 0.6
.positive &
color: #10B981
.negative &
color: #EF4444
&__percent
font-size: 1.2rem
font-weight: 600
margin-top: 0.25rem
.positive &
color: #10B981
.negative &
color: #EF4444
&__detail
font-size: 0.75rem
color: #9A9898
margin-top: 0.75rem
// ─── Value Cards ──────────────────────────────────────────────────────────────
.value-cards
display: flex
flex-direction: column
gap: 16px
@media (min-width: 768px)
flex-direction: row
.value-card
flex: 1
background: #07111B
border: 1px solid rgba(117, 80, 174, 0.2)
border-radius: 16px
padding: 24px
display: flex
flex-direction: column
gap: 6px
&__label
font-size: 12px
text-transform: uppercase
letter-spacing: 1px
color: #9A9898
&__value
font-size: 28px
color: #ffffff
line-height: 1.2
&__unit
font-size: 13px
color: #7550AE
font-weight: 600
</style>