harb/web-app/src/views/StakeView.vue
openhands ca8e4737fe fix: Fix collapse component formatting: taxPaid display, string coercion, BigInt precision (#260)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 08:06:49 +00:00

289 lines
9.4 KiB
Vue

<template>
<div class="stake-view">
<div class="getting-started-guide">
<h4 class="getting-started-guide__title">🚀 Getting Started</h4>
<div class="getting-started-guide__steps">
<div class="getting-started-step">
<div class="step-number">1</div>
<div class="step-content">
<div class="step-title">Connect your wallet</div>
<div class="step-description">Use the button below to connect your Web3 wallet</div>
</div>
</div>
<div class="getting-started-step">
<div class="step-number">2</div>
<div class="step-content">
<div class="step-title">Get KRK</div>
<div class="step-description">Swap ETH for $KRK on Uniswap (link above)</div>
</div>
</div>
<div class="getting-started-step">
<div class="step-number">3</div>
<div class="step-content">
<div class="step-title">Stake your position</div>
<div class="step-description">Choose your tax rate and stake to claim owner slots</div>
</div>
</div>
</div>
</div>
<div class="stake-view-wrapper">
<h3>
Staking Dashboard
<IconInfo size="20px">
<template #text>
Stake your $KRK and claim owner slots. Owner slots give a return on every token buy that increases the liquidity of KrAIken.
Owner slots are limited to 20,000 slots in total. To enable everyone and anyone to join staking is protected by a "Harberger
Tax" mechanism.
</template>
</IconInfo>
</h3>
<ProtocolStatsCard></ProtocolStatsCard>
<div class="stake-view-body">
<ChartComplete></ChartComplete>
<div class="hold-stake-wrapper">
<FCard class="inner-border">
<template v-if="!isChainSupported"> Chain not supported </template>
<template v-else-if="status !== 'connected'">
<FButton @click="showPanel = true" size="large" block>Connect Wallet</FButton>
</template>
<template v-else>
<StakeHolder></StakeHolder>
</template>
</FCard>
</div>
</div>
</div>
<div class="statistics-wrapper">
<h3>Statistics</h3>
<div class="statistics-outputs-wrapper">
<StatsOutput headline="Average Slot Tax" :price="`${averageTaxRate.toFixed(2)} %`"></StatsOutput>
<StatsOutput
headline="Claimed Owner Slots"
:price="`${commaNumber(stats.claimedSlots)} / ${commaNumber(stats.maxSlots)}`"
></StatsOutput>
<StatsOutput headline="Total Supply Change / 7d" :price="`+ ${stats.totalSupplyChange7d} $KRK`"></StatsOutput>
<StatsOutput headline="Inflation / 7d" :price="`+${stats.inflation7d}%`"></StatsOutput>
</div>
</div>
<div class="active-positions-wrapper">
<h3>Active Positions</h3>
<div v-if="myActivePositions.length === 0 && myClosedPositions.length > 0" class="no-active-positions">
No active positions your position history is below.
</div>
<div class="active-positions-list">
<CollapseActive
v-for="position in myActivePositions"
:taxRate="position.taxRatePercentage"
:taxRateIndex="position.taxRateIndex"
:amount="position.amount"
:tresholdIndex="tresholdValue"
:id="position.positionId"
:position="position"
:key="position.id"
></CollapseActive>
</div>
</div>
<div v-if="myClosedPositions.length > 0" class="position-history-wrapper">
<h3>Position History</h3>
<div class="position-history-list">
<CollapseHistory
v-for="position in myClosedPositions"
:key="position.id"
:taxRate="position.taxRatePercentage"
:taxPaid="weiToNumber(position.taxPaid)"
:id="position.positionId"
:amount="position.amount"
:position="position"
></CollapseHistory>
</div>
</div>
<!-- <f-button @click="getGraphData">graphql test</f-button>
<div v-for="position in positions" :key="position.id">
{{ position }}
</div> -->
</div>
</template>
<script setup lang="ts">
import StakeHolder from '@/components/StakeHolder.vue';
import ChartComplete from '@/components/chart/ChartComplete.vue';
import StatsOutput from '@/components/StatsOutput.vue';
import CollapseActive from '@/components/collapse/CollapseActive.vue';
import CollapseHistory from '@/components/collapse/CollapseHistory.vue';
import ProtocolStatsCard from '@/components/ProtocolStatsCard.vue';
import { onMounted, computed, inject } from 'vue';
import { useStatCollection } from '@/composables/useStatCollection';
import { useChains, useAccount } from '@wagmi/vue';
import { useWallet } from '@/composables/useWallet';
import FCard from '@/components/fcomponents/FCard.vue';
import IconInfo from '@/components/icons/IconInfo.vue';
import FButton from '@/components/fcomponents/FButton.vue';
import { DEFAULT_CHAIN_ID } from '@/config';
// todo interface positions
import { usePositions } from '@/composables/usePositions';
const { status } = useAccount();
const showPanel = inject('showPanel');
import { commaNumber, weiToNumber } from 'kraiken-lib/format';
const wallet = useWallet();
const initialChainId = wallet.account.chainId ?? DEFAULT_CHAIN_ID;
const { myActivePositions, myClosedPositions, tresholdValue, activePositions } = usePositions(initialChainId);
const stats = useStatCollection(initialChainId);
const chains = useChains();
function calculateAverageTaxRate(data: Array<{ taxRate: number | string }>): number {
if (data.length === 0) {
return 0;
}
const totalTaxRate = data.reduce((sum: number, entry: { taxRate: number | string }) => sum + parseFloat(String(entry.taxRate)), 0);
const averageTaxRate = totalTaxRate / data.length;
return averageTaxRate * 100;
}
const averageTaxRate = computed(() => calculateAverageTaxRate(activePositions.value));
const supportedChainIds = computed(() => chains.value.map(chain => chain.id));
const currentChainId = computed(() => wallet.account.chainId ?? DEFAULT_CHAIN_ID);
const isChainSupported = computed(() => supportedChainIds.value.includes(currentChainId.value));
// Refresh balance when navigating to stake page (e.g., after swapping)
onMounted(async () => {
await wallet.loadBalance();
});
</script>
<style lang="sass">
.stake-view
display: flex
flex-direction: column
gap: 32px
padding: 16px
@media (min-width: 992px)
padding: 48px
.getting-started-guide
background: linear-gradient(135deg, rgba(117, 80, 174, 0.1) 0%, rgba(117, 80, 174, 0.05) 100%)
border: 1px solid rgba(117, 80, 174, 0.2)
border-radius: 16px
padding: 24px
margin-bottom: 8px
&__title
margin: 0 0 20px 0
font-size: 20px
font-weight: 600
color: #FFFFFF
&__steps
display: flex
flex-direction: column
gap: 16px
@media (min-width: 768px)
flex-direction: row
gap: 20px
.getting-started-step
display: flex
gap: 12px
align-items: flex-start
flex: 1
.step-number
flex-shrink: 0
width: 32px
height: 32px
border-radius: 50%
background-color: #7550AE
color: #FFFFFF
display: flex
align-items: center
justify-content: center
font-weight: 700
font-size: 16px
.step-content
flex: 1
.step-title
font-weight: 600
font-size: 15px
color: #FFFFFF
margin-bottom: 4px
.step-description
font-size: 13px
color: #9A9898
line-height: 1.4
.stake-view
.stake-view-wrapper
background-color: #07111B
padding: 12px 0
border-radius: 24px
@media (min-width: 992px)
padding: 24px
.stake-view-body
display: flex
gap: 40px
position: relative
flex-direction: column
@media (min-width: 992px)
flex-direction: row
.positions-graph
flex: 1 1 67%
.hold-stake-wrapper
flex: 1 1 33%
display: flex
flex-direction: column
gap: 48px
.f-card
overflow: unset
.f-card__body
height: 100%
display: flex
align-items: center
justify-content: center
&.inner-border
padding: 0
.statistics-wrapper
.statistics-outputs-wrapper
display: flex
justify-content: center
flex-direction: column
gap: 46px
@media (min-width: 768px)
flex-direction: row
flex-wrap: wrap
.stats-output
min-width: 280px
.active-positions-wrapper
width: 100%
margin-left: auto
margin-right: auto
@media (min-width: 768px)
width: 580px
.no-active-positions
color: #9A9898
font-size: 14px
padding: 16px 0
.active-positions-list
display: flex
flex-direction: column
gap: 12px
.position-history-wrapper
width: 100%
margin-left: auto
margin-right: auto
@media (min-width: 768px)
width: 580px
.position-history-list
display: flex
flex-direction: column
gap: 12px
</style>