Merge pull request #372 from alisaweb3/v3-single

UI Refactor: language switch, theme toggle,governance,block,staking
This commit is contained in:
ping 2023-05-06 15:58:55 +08:00 committed by GitHub
commit 8da4118f31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 558 additions and 491 deletions

View File

@ -25,7 +25,7 @@ function calculateValue(value: any){
</script> </script>
<template> <template>
<div <div
class="bg-card px-4 pt-3 pb-4 rounded mt-5" class="bg-base-100 px-4 pt-3 pb-4 rounded mt-5"
v-if="props.cardItem?.items && props.cardItem?.items?.length > 0" v-if="props.cardItem?.items && props.cardItem?.items?.length > 0"
> >
<div class="text-base mb-3 text-main">{{ props.cardItem?.title }}</div> <div class="text-base mb-3 text-main">{{ props.cardItem?.title }}</div>

View File

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { getLogo, useDashboard, } from '@/stores/useDashboard'; import { getLogo, useDashboard } from '@/stores/useDashboard';
import { computed } from 'vue'; import { computed } from 'vue';
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue';
const props = defineProps({ const props = defineProps({
name: { name: {
@ -10,27 +10,40 @@ const props = defineProps({
}, },
}); });
const dashboardStore = useDashboard() const dashboardStore = useDashboard();
const conf = computed(() => dashboardStore.chains[props.name] || {}) const conf = computed(() => dashboardStore.chains[props.name] || {});
const addFavor = (e: Event) => { const addFavor = (e: Event) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
dashboardStore.favoriteMap[props.name] = !dashboardStore?.favoriteMap?.[props.name]; dashboardStore.favoriteMap[props.name] =
window.localStorage.setItem('favoriteMap', JSON.stringify(dashboardStore.favoriteMap)) !dashboardStore?.favoriteMap?.[props.name];
} window.localStorage.setItem(
'favoriteMap',
JSON.stringify(dashboardStore.favoriteMap)
);
};
</script> </script>
<template> <template>
<RouterLink :to="`/${name}`" class="bg-base-100 rounded shadow flex items-center px-3 py-3 cursor-pointer"> <RouterLink
:to="`/${name}`"
class="bg-base-100 hover:bg-base-content rounded shadow flex items-center px-3 py-3 cursor-pointer"
>
<div class="w-8 h-8 rounded-full overflow-hidden"> <div class="w-8 h-8 rounded-full overflow-hidden">
<img :src="conf.logo" /> <img :src="conf.logo" />
</div> </div>
<div class="font-semibold ml-4 text-base flex-1"> <div class="font-semibold ml-4 text-base flex-1">
{{ conf?.prettyName || props.name }} {{ conf?.prettyName || props.name }}
</div> </div>
<div @click="addFavor" class="pl-4 text-xl" <div
:class="{ 'text-warning': dashboardStore?.favoriteMap?.[props.name], 'text-gray-300 dark:text-gray-500': !dashboardStore?.favoriteMap?.[props.name] }"> @click="addFavor"
class="pl-4 text-xl"
:class="{
'text-warning': dashboardStore?.favoriteMap?.[props.name],
'text-gray-300 dark:text-gray-500':
!dashboardStore?.favoriteMap?.[props.name],
}"
>
<Icon icon="mdi-star" /> <Icon icon="mdi-star" />
</div> </div>
</RouterLink> </RouterLink>

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useThemeConfig } from '@core/composable/useThemeConfig'; import { useThemeConfig } from '@core/composable/useThemeConfig';
import type { ThemeSwitcherTheme } from '@layouts/types'; import type { ThemeSwitcherTheme } from '@layouts/types';
import { Icon } from '@iconify/vue';
import { onMounted, watch } from 'vue'; import { onMounted, watch } from 'vue';
const props = defineProps<{ const props = defineProps<{
@ -50,10 +51,12 @@ onMounted(() => {
</script> </script>
<template> <template>
<IconBtn @click="changeTheme"> <div class="tooltip tooltip-bottom delay-1000" :data-tip="currentThemeName">
<VIcon :icon="props.themes[currentThemeIndex].icon" /> <button
<VTooltip activator="parent" open-delay="1000"> class="btn btn-ghost btn-circle btn-sm mx-1"
<span class="text-capitalize">{{ currentThemeName }}</span> @click="changeTheme"
</VTooltip> >
</IconBtn> <Icon :icon="props.themes[currentThemeIndex].icon" class="text-2xl" />
</button>
</div>
</template> </template>

View File

@ -6,14 +6,18 @@ const props = defineProps(["value"]);
</script> </script>
<template> <template>
<VTable> <div class="overflow-x-auto">
<tbody> <table class="table w-full text-sm">
<tr v-for="(v, k) of value"> <tbody>
<td class="text-capitalize" style="max-width: 200px;">{{ k }}</td> <tr v-for="(v, k) of value">
<td><div class="overflow-hidden w-auto" style="max-width: 1000px;"> <td class="text-capitalize" style="max-width: 200px;">{{ k }}</td>
<Component v-if="v" :is="select(v, 'horizontal')" :value="v"></Component></div> <td>
</td> <div class="overflow-hidden w-auto whitespace-normal" style="max-width: 1000px;">
</tr> <Component v-if="v" :is="select(v, 'horizontal')" :value="v"></Component>
</tbody> </div>
</VTable> </td>
</tr>
</tbody>
</table>
</div>
</template> </template>

View File

@ -1,33 +0,0 @@
<template>
<div class="h-100 d-flex align-center justify-space-between">
<!-- 👉 Footer: left content -->
<span class="d-flex align-center">
&copy;
{{ new Date().getFullYear() }}
Made With
<VIcon
icon="mdi-heart-outline"
color="error"
size="1.25rem"
class="mx-1"
/>
By <a
href="https://ping.pub"
target="_blank"
rel="noopener noreferrer"
class="text-primary ms-1"
>Ping.pub</a>
</span>
<!-- 👉 Footer: right content -->
<span class="d-md-flex gap-x-4 text-primary d-none">
<a
href="https://github.com/ping-pub/explorer/blob/master/LICENSE"
target="noopener noreferrer"
>License</a>
<a
href="https://github.com/ping-pub/explorer"
target="noopener noreferrer"
>Github</a>
</span>
</div>
</template>

View File

@ -20,6 +20,5 @@ const themes: ThemeSwitcherTheme[] = [
<template> <template>
<div> <div>
<NewThemeSwitcher :themes="themes"/> <NewThemeSwitcher :themes="themes"/>
<!-- <ThemeSwitcher :themes="themes" /> -->
</div> </div>
</template> </template>

View File

@ -1,58 +1,66 @@
<script lang="ts" setup> <script lang="ts" setup>
import TxsElement from '@/components/dynamic/TxsElement.vue'; import TxsElement from '@/components/dynamic/TxsElement.vue';
import { useBlockModule } from './block' import { useBlockModule } from './block';
import DynamicComponent from '@/components/dynamic/DynamicComponent.vue'; import DynamicComponent from '@/components/dynamic/DynamicComponent.vue';
import { computed } from '@vue/reactivity'; import { computed } from '@vue/reactivity';
import { onBeforeRouteUpdate } from 'vue-router'; import { onBeforeRouteUpdate } from 'vue-router';
const props = defineProps(["height", "chain"]); const props = defineProps(['height', 'chain']);
const store = useBlockModule() const store = useBlockModule();
store.fetchBlock(props.height) store.fetchBlock(props.height);
const tab = ref('summary') const tab = ref('summary');
const height = computed(() => { const height = computed(() => {
return Number(store.current.block?.header?.height || props.height || 0) return Number(store.current.block?.header?.height || props.height || 0);
}) });
onBeforeRouteUpdate(async (to, from, next) => { onBeforeRouteUpdate(async (to, from, next) => {
if (from.path !== to.path) { if (from.path !== to.path) {
store.fetchBlock(String(to.params.height)) store.fetchBlock(String(to.params.height));
next() next();
} }
}) });
</script> </script>
<template> <template>
<div> <div>
<VCard> <VCard>
<VCardTitle class="d-flex justify-space-between"> <VCardTitle class="d-flex justify-space-between">
<span class="mt-2">#{{ store.current.block?.header?.height }}</span> <span class="mt-2">#{{ store.current.block?.header?.height }}</span>
<span v-if="props.height" class="mt-2"> <span v-if="props.height" class="mt-2">
<VBtn size="32" :to="`/${store.blockchain.chainName}/block/${height - 1}`" class="mr-2"><VIcon icon="mdi-arrow-left"/></VBtn> <VBtn
<VBtn size="32" :to="`/${store.blockchain.chainName}/block/${height + 1}`"><VIcon icon="mdi-arrow-right"/></VBtn> size="32"
</span> :to="`/${store.blockchain.chainName}/block/${height - 1}`"
</VCardTitle> class="mr-2"
<VCardItem class="pt-0"> ><VIcon icon="mdi-arrow-left"
<DynamicComponent :value="store.current.block_id"/> /></VBtn>
</VCardItem> <VBtn
size="32"
:to="`/${store.blockchain.chainName}/block/${height + 1}`"
><VIcon icon="mdi-arrow-right"
/></VBtn>
</span>
</VCardTitle>
<VCardItem class="pt-0">
<DynamicComponent :value="store.current.block_id" />
</VCardItem>
</VCard> </VCard>
<VCard title="Block Header" class="my-5"> <VCard title="Block Header" class="my-5">
<VCardItem class="pt-0"> <VCardItem class="pt-0">
<DynamicComponent :value="store.current.block?.header"/> <DynamicComponent :value="store.current.block?.header" />
</VCardItem> </VCardItem>
</VCard> </VCard>
<VCard title="Transactions"> <VCard title="Transactions">
<VCardItem class="pt-0"> <VCardItem class="pt-0">
<TxsElement :value="store.current.block?.data?.txs"/> <TxsElement :value="store.current.block?.data?.txs" />
</VCardItem> </VCardItem>
</VCard> </VCard>
<VCard title="Last Commit" class="mt-5"> <VCard title="Last Commit" class="mt-5">
<VCardItem class="pt-0"> <VCardItem class="pt-0">
<DynamicComponent :value="store.current.block?.last_commit"/> <DynamicComponent :value="store.current.block?.last_commit" />
</VCardItem> </VCardItem>
</VCard> </VCard>
</div> </div>
</template> </template>

View File

@ -11,76 +11,82 @@ const tab = ref('blocks');
const format = useFormatter(); const format = useFormatter();
</script> </script>
<template> <template>
<VCard> <div>
<VCardTitle class="d-flex justify-space-between"> <div class="tabs tabs-boxed bg-transparent mb-4">
<VTabs v-model="tab"> <a
<VTab value="blocks">Blocks</VTab> class="tab text-gray-400 uppercase"
<VTab value="transactions">Transactions</VTab> :class="{ 'tab-active': tab === 'blocks' }"
</VTabs> @click="tab = 'blocks'"
</VCardTitle> >Blocks</a
<VWindow v-model="tab"> >
<VWindowItem value="blocks"> <a
<VTable> class="tab text-gray-400 uppercase"
<thead> :class="{ 'tab-active': tab === 'transactions' }"
<tr> @click="tab = 'transactions'"
<th>Height</th> >Transactions</a
<th>Hash</th> >
<th>Proposer</th> </div>
<th>Txs</th>
<th>Time</th>
</tr>
</thead>
<tbody>
<tr v-for="item in store.recents">
<td class="text-sm text-primary">
<RouterLink
:to="`/${props.chain}/block/${item.block?.header?.height}`"
>{{ item.block?.header?.height }}</RouterLink
>
</td>
<td>{{ item.block_id?.hash }}</td>
<td>
{{ format.validator(item.block?.header?.proposer_address) }}
</td>
<td>{{ item.block?.data?.txs.length }}</td>
<td>{{ format.toDay(item.block?.header?.time, 'from') }}</td>
</tr>
</tbody>
</VTable>
</VWindowItem>
<VWindowItem value="transactions">
<VTable>
<thead>
<tr>
<th>Hash</th>
<th>Messages</th>
<th>Fees</th>
</tr>
</thead>
<tbody>
<tr v-for="item in store.txsInRecents">
<td>
<RouterLink :to="`/${props.chain}/tx/${item.hash}`">{{
item.hash
}}</RouterLink>
</td>
<td>{{ format.messages(item.tx.body.messages) }}</td>
<td>{{ format.formatTokens(item.tx.authInfo.fee?.amount) }}</td>
</tr>
</tbody>
</VTable>
<VCardItem> <div v-if="tab === 'blocks'" class="bg-base-100 rounded">
<v-alert <VTable>
type="info" <thead>
text="Only show txs in recent blocks" <tr>
variant="tonal" <th>Height</th>
></v-alert> <th>Hash</th>
</VCardItem> <th>Proposer</th>
</VWindowItem> <th>Txs</th>
</VWindow> <th>Time</th>
<VCardActions> </VCardActions> </tr>
</VCard> </thead>
<tbody>
<tr v-for="item in store.recents">
<td class="text-sm text-primary">
<RouterLink
:to="`/${props.chain}/block/${item.block?.header?.height}`"
>{{ item.block?.header?.height }}</RouterLink
>
</td>
<td>{{ item.block_id?.hash }}</td>
<td>
{{ format.validator(item.block?.header?.proposer_address) }}
</td>
<td>{{ item.block?.data?.txs.length }}</td>
<td>{{ format.toDay(item.block?.header?.time, 'from') }}</td>
</tr>
</tbody>
</VTable>
</div>
<div class="bg-base-100 rounded" v-if="tab === 'transactions'">
<VTable>
<thead>
<tr>
<th>Hash</th>
<th>Messages</th>
<th>Fees</th>
</tr>
</thead>
<tbody>
<tr v-for="item in store.txsInRecents">
<td>
<RouterLink :to="`/${props.chain}/tx/${item.hash}`">{{
item.hash
}}</RouterLink>
</td>
<td>{{ format.messages(item.tx.body.messages) }}</td>
<td>{{ format.formatTokens(item.tx.authInfo.fee?.amount) }}</td>
</tr>
</tbody>
</VTable>
<div class="p-4">
<v-alert
type="info"
text="Only show txs in recent blocks"
variant="tonal"
></v-alert>
</div>
</div>
</div>
</template> </template>
<route> <route>

View File

@ -100,8 +100,11 @@ const total = computed(()=> {
}) })
const turnout = computed(() => { const turnout = computed(() => {
if (total.value > 0) {
const bonded = useStakingStore().pool?.bonded_tokens || "1" const bonded = useStakingStore().pool?.bonded_tokens || "1"
return format.percent(total.value / Number(bonded)) return format.percent(total.value / Number(bonded))
}
return 0
}) })
const yes = computed(()=> { const yes = computed(()=> {
@ -135,75 +138,58 @@ const abstain = computed(()=> {
} }
return 0 return 0
}) })
const processList = computed(()=>{
return [
{name: 'Turnout', value : turnout.value, class: 'bg-info' },
{name: 'Yes', value : yes.value, class: 'bg-success' },
{name: 'No', value : no.value, class: 'bg-error' },
{name: 'No With Veto', value : veto.value, class: 'bg-primary' },
{name: 'Abstain', value : abstain.value, class: 'bg-warning' }
]
})
</script> </script>
<template> <template>
<div> <div>
<VCard> <div class="bg-base-100 px-4 pt-3 pb-4 rounded mb-4 shadow">
<VCardItem> <h2 class="card-title flex flex-col md:justify-between md:flex-row">
<VCardTitle> <p class="truncate w-full">{{ proposal_id }}. {{ proposal.content?.title }} </p>
{{ proposal_id }}. {{ proposal.content?.title }} <VChip label :color="color" class="float-right">{{ status }}</VChip> <div
</VCardTitle> class="badge badge-ghost"
:class="
color === 'success'
? 'text-yes'
: color === 'error'
? 'text-no'
: 'text-info'
"
>{{ status }}</div>
</h2>
<div class="">
<ObjectElement :value="proposal.content"/> <ObjectElement :value="proposal.content"/>
</VCardItem> </div>
</VCard> </div>
<!-- grid lg:grid-cols-3 auto-rows-max-->
<VRow class="my-5"> <!-- flex-col lg:flex-row flex -->
<VCol cols=12 md="4"> <div class="gap-4 mb-4 grid lg:grid-cols-3 auto-rows-max ">
<VCard class="h-100"> <!-- flex-1 -->
<VCardItem> <div class="bg-base-100 px-4 pt-3 pb-4 rounded shadow ">
<VCardTitle>Tally</VCardTitle> <h2 class="card-title">Tally</h2>
<label>Turnout</label> <div v-for="(item,index) of processList" :key="index">
<v-progress-linear <label class="block">{{item.name }}</label>
:model-value="turnout" <div class="h-6 w-full relative">
height="25" <div class="absolute inset-x-0 inset-y-0 w-full opacity-10" :class="`${item.class}`"></div>
color="info" <div class="absolute inset-x-0 inset-y-0" :class="`${item.class}`" :style="`width: ${item.value}`"></div>
> <strong class="absolute inset-x-0 inset-y-0 text-center">{{ item.value }}</strong>
<strong>{{ turnout }}</strong> </div>
</v-progress-linear> </div>
<label>Yes</label> </div>
<v-progress-linear <!-- lg:col-span-2 -->
:model-value="yes" <!-- lg:flex-[2_2_0%] -->
height="25" <div class="h-max bg-base-100 px-4 pt-3 pb-4 rounded shadow lg:col-span-2">
color="success" <h2 class="card-title">Timeline</h2>
> <VTimeline
<strong>{{ yes }}</strong>
</v-progress-linear>
<label>No</label>
<v-progress-linear
:model-value="no"
height="25"
color="error"
>
<strong>{{ no }}</strong>
</v-progress-linear>
<label>No With Veto</label>
<v-progress-linear
:model-value="veto"
height="25"
color="primary"
>
<strong>{{ veto }}</strong>
</v-progress-linear>
<label>Abstain</label>
<v-progress-linear
:model-value="abstain"
height="25"
color="dark"
>
<strong>{{ abstain }}</strong>
</v-progress-linear>
</VCardItem>
</VCard>
</VCol>
<VCol cols=12 md="8">
<VCard>
<VCardItem>
<VCardTitle>
Timeline
</VCardTitle>
<VTimeline
class="mt-2" class="mt-2"
side="end" side="end"
align="start" align="start"
@ -301,12 +287,23 @@ const abstain = computed(()=> {
</p> </p>
</VTimelineItem> </VTimelineItem>
</VTimeline> </VTimeline>
</VCardItem>
</VCard>
</VCol>
</VRow>
<VCard> </div>
</div>
<div class="bg-base-100 px-4 pt-3 pb-4 rounded mb-4 shadow">
<h2 class="card-title">Votes</h2>
<table class="table w-full ">
<tbody>
<tr v-for="(item,index) of votes" :key="index">
<td>{{ item.voter }}</td>
<td>{{ item.option }}</td>
</tr>
</tbody>
</table>
<VBtn v-if="votePage.next_key" block variant="outlined" @click="loadMore()" :disabled="loading">Load more</VBtn>
</div>
<!-- <VCard>
<VCardItem> <VCardItem>
<VCardTitle> <VCardTitle>
Votes Votes
@ -321,6 +318,6 @@ const abstain = computed(()=> {
</VTable> </VTable>
<VBtn v-if="votePage.next_key" block variant="outlined" @click="loadMore()" :disabled="loading">Load more</VBtn> <VBtn v-if="votePage.next_key" block variant="outlined" @click="loadMore()" :disabled="loading">Load more</VBtn>
</VCardItem> </VCardItem>
</VCard> </VCard> -->
</div> </div>
</template> </template>

View File

@ -2,7 +2,7 @@
import { useGovStore } from '@/stores'; import { useGovStore } from '@/stores';
import ProposalListItem from '@/components/ProposalListItem.vue'; import ProposalListItem from '@/components/ProposalListItem.vue';
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
const tab = ref(''); const tab = ref('2');
const store = useGovStore(); const store = useGovStore();
onMounted(() => { onMounted(() => {
@ -13,33 +13,41 @@ onMounted(() => {
} }
}); });
}); });
const changeTab = (val: '2' | '3' | '4') => {
tab.value = val;
store.fetchProposals(val);
};
</script> </script>
<template> <template>
<div> <div>
<VTabs v-model="tab" class="v-tabs-pill"> <div class="tabs tabs-boxed bg-transparent mb-4">
<VTab value="2">Voting</VTab> <a
<VTab value="3" @click="store.fetchProposals('3')">Passed</VTab> class="tab text-gray-400 uppercase"
<VTab value="4" @click="store.fetchProposals('4')">Rejected</VTab> :class="{ 'tab-active': tab === '2' }"
</VTabs> @click="changeTab('2')"
<VWindow v-model="tab" class="mt-5"> >Voting</a
<VWindowItem value="2"> >
<ProposalListItem :proposals="store?.proposals['2']" /> <a
</VWindowItem> class="tab text-gray-400 uppercase"
:class="{ 'tab-active': tab === '3' }"
<VWindowItem value="3"> @click="changeTab('3')"
<ProposalListItem :proposals="store?.proposals['3']" /> >Passed</a
</VWindowItem> >
<a
<VWindowItem value="4"> class="tab text-gray-400 uppercase"
<ProposalListItem :proposals="store?.proposals['4']" /> :class="{ 'tab-active': tab === '4' }"
</VWindowItem> @click="changeTab('4')"
</VWindow> >Rejected</a
>
</div>
<ProposalListItem :proposals="store?.proposals[tab]" />
</div> </div>
</template> </template>
<route> <route>
{ {
meta: { meta: {
i18n: 'governance' i18n: 'governance'
}
} }
</route> }
</route>

View File

@ -14,7 +14,7 @@ onMounted(() => {
<template> <template>
<div class="overflow-hidden"> <div class="overflow-hidden">
<!-- Chain ID --> <!-- Chain ID -->
<div class="bg-card px-4 pt-3 pb-4 rounded"> <div class="bg-base-100 px-4 pt-3 pb-4 rounded">
<div class="text-base mb-3 text-main">{{ chain.title }}</div> <div class="text-base mb-3 text-main">{{ chain.title }}</div>
<div <div
class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 2xl:grid-cols-6 gap-4" class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 2xl:grid-cols-6 gap-4"
@ -40,13 +40,13 @@ onMounted(() => {
<!-- Slashing Parameters --> <!-- Slashing Parameters -->
<CardParameter :cardItem="store.slashing"/> <CardParameter :cardItem="store.slashing"/>
<!-- Application Version --> <!-- Application Version -->
<div class="bg-card px-4 pt-3 pb-4 rounded-sm mt-6"> <div class="bg-base-100 px-4 pt-3 pb-4 rounded-sm mt-6">
<div class="text-base mb-3 text-main">{{ store.appVersion?.title }}</div> <div class="text-base mb-3 text-main">{{ store.appVersion?.title }}</div>
<ArrayObjectElement :value="store.appVersion?.items" :thead="false"/> <ArrayObjectElement :value="store.appVersion?.items" :thead="false"/>
</div> </div>
<!-- Node Information --> <!-- Node Information -->
<div class="bg-card px-4 pt-3 pb-4ß rounded-sm mt-6"> <div class="bg-base-100 px-4 pt-3 pb-4ß rounded-sm mt-6">
<div class="text-base mb-3 text-main">{{ store.nodeVersion?.title }}</div> <div class="text-base mb-3 text-main">{{ store.nodeVersion?.title }}</div>
<ArrayObjectElement :value="store.nodeVersion?.items" :thead="false"/> <ArrayObjectElement :value="store.nodeVersion?.items" :thead="false"/>
</div> </div>

View File

@ -1,30 +1,30 @@
<script lang=ts setup> <script lang="ts" setup>
import { useBaseStore, useFormatter, useStakingStore } from '@/stores'; import { useBaseStore, useFormatter, useStakingStore } from '@/stores';
import { toBase64, toHex } from '@cosmjs/encoding'; import { toBase64, toHex } from '@cosmjs/encoding';
import { computed } from '@vue/reactivity'; import { computed } from '@vue/reactivity';
import { onMounted, ref, type DebuggerEvent } from 'vue'; import { onMounted, ref, type DebuggerEvent } from 'vue';
import { consensusPubkeyToHexAddress } from '@/libs' import { consensusPubkeyToHexAddress } from '@/libs';
import type { Key, Validator } from '@/types'; import type { Key, Validator } from '@/types';
const staking = useStakingStore() const staking = useStakingStore();
const format = useFormatter() const format = useFormatter();
const cache = JSON.parse(localStorage.getItem('avatars')||'{}') const cache = JSON.parse(localStorage.getItem('avatars') || '{}');
const avatars = ref( cache || {} ) const avatars = ref(cache || {});
const latest = ref({} as Record<string, number>) const latest = ref({} as Record<string, number>);
const yesterday = ref({} as Record<string, number>) const yesterday = ref({} as Record<string, number>);
const tab = ref('active') const tab = ref('active');
const unbondList = ref([] as Validator[]) const unbondList = ref([] as Validator[]);
const base = useBaseStore() const base = useBaseStore();
onMounted(()=> { onMounted(() => {
fetchChange(0) fetchChange(0);
staking.fetchInacitveValdiators().then(x => { staking.fetchInacitveValdiators().then((x) => {
unbondList.value = x unbondList.value = x;
}) });
}) });
function fetchChange(offset: number) { function fetchChange(offset: number) {
const base = useBaseStore() const base = useBaseStore();
const diff = 86400000 / base.blocktime const diff = 86400000 / base.blocktime;
// base.fetchAbciInfo().then(h => { // base.fetchAbciInfo().then(h => {
// // console.log('block:', h) // // console.log('block:', h)
// base.fetchValidatorByHeight(h.lastBlockHeight, offset).then(x => { // base.fetchValidatorByHeight(h.lastBlockHeight, offset).then(x => {
@ -44,190 +44,233 @@ function fetchChange(offset: number) {
const change24 = (key: Key) => { const change24 = (key: Key) => {
// console.log('hex key:', consensusPubkeyToHexAddress(key)) // console.log('hex key:', consensusPubkeyToHexAddress(key))
const txt = key.key const txt = key.key;
const n : number = latest.value[txt]; const n: number = latest.value[txt];
const o : number = yesterday.value[txt] const o: number = yesterday.value[txt];
// console.log( txt, n, o) // console.log( txt, n, o)
return n >0 && o > 0 ? n - o : 0 return n > 0 && o > 0 ? n - o : 0;
} };
const change24Text = (key?: Key) => { const change24Text = (key?: Key) => {
if(!key) return '' if (!key) return '';
const v = change24(key) const v = change24(key);
return v!==0 ? format.numberAndSign(v) : '' return v !== 0 ? format.numberAndSign(v) : '';
} };
const change24Color = (key?: Key) => { const change24Color = (key?: Key) => {
if(!key) return '' if (!key) return '';
const v = change24(key) const v = change24(key);
if(v > 0) return 'text-success' if (v > 0) return 'text-success';
if(v < 0) return 'text-error' if (v < 0) return 'text-error';
} };
const update = (m: DebuggerEvent) => { const update = (m: DebuggerEvent) => {
if(m.key === 'validators') { if (m.key === 'validators') {
loadAvatars() loadAvatars();
} }
} };
const list = computed(() => { const list = computed(() => {
return tab.value === 'active' ? staking.validators: unbondList.value return tab.value === 'active' ? staking.validators : unbondList.value;
// return staking.validators // return staking.validators
}) });
const loadAvatars = () => { const loadAvatars = () => {
// fetch avatar from keybase // fetch avatar from keybase
let promise = Promise.resolve() let promise = Promise.resolve();
staking.validators.forEach(item => { staking.validators.forEach((item) => {
promise = promise.then(() => new Promise(resolve => { promise = promise.then(
const identity = item.description?.identity () =>
if(identity && !avatars.value[identity]){ new Promise((resolve) => {
staking.keybase(identity).then(d => { const identity = item.description?.identity;
if (Array.isArray(d.them) && d.them.length > 0) { if (identity && !avatars.value[identity]) {
const uri = String(d.them[0]?.pictures?.primary?.url).replace("https://s3.amazonaws.com/keybase_processed_uploads/", "") staking.keybase(identity).then((d) => {
if(uri) { if (Array.isArray(d.them) && d.them.length > 0) {
avatars.value[identity] = uri const uri = String(d.them[0]?.pictures?.primary?.url).replace(
localStorage.setItem('avatars', JSON.stringify(avatars.value)) 'https://s3.amazonaws.com/keybase_processed_uploads/',
} ''
);
if (uri) {
avatars.value[identity] = uri;
localStorage.setItem(
'avatars',
JSON.stringify(avatars.value)
);
} }
resolve() }
}) resolve();
}else{ });
resolve() } else {
} resolve();
})) }
})
}
staking.$subscribe((m, s)=> {
if (Array.isArray(m.events)) {
m.events.forEach(x => {
update(x)
}) })
} else { );
update(m.events) });
} };
})
const logo = (identity?: string) => {
if(!identity) return ''
const url = avatars.value[identity] || ''
return url.startsWith('http')? url: `https://s3.amazonaws.com/keybase_processed_uploads/${url}`
}
const rank = function(position: number) {
let sum = 0
for(let i = 0;i < position; i++) {
sum += Number(staking.validators[i]?.delegator_shares)
}
const percent = (sum / staking.totalPower)
switch (true) { staking.$subscribe((m, s) => {
case tab.value ==='active' && percent < 0.33: return 'error' if (Array.isArray(m.events)) {
case tab.value ==='active' && percent < 0.67: return 'warning' m.events.forEach((x) => {
default: return 'primary' update(x);
} });
} } else {
update(m.events);
}
});
const logo = (identity?: string) => {
if (!identity) return '';
const url = avatars.value[identity] || '';
return url.startsWith('http')
? url
: `https://s3.amazonaws.com/keybase_processed_uploads/${url}`;
};
const rank = function (position: number) {
let sum = 0;
for (let i = 0; i < position; i++) {
sum += Number(staking.validators[i]?.delegator_shares);
}
const percent = sum / staking.totalPower;
switch (true) {
case tab.value === 'active' && percent < 0.33:
return 'error';
case tab.value === 'active' && percent < 0.67:
return 'warning';
default:
return 'primary';
}
};
</script> </script>
<template> <template>
<div>
<div class="flex items-center justify-between">
<div class="tabs tabs-boxed bg-transparent mb-4">
<a
class="tab text-gray-400"
:class="{ 'tab-active': tab === 'active' }"
@click="tab = 'active'"
>Active</a
>
<a
class="tab text-gray-400"
:class="{ 'tab-active': tab === 'inactive' }"
@click="tab = 'inactive'"
>Inactive</a
>
</div>
<div class="text-lg font-semibold">
{{ list.length }}/{{ staking.params.max_validators }}
</div>
</div>
<div> <div>
<VCard> <VCard>
<VCardTitle class="d-flex justify-space-between">
<VBtnToggle v-model="tab" size="small" color="primary">
<VBtn value="active" variant="outlined" >Active</VBtn>
<VBtn value="inactive" variant="outlined">Inactive</VBtn>
</VBtnToggle>
<span class="mt-2">{{ list.length }}/{{ staking.params.max_validators }}</span>
</VCardTitle>
<VTable class="text-no-wrap table-header-bg rounded-0"> <VTable class="text-no-wrap table-header-bg rounded-0">
<thead> <thead>
<tr> <tr>
<th <th scope="col" style="width: 3rem">#</th>
scope="col" <th scope="col">VALIDATOR</th>
style="width: 3rem;" <th scope="col" class="text-right">VOTING POWER</th>
>#</th> <th scope="col" class="text-right">24h CHANGES</th>
<th scope="col"> <th scope="col" class="text-right">COMMISSION</th>
VALIDATOR <th scope="col">ACTIONS</th>
</th> </tr>
<th scope="col" class="text-right"> </thead>
VOTING POWER <tbody>
</th> <tr v-for="(v, i) in list" :key="v.operator_address">
<th scope="col" class="text-right"> <!-- 👉 rank -->
24h CHANGES <td>
</th> <VChip label :color="rank(i)">
<th scope="col" class="text-right"> {{ i + 1 }}
COMMISSION </VChip>
</th> </td>
<th scope="col">
ACTIONS
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(v, i) in list"
:key="v.operator_address"
>
<!-- 👉 rank -->
<td>
<VChip label :color="rank(i)">
{{ i + 1 }}
</VChip>
</td>
<!-- 👉 Validator --> <!-- 👉 Validator -->
<td> <td>
<div class="d-flex align-center overflow-hidden" style="max-width: 400px;"> <div
<VAvatar class="d-flex align-center overflow-hidden"
variant="tonal" style="max-width: 400px"
class="me-3" >
size="34" <VAvatar
icon="mdi-help-circle-outline" variant="tonal"
:image="logo(v.description?.identity)" class="me-3"
/> size="34"
<div class="d-flex flex-column"> icon="mdi-help-circle-outline"
<h6 class="text-sm text-primary"> :image="logo(v.description?.identity)"
<RouterLink />
:to="{name: 'chain-staking-validator', params: {validator: v.operator_address}}" <div class="d-flex flex-column">
class="font-weight-medium user-list-name" <h6 class="text-sm text-primary">
> <RouterLink
{{ v.description?.moniker }} :to="{
</RouterLink> name: 'chain-staking-validator',
params: { validator: v.operator_address },
</h6> }"
<span class="text-xs">{{ v.description?.website || v.description?.identity || '-' }}</span> class="font-weight-medium user-list-name"
>
{{ v.description?.moniker }}
</RouterLink>
</h6>
<span class="text-xs">{{
v.description?.website || v.description?.identity || '-'
}}</span>
</div>
</div> </div>
</div> </td>
</td>
<!-- 👉 Voting Power --> <!-- 👉 Voting Power -->
<td class="text-right"> <td class="text-right">
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<h6 class="text-sm font-weight-medium"> <h6 class="text-sm font-weight-medium">
{{ format.formatToken( {amount: parseInt(v.tokens).toString(), denom: staking.params.bond_denom }, true, "0,0") }} {{
format.formatToken(
{
amount: parseInt(v.tokens).toString(),
denom: staking.params.bond_denom,
},
true,
'0,0'
)
}}
</h6> </h6>
<span class="text-xs">{{ format.calculatePercent(v.delegator_shares, staking.totalPower) }}</span> <span class="text-xs">{{
format.calculatePercent(
v.delegator_shares,
staking.totalPower
)
}}</span>
</div> </div>
</td> </td>
<!-- 👉 24h Changes --> <!-- 👉 24h Changes -->
<td class="text-right text-xs" :class="change24Color(v.consensus_pubkey)"> <td
{{ change24Text(v.consensus_pubkey) }} <VChip label v-if="v.jailed" color="error">Jailed</VChip> class="text-right text-xs"
</td> :class="change24Color(v.consensus_pubkey)"
<!-- 👉 commission --> >
<td class="text-right"> {{ change24Text(v.consensus_pubkey) }}
{{ format.formatCommissionRate(v.commission?.commission_rates?.rate) }} <VChip label v-if="v.jailed" color="error">Jailed</VChip>
</td> </td>
<!-- 👉 Action --> <!-- 👉 commission -->
<td> <td class="text-right">
{{ 2 }} {{
</td> format.formatCommissionRate(
v.commission?.commission_rates?.rate
)
}}
</td>
<!-- 👉 Action -->
<td>
{{ 2 }}
</td>
</tr> </tr>
</tbody> </tbody>
</VTable> </VTable>
<VDivider/> <VDivider />
<VCardActions class="py-2"> <VCardActions class="py-2">
<VChip label color="error">Top 33%</VChip> <VChip label color="warning" class="mx-2">Top 67%</VChip> <VChip label color="error">Top 33%</VChip>
<VChip label color="warning" class="mx-2">Top 67%</VChip>
</VCardActions> </VCardActions>
</VCard> </VCard>
</div> </div>
</div>
</template> </template>
<route> <route>

View File

@ -1,24 +1,29 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useDashboard, LoadingStatus, type ChainConfig } from '@/stores/useDashboard'; import {
useDashboard,
LoadingStatus,
type ChainConfig,
} from '@/stores/useDashboard';
import ChainSummary from '@/components/ChainSummary.vue'; import ChainSummary from '@/components/ChainSummary.vue';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useBlockchain } from '@/stores'; import { useBlockchain } from '@/stores';
const dashboard = useDashboard() const dashboard = useDashboard();
dashboard.$subscribe((mutation, state) => { dashboard.$subscribe((mutation, state) => {
localStorage.setItem('favorite', JSON.stringify(state.favorite)) localStorage.setItem('favorite', JSON.stringify(state.favorite));
}) });
const keywords = ref('') const keywords = ref('');
const chains = computed(() => { const chains = computed(() => {
if (keywords.value) { if (keywords.value) {
return Object.values(dashboard.chains).filter((x: ChainConfig) => x.chainName.indexOf(keywords.value) > -1) return Object.values(dashboard.chains).filter(
(x: ChainConfig) => x.chainName.indexOf(keywords.value) > -1
);
} else { } else {
return Object.values(dashboard.chains) return Object.values(dashboard.chains);
} }
}) });
const chain = useBlockchain() const chain = useBlockchain();
</script> </script>
<template> <template>
<div class=""> <div class="">
@ -29,29 +34,43 @@ const chain = useBlockchain()
<h1 class="text-primary text-3xl md:text-6xl font-bold mr-2"> <h1 class="text-primary text-3xl md:text-6xl font-bold mr-2">
Ping dashboard Ping dashboard
</h1> </h1>
<div class="badge badge-info badge-outline mt-1 text-sm md:mt-8">Beta</div> <div class="badge badge-info badge-outline mt-1 text-sm md:mt-8">
Beta
</div>
</div> </div>
<div class="text-center text-base"> <div class="text-center text-base">
<p class="mb-1"> <p class="mb-1">
{{ $t('index.slogan') }} {{ $t('index.slogan') }}
</p> </p>
<h2 class="mb-6"> <h2 class="mb-6">Cosmos Ecosystem Blockchains 🚀</h2>
Cosmos Ecosystem Blockchains 🚀 </div>
</h2> <div
v-if="dashboard.status !== LoadingStatus.Loaded"
class="flex justify-center"
>
<progress class="progress progress-info w-80 h-1"></progress>
</div> </div>
<div v-if="dashboard.status !== LoadingStatus.Loaded" class="flex justify-center"><progress
class="progress progress-info w-80 h-1"></progress></div>
<VTextField v-model="keywords" variant="underlined" :placeholder="$t('index.search_placeholder')" <VTextField
style="max-width: 300px;" app> v-model="keywords"
variant="underlined"
:placeholder="$t('index.search_placeholder')"
style="max-width: 300px"
app
>
<template #append-inner> <template #append-inner>
{{ chains.length }}/{{ dashboard.length }} {{ chains.length }}/{{ dashboard.length }}
</template> </template>
</VTextField> </VTextField>
<div class="grid grid-cols-2 gap-4 mt-6 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-5"> <div
<ChainSummary v-for="(chain, index) in chains" :key="index" :name="chain.chainName" /> class="grid grid-cols-2 gap-4 mt-6 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-5"
>
<ChainSummary
v-for="(chain, index) in chains"
:key="index"
:name="chain.chainName"
/>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from '@iconify/vue'; import { Icon } from '@iconify/vue';
import { defineEmits, ref, watch } from 'vue'
import type { Anchor } from 'vuetify/lib/components'; import type { Anchor } from 'vuetify/lib/components';
import type { I18nLanguage } from '@layouts/types'; import type { I18nLanguage } from '@layouts/types';
@ -7,7 +8,7 @@ const props = withDefaults(defineProps<Props>(), {
location: 'bottom end', location: 'bottom end',
}); });
defineEmits<{ const emit = defineEmits<{
(e: 'change', id: string): void; (e: 'change', id: string): void;
}>(); }>();
@ -16,30 +17,37 @@ interface Props {
location?: Anchor; location?: Anchor;
} }
const { locale } = useI18n({ useScope: 'global' }); let locale = ref(useI18n({ useScope: 'global' }).locale)
watch(locale, (val: string) => {
watch(locale, (val) => {
document.documentElement.setAttribute('lang', val as string); document.documentElement.setAttribute('lang', val as string);
}); });
const currentLang = ref([localStorage.getItem('lang') || 'en']); let currentLang = ref(localStorage.getItem('lang') || 'en');
function changeLang(lang: string){
locale.value = lang
currentLang.value = lang
emit('change', lang)
}
</script> </script>
<template> <template>
<div class="dropdown dropdown-end"> <div
class="dropdown"
:class="currentLang === 'ar'?'dropdown-right': 'dropdown-bottom dropdown-end'"
>
<label tabindex="0" class="btn btn-ghost btn-circle btn-sm mx-1"> <label tabindex="0" class="btn btn-ghost btn-circle btn-sm mx-1">
<Icon icon="mdi-translate" style="font-size: 24px" /> <Icon icon="mdi-translate" class="text-2xl" />
</label> </label>
<ul <ul
tabindex="0" tabindex="0"
class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-40"
> >
<li v-for="lang in props.languages" :key="lang.i18nLang"> <li v-for="lang in props.languages" :key="lang.i18nLang">
<a <a
@click=" class="hover:bg-base-content"
locale = lang.i18nLang; :class="{ 'text-primary': currentLang === lang.i18nLang }"
$emit('change', lang.i18nLang); @click="changeLang(lang.i18nLang)"
"
>{{ lang.label }}</a >{{ lang.label }}</a
> >
</li> </li>

View File

@ -6,16 +6,12 @@
:root { :root {
--text-main: #333; --text-main: #333;
--text-secondary: #4b525d; --text-secondary: #4b525d;
--bg-card: #fff;
--bg-active: #fbfbfc; --bg-active: #fbfbfc;
--bg-hover: #eee;
} }
html.dark { html.dark {
--text-main: #f7f7f7; --text-main: #f7f7f7;
--text-secondary: #6f6e84; --text-secondary: #6f6e84;
--bg-card: #28334e;
--bg-active: #242b40; --bg-active: #242b40;
--bg-hover: #303044;
} }
} }

View File

@ -11,34 +11,30 @@ module.exports = {
primary: '#666cff', primary: '#666cff',
main: 'var(--text-main)', main: 'var(--text-main)',
secondary: 'var(--text-secondary)', secondary: 'var(--text-secondary)',
card: 'var(--bg-card)',
hover: 'var(--bg-hover)',
active: 'var(--bg-active)', active: 'var(--bg-active)',
}, },
}, },
}, },
plugins: [ plugins: [require('daisyui')],
require("daisyui")
],
daisyui: { daisyui: {
themes: [ themes: [
{
myTheme: {
info: "#666CFF",
}
},
{ {
light: { light: {
...require("daisyui/src/colors/themes")["[data-theme=light]"], ...require('daisyui/src/colors/themes')['[data-theme=light]'],
info: "#666CFF", primary: '#666cff',
} info: '#666CFF',
'base-content': '#e9eaeb'
},
}, },
{ {
dark: { dark: {
...require("daisyui/src/colors/themes")["[data-theme=dark]"], ...require('daisyui/src/colors/themes')['[data-theme=dark]'],
info: "#666CFF", primary: '#666cff',
} info: '#666CFF',
'base-100': '#2a334c',
'base-content': '#373f57'
},
}, },
], ],
} },
}; };