harb/web-app/src/components/LocalSwapWidget.vue
openhands 5b69d9ee3d fix: address review findings in sell KRK widget
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 12:39:15 +00:00

221 lines
5.7 KiB
Vue

<template>
<div class="local-swap-widget">
<template v-if="!canSwap">
<p class="swap-warning">Connect your wallet to swap ETH for $KRK on the local Uniswap pool.</p>
</template>
<template v-else>
<div class="swap-tabs">
<button
class="swap-tab"
:class="{ active: mode === 'buy' }"
data-testid="swap-mode-buy"
@click="setMode('buy')"
>Buy</button>
<button
class="swap-tab"
:class="{ active: mode === 'sell' }"
data-testid="swap-mode-sell"
@click="setMode('sell')"
>Sell</button>
</div>
<template v-if="mode === 'buy'">
<div class="swap-field">
<label for="local-swap-amount" class="swap-label">ETH to spend</label>
<input id="local-swap-amount" data-testid="swap-amount-input" :value="swapAmount" @input="swapAmount = ($event.target as HTMLInputElement).value" type="number" min="0" step="0.01" class="swap-input" :disabled="swapping" />
</div>
<button class="swap-button" data-testid="swap-buy-button" :disabled="swapping" @click="buyKrk">
{{ swapping ? 'Submitting' : 'Buy KRK' }}
</button>
<p class="swap-hint">Wraps ETH WETH, approves the swap router, then trades into $KRK.</p>
</template>
<template v-else>
<div class="swap-field">
<div class="swap-label-row">
<label for="local-sell-amount" class="swap-label">KRK to sell</label>
<span class="swap-balance">Balance: {{ formattedKrkBalance }} KRK</span>
</div>
<div class="swap-input-row">
<input id="local-sell-amount" data-testid="swap-sell-amount-input" :value="sellAmount" @input="sellAmount = ($event.target as HTMLInputElement).value" type="number" min="0" step="0.01" class="swap-input" :disabled="selling" />
<button class="max-button" :disabled="selling" @click="setMax">Max</button>
</div>
</div>
<button class="swap-button sell-button" data-testid="swap-sell-button" :disabled="selling" @click="sellKrk">
{{ selling ? (sellPhase === 'approving' ? 'Approving' : 'Selling') : 'Sell KRK' }}
</button>
<p class="swap-hint">Approves the swap router, then trades $KRK WETH.</p>
</template>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { useSwapKrk } from '@/composables/useSwapKrk';
import { formatUnits } from 'viem';
const { swapAmount, swapping, canSwap, buyKrk, sellAmount, selling, sellPhase, krkBalance, loadKrkBalance, sellKrk } = useSwapKrk();
const mode = ref<'buy' | 'sell'>('buy');
const formattedKrkBalance = computed(() => {
const s = formatUnits(krkBalance.value, 18);
const [whole, frac = ''] = s.split('.');
const truncFrac = frac.slice(0, 4).replace(/0+$/, '');
const wholeFormatted = BigInt(whole).toLocaleString();
return truncFrac ? `${wholeFormatted}.${truncFrac}` : wholeFormatted;
});
function setMode(m: 'buy' | 'sell') {
mode.value = m;
if (m === 'sell') loadKrkBalance();
}
async function setMax() {
await loadKrkBalance();
sellAmount.value = formatUnits(krkBalance.value, 18);
}
onMounted(() => {
if (canSwap.value) loadKrkBalance();
});
</script>
<style lang="sass" scoped>
.local-swap-widget
display: flex
flex-direction: column
gap: 16px
.swap-warning
margin: 0
color: #FFB347
font-size: 14px
.swap-tabs
display: flex
gap: 8px
.swap-tab
flex: 1
padding: 8px 0
border-radius: 8px
border: 1px solid #3a3a3a
background: #111111
color: #a3a3a3
font-size: 14px
font-weight: 600
cursor: pointer
transition: all 0.2s
&.active
background: #1e3a5f
border-color: #60a5fa
color: #ffffff
&:hover:not(.active)
border-color: #60a5fa
color: #ffffff
.swap-label
display: block
font-size: 13px
color: #a3a3a3
margin-bottom: 6px
.swap-label-row
display: flex
justify-content: space-between
align-items: baseline
margin-bottom: 6px
.swap-balance
font-size: 12px
color: #a3a3a3
.swap-field
display: flex
flex-direction: column
.swap-input-row
display: flex
gap: 8px
.swap-input
background: #111111
border: 1px solid #3a3a3a
border-radius: 8px
color: #ffffff
font-size: 16px
padding: 12px 16px
outline: none
transition: border-color 0.2s
flex: 1
min-width: 0
&:focus
border-color: #60a5fa
&:disabled
opacity: 0.5
cursor: not-allowed
.max-button
padding: 0 16px
border-radius: 8px
border: 1px solid #3a3a3a
background: #1a1a1a
color: #60a5fa
font-size: 13px
font-weight: 600
cursor: pointer
transition: all 0.2s
white-space: nowrap
&:hover:not(:disabled)
border-color: #60a5fa
background: #1e3a5f
&:disabled
opacity: 0.5
cursor: not-allowed
.swap-button
display: flex
align-items: center
justify-content: center
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%)
color: #ffffff
font-size: 16px
font-weight: 600
padding: 16px 32px
border-radius: 12px
border: none
cursor: pointer
transition: all 0.3s ease
box-shadow: 0 4px 12px rgba(96, 165, 250, 0.3)
&:hover:not(:disabled)
transform: translateY(-2px)
box-shadow: 0 6px 16px rgba(96, 165, 250, 0.4)
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)
&:disabled
opacity: 0.6
cursor: not-allowed
transform: none
&.sell-button
background: linear-gradient(135deg, #f472b6 0%, #ec4899 100%)
box-shadow: 0 4px 12px rgba(244, 114, 182, 0.3)
&:hover:not(:disabled)
box-shadow: 0 6px 16px rgba(244, 114, 182, 0.4)
background: linear-gradient(135deg, #ec4899 0%, #db2777 100%)
.swap-hint
margin: 0
font-size: 12px
color: #a3a3a3
</style>