fix: Post-purchase holder dashboard on landing page (#150)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
058451792f
commit
e89fd4013d
11 changed files with 779 additions and 5 deletions
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { useAccount } from '@harb/web3';
|
||||
|
||||
const { address, isConnected } = useAccount();
|
||||
|
|
@ -78,7 +79,7 @@ const pnlPercent = computed(() => {
|
|||
|
||||
const pnlClass = computed(() => (pnlPercent.value >= 0 ? 'positive' : 'negative'));
|
||||
|
||||
const appUrl = computed(() => `/app/wallet/${address.value}`);
|
||||
const walletRoute = computed(() => ({ name: 'wallet', params: { address: address.value } }));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -87,7 +88,7 @@ const appUrl = computed(() => `/app/wallet/${address.value}`);
|
|||
<div v-if="avgCost > 0" class="wallet-card__pnl">
|
||||
{{ pnlPercent >= 0 ? '+' : '' }}{{ pnlPercent.toFixed(1) }}%
|
||||
</div>
|
||||
<a :href="appUrl" class="wallet-card__link">View Dashboard →</a>
|
||||
<RouterLink :to="walletRoute" class="wallet-card__link">View Dashboard →</RouterLink>
|
||||
</div>
|
||||
<div v-else-if="isConnected && !loading && !hasPosition" class="wallet-card wallet-card--empty">
|
||||
<span>No KRK yet</span>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ const router = createRouter({
|
|||
name: 'mixed',
|
||||
component: () => import('../views/HomeViewMixed.vue'),
|
||||
},
|
||||
{
|
||||
path: '/wallet/:address',
|
||||
name: 'wallet',
|
||||
component: () => import('../views/HolderDashboardView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/docs',
|
||||
name: 'Docs',
|
||||
|
|
|
|||
288
landing/src/views/HolderDashboardView.vue
Normal file
288
landing/src/views/HolderDashboardView.vue
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
<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&L</div>
|
||||
<div class="pnl-card__value">
|
||||
{{ unrealizedPnlEth >= 0 ? '+' : '' }}{{ formatEth(unrealizedPnlEth) }} ETH
|
||||
</div>
|
||||
<div class="pnl-card__percent">
|
||||
{{ unrealizedPnlPercent >= 0 ? '+' : '' }}{{ unrealizedPnlPercent.toFixed(1) }}%
|
||||
</div>
|
||||
<div class="pnl-card__detail">
|
||||
Avg cost: {{ formatEth(avgCostBasis) }} ETH/KRK · Current: {{ formatEth(currentPriceEth) }} ETH/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">{{ formatEth(ethBacking) }}</div>
|
||||
<div class="value-card__unit">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, 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 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 });
|
||||
}
|
||||
|
||||
function formatEth(val: number): string {
|
||||
if (!Number.isFinite(val)) return '0.0000';
|
||||
return val.toLocaleString('en-US', { minimumFractionDigits: 4, maximumFractionDigits: 6 });
|
||||
}
|
||||
</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
|
||||
|
||||
&__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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue