feat: shared @harb/web3 package + landing wallet connect (#157) (#159)

This commit is contained in:
johba 2026-02-19 20:18:27 +01:00
parent 66106077ba
commit db3633425a
17 changed files with 18548 additions and 199 deletions

View file

@ -16,6 +16,7 @@
<SocialButton type="telegram" href="https://t.me/kraikenportal"></SocialButton>
<SocialButton type="twitter" href="https://x.com/KrAIkenProtocol"></SocialButton>
</div>
<WalletButton />
</div>
<div class="menu-trigger" @click.stop="toggleMenu">
<svg width="24" height="24" viewBox="0 0 24 24">
@ -42,6 +43,7 @@
<script setup lang="ts">
import { RouterLink, useRouter } from 'vue-router';
import SocialButton from './SocialButton.vue';
import WalletButton from './WalletButton.vue';
import { onMounted, onUnmounted, ref, computed } from 'vue';
const router = useRouter();

View file

@ -0,0 +1,95 @@
<script setup lang="ts">
import { useAccount, useConnect, useDisconnect } from '@harb/web3';
import { ref } from 'vue';
const { address, isConnected } = useAccount();
const { connectors, connect } = useConnect();
const { disconnect } = useDisconnect();
const showConnectors = ref(false);
function shortAddress(addr: string) {
return `${addr.slice(0, 6)}${addr.slice(-4)}`;
}
function handleConnect(connector: (typeof connectors)[number]) {
connect({ connector });
showConnectors.value = false;
}
</script>
<template>
<div class="wallet-button">
<template v-if="isConnected && address">
<button class="wallet-button__connected" @click="disconnect()">
{{ shortAddress(address) }}
</button>
</template>
<template v-else>
<button class="wallet-button__connect" @click="showConnectors = !showConnectors">
Connect Wallet
</button>
<div v-if="showConnectors" class="wallet-button__dropdown">
<button
v-for="connector in connectors"
:key="connector.uid"
class="wallet-button__option"
@click="handleConnect(connector)"
>
{{ connector.name }}
</button>
</div>
</template>
</div>
</template>
<style lang="sass" scoped>
.wallet-button
position: relative
&__connect, &__connected
padding: 0.5rem 1rem
border-radius: 8px
border: 1px solid rgba(255, 255, 255, 0.2)
background: rgba(255, 255, 255, 0.08)
color: #fff
cursor: pointer
font-size: 0.9rem
transition: background 0.2s
&:hover
background: rgba(255, 255, 255, 0.15)
&__connect
background: #3b82f6
border-color: #3b82f6
&:hover
background: #2563eb
&__dropdown
position: absolute
top: 100%
right: 0
margin-top: 0.5rem
background: #1a1a2e
border: 1px solid rgba(255, 255, 255, 0.15)
border-radius: 8px
padding: 0.5rem
min-width: 200px
z-index: 100
&__option
display: block
width: 100%
padding: 0.5rem 0.75rem
background: none
border: none
color: #fff
cursor: pointer
text-align: left
border-radius: 4px
font-size: 0.85rem
&:hover
background: rgba(255, 255, 255, 0.1)
</style>

View file

@ -0,0 +1,137 @@
<script setup lang="ts">
import { ref, watch, computed } from 'vue';
import { useAccount } from '@harb/web3';
import axios from 'axios';
const { address, isConnected } = useAccount();
const holder = ref<{
balance: string;
totalEthSpent: string;
totalTokensAcquired: string;
} | null>(null);
const stats = ref<{ currentPriceWei: string } | null>(null);
const loading = ref(false);
const endpoint = `${window.location.origin}/api/graphql`;
async function fetchWalletData(addr: string) {
loading.value = true;
try {
const res = await axios.post(endpoint, {
query: `{
holders(address: "${addr.toLowerCase()}") {
balance
totalEthSpent
totalTokensAcquired
}
protocolStatss(where: { id: "0x01" }) {
items { currentPriceWei }
}
}`,
});
holder.value = res.data?.data?.holders ?? null;
stats.value = res.data?.data?.protocolStatss?.items?.[0] ?? null;
} catch {
holder.value = null;
} finally {
loading.value = false;
}
}
watch([address, isConnected], () => {
if (isConnected.value && address.value) {
void fetchWalletData(address.value);
} else {
holder.value = null;
}
}, { immediate: true });
function fmt(wei: string, decimals = 4) {
const n = Number(wei) / 1e18;
return n.toLocaleString('en', { maximumFractionDigits: decimals });
}
const balanceKrk = computed(() => fmt(holder.value?.balance ?? '0', 2));
const hasPosition = computed(() => holder.value && BigInt(holder.value.balance) > 0n);
const avgCost = computed(() => {
if (!holder.value) return 0;
const spent = Number(holder.value.totalEthSpent) / 1e18;
const acquired = Number(holder.value.totalTokensAcquired) / 1e18;
return acquired > 0 ? spent / acquired : 0;
});
const currentPrice = computed(() =>
stats.value ? Number(stats.value.currentPriceWei) / 1e18 : 0,
);
const pnlPercent = computed(() => {
if (avgCost.value === 0) return 0;
return (currentPrice.value / avgCost.value - 1) * 100;
});
const pnlClass = computed(() => (pnlPercent.value >= 0 ? 'positive' : 'negative'));
const appUrl = computed(() => `/app/#/wallet/${address.value}`);
</script>
<template>
<div v-if="isConnected && hasPosition" class="wallet-card" :class="pnlClass">
<div class="wallet-card__balance">{{ balanceKrk }} KRK</div>
<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>
</div>
<div v-else-if="isConnected && !loading && !hasPosition" class="wallet-card wallet-card--empty">
<span>No KRK yet</span>
<a href="#get-krk" class="wallet-card__link">Get KRK </a>
</div>
</template>
<style lang="sass" scoped>
.wallet-card
display: flex
align-items: center
gap: 1rem
padding: 0.75rem 1.25rem
border-radius: 12px
background: rgba(255, 255, 255, 0.06)
border: 1px solid rgba(255, 255, 255, 0.15)
margin-top: 1rem
&.positive
border-color: rgba(16, 185, 129, 0.4)
background: rgba(16, 185, 129, 0.06)
&.negative
border-color: rgba(239, 68, 68, 0.4)
background: rgba(239, 68, 68, 0.06)
&--empty
color: #9a9898
font-size: 0.85rem
&__balance
font-weight: 700
font-size: 1.1rem
&__pnl
font-weight: 600
.positive &
color: #10b981
.negative &
color: #ef4444
&__link
margin-left: auto
color: #3b82f6
text-decoration: none
font-size: 0.85rem
&:hover
text-decoration: underline
</style>