forked from cerc-io/cosmos-explorer
Merge pull request #365 from alisaweb3/v3-single
Parameters Page: data docking, perfect page content, mobile adaptation
This commit is contained in:
commit
016f022e27
@ -76,7 +76,7 @@
|
|||||||
"unplugin-auto-import": "^0.13.0",
|
"unplugin-auto-import": "^0.13.0",
|
||||||
"unplugin-vue-components": "^0.23.0",
|
"unplugin-vue-components": "^0.23.0",
|
||||||
"unplugin-vue-define-options": "1.1.4",
|
"unplugin-vue-define-options": "1.1.4",
|
||||||
"vite": "^4.3.1",
|
"vite": "^4.3.3",
|
||||||
"vite-plugin-pages": "^0.28.0",
|
"vite-plugin-pages": "^0.28.0",
|
||||||
"vue-tsc": "^1.0.12"
|
"vue-tsc": "^1.0.12"
|
||||||
}
|
}
|
||||||
|
@ -2,20 +2,26 @@
|
|||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue';
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
cardItem: {
|
cardItem: {
|
||||||
type: Object as PropType<{title: string;items: Array<any>;}>,
|
type: Object as PropType<{ title: string; items: Array<any> }>,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-card px-4 pt-3 pb-4 rounded-sm mt-6"
|
<div
|
||||||
v-if="props.cardItem?.items && props.cardItem?.items?.length > 0">
|
class="bg-card px-4 pt-3 pb-4 rounded mt-5"
|
||||||
|
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>
|
||||||
<div class="grid grid-cols-5 gap-4">
|
<div
|
||||||
<div v-for="(item,index) of props.cardItem?.items" :key="index" class="rounded-sm bg-active px-4 py-2">
|
class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 2xl:grid-cols-6 gap-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(item, index) of props.cardItem?.items"
|
||||||
|
:key="index"
|
||||||
|
class="rounded-sm bg-active px-4 py-2"
|
||||||
|
>
|
||||||
<div class="text-xs mb-2 text-secondary">{{ item?.subtitle }}</div>
|
<div class="text-xs mb-2 text-secondary">{{ item?.subtitle }}</div>
|
||||||
<div class="text-base text-main">{{ item?.value }}</div>
|
<div class="text-base text-main">{{ Array.isArray(item?.value) ? (item?.value[0] && item?.value[0].amount)|| '-':`${Number(item?.value)}` === 'NaN' ? item?.value : Number(item?.value)}}</div>
|
||||||
<!-- {{ item }} -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { PropType } from 'vue';
|
|
||||||
const props = defineProps({
|
|
||||||
tableItem: {
|
|
||||||
type: Object as PropType<{title: string;items: Object;}>,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
function formatTitle (name: string){
|
|
||||||
return String(name).replaceAll('_', ' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<div class="bg-card px-4 pt-3 pb-4 rounded-sm mt-6">
|
|
||||||
<div class="text-base mb-3 text-main">{{ props.tableItem?.title }}</div>
|
|
||||||
<div class="">
|
|
||||||
<div class="d-flex flex-nowrap" v-for="(item,index ) of props.tableItem?.items" :key="index">
|
|
||||||
<div class="mr-6">{{ formatTitle(item?.subtitle) }}</div>
|
|
||||||
<div class="flex-1" >{{ item?.value }}</div>
|
|
||||||
<div>{{typeof(item?.value )}}</div>
|
|
||||||
<!-- {{ item }} -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
@ -3,7 +3,11 @@ import { computed } from '@vue/reactivity';
|
|||||||
import DynamicComponent from './DynamicComponent.vue';
|
import DynamicComponent from './DynamicComponent.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
value: { type: Array<Object>},
|
value: { type: null as any },
|
||||||
|
thead: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const header = computed(() => {
|
const header = computed(() => {
|
||||||
@ -15,16 +19,20 @@ const header = computed(() => {
|
|||||||
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VTable v-if="header.length > 0" density="compact" height="300px" fixed-header>
|
<VTable v-if="header.length > 0" density="compact" height="300px" fixed-header hover>
|
||||||
<thead>
|
<thead v-if="thead">
|
||||||
<tr>
|
<tr>
|
||||||
<th v-for="k in header" class="text-left text-capitalize">{{ k }}</th>
|
<th v-for="(item, index) in header" :key="index" class="text-left text-capitalize">{{ item }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="v in props.value">
|
<tr v-for="(item, index) in value" :key="index">
|
||||||
<td v-for="k in header"> <DynamicComponent :value="v[k]" /></td>
|
<td v-for="(el, key) in header" :key="key"> <DynamicComponent :value="item[el]" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</VTable>
|
</VTable>
|
||||||
|
|
||||||
|
<div v-else class="h-[300px]">
|
||||||
|
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
@ -46,3 +46,65 @@ export function numberWithCommas(x: any) {
|
|||||||
return parts.join('.')
|
return parts.join('.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function tokenFormatter(tokens:any, denoms = {}, decimal = 2) {
|
||||||
|
if (Array.isArray(tokens)) {
|
||||||
|
return tokens.map(t => formatToken(t, denoms, decimal)).join(', ')
|
||||||
|
}
|
||||||
|
return formatToken(tokens, denoms, 2)
|
||||||
|
}
|
||||||
|
export function formatToken(token:any, IBCDenom = {}, decimals = 2, withDenom = true) {
|
||||||
|
if (token) {
|
||||||
|
const denom = IBCDenom[token.denom] || token.denom
|
||||||
|
if (withDenom) {
|
||||||
|
return `${formatTokenAmount(token.amount, decimals, denom)} ${formatTokenDenom(denom)}`
|
||||||
|
}
|
||||||
|
return formatTokenAmount(token.amount, decimals, denom)
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
export function formatTokenDenom(tokenDenom:any) {
|
||||||
|
if (tokenDenom && tokenDenom.code === undefined) {
|
||||||
|
let denom = tokenDenom.denom_trace ? tokenDenom.denom_trace.base_denom : tokenDenom
|
||||||
|
const chains = getLocalChains()
|
||||||
|
const selected = localStorage.getItem('selected_chain')
|
||||||
|
const selChain = chains[selected]
|
||||||
|
const nativeAsset = selChain.assets.find(a => (a.base === denom))
|
||||||
|
if (nativeAsset) {
|
||||||
|
denom = nativeAsset.symbol
|
||||||
|
} else {
|
||||||
|
const config = Object.values(chains)
|
||||||
|
config.forEach(x => {
|
||||||
|
if (x.assets) {
|
||||||
|
const asset = x.assets.find(a => (a.base === denom))
|
||||||
|
if (asset) denom = asset.symbol
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return denom.length > 10 ? `${denom.substring(0, 7).toUpperCase()}..${denom.substring(denom.length - 3)}` : denom.toUpperCase()
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isToken(value: string) {
|
||||||
|
let is = false
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
is = value.findIndex(x => Object.keys(x).includes('denom')) > -1
|
||||||
|
} else {
|
||||||
|
is = Object.keys(value).includes('denom')
|
||||||
|
}
|
||||||
|
return is
|
||||||
|
}
|
||||||
|
export function isStringArray(value: any) {
|
||||||
|
let is = false
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
is = value.findIndex(x => typeof x === 'string') > -1
|
||||||
|
}
|
||||||
|
return is
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isHexAddress(v: any) {
|
||||||
|
// const re = /^[A-Z\d]{40}$/
|
||||||
|
// return re.test(v)
|
||||||
|
return v.length === 28
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -1,33 +1,32 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
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('');
|
||||||
const store = useGovStore()
|
const store = useGovStore();
|
||||||
|
|
||||||
onMounted(()=>{
|
|
||||||
store.fetchProposals('2')
|
|
||||||
})
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
store.fetchProposals('2');
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<VTabs v-model="tab">
|
<VTabs v-model="tab" class="v-tabs-pill">
|
||||||
<VTab value="2">Voting</VTab>
|
<VTab value="2">Voting</VTab>
|
||||||
<VTab value="3" @click="store.fetchProposals('3')">Passed</VTab>
|
<VTab value="3" @click="store.fetchProposals('3')">Passed</VTab>
|
||||||
<VTab value="4" @click="store.fetchProposals('4')">Rejected</VTab>
|
<VTab value="4" @click="store.fetchProposals('4')">Rejected</VTab>
|
||||||
</VTabs>
|
</VTabs>
|
||||||
<VWindow v-model="tab" class="mt-5">
|
<VWindow v-model="tab" class="mt-5">
|
||||||
<VWindowItem value="2">
|
<VWindowItem value="2">
|
||||||
<ProposalListItem :proposals="store?.proposals['2']"/>
|
<ProposalListItem :proposals="store?.proposals['2']" />
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
|
|
||||||
<VWindowItem value="3">
|
<VWindowItem value="3">
|
||||||
<ProposalListItem :proposals="store?.proposals['3']"/>
|
<ProposalListItem :proposals="store?.proposals['3']" />
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
|
|
||||||
<VWindowItem value="4">
|
<VWindowItem value="4">
|
||||||
<ProposalListItem :proposals="store?.proposals['4']"/>
|
<ProposalListItem :proposals="store?.proposals['4']" />
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
</VWindow>
|
</VWindow>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
import { useParamStore } from '@/stores';
|
import { useParamStore } from '@/stores';
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import CardParameter from '@/components/CardParameter.vue'
|
import CardParameter from '@/components/CardParameter.vue'
|
||||||
import TableParameter from '@/components/TableParameter.vue'
|
import ArrayObjectElement from '@/components/dynamic/ArrayObjectElement.vue';
|
||||||
import { sort } from 'semver';
|
|
||||||
const store = useParamStore()
|
const store = useParamStore()
|
||||||
const chain = ref(store.chain)
|
const chain = ref(store.chain)
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -13,12 +12,18 @@ onMounted(() => {
|
|||||||
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="overflow-hidden">
|
||||||
<!-- Chain ID -->
|
<!-- Chain ID -->
|
||||||
<div class="bg-card px-4 pt-3 pb-4 rounded-sm">
|
<div class="bg-card 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 class="grid grid-cols-5 gap-4">
|
<div
|
||||||
<div v-for="(item,index) of chain.items" :key="index" class="rounded-sm bg-active px-4 py-2">
|
class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 2xl:grid-cols-6 gap-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(item, index) of chain.items"
|
||||||
|
:key="index"
|
||||||
|
class="rounded-sm bg-active px-4 py-2"
|
||||||
|
>
|
||||||
<div class="text-xs mb-2 text-secondary">{{ item.subtitle }}</div>
|
<div class="text-xs mb-2 text-secondary">{{ item.subtitle }}</div>
|
||||||
<div class="text-base text-main">{{ item.value }}</div>
|
<div class="text-base text-main">{{ item.value }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -35,12 +40,24 @@ onMounted(() => {
|
|||||||
<!-- Slashing Parameters -->
|
<!-- Slashing Parameters -->
|
||||||
<CardParameter :cardItem="store.slashing"/>
|
<CardParameter :cardItem="store.slashing"/>
|
||||||
<!-- Application Version -->
|
<!-- Application Version -->
|
||||||
<TableParameter :tableItem="store.appVersion"/>
|
<div class="bg-card px-4 pt-3 pb-4 rounded-sm mt-6">
|
||||||
<!-- Node Information -->
|
<div class="text-base mb-3 text-main">{{ store.appVersion?.title }}</div>
|
||||||
<TableParameter :tableItem="store.nodeVersion"/>
|
<ArrayObjectElement :value="store.appVersion?.items" :thead="false"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Node Information -->
|
||||||
|
<div class="bg-card px-4 pt-3 pb-4ß rounded-sm mt-6">
|
||||||
|
<div class="text-base mb-3 text-main">{{ store.nodeVersion?.title }}</div>
|
||||||
|
<ArrayObjectElement :value="store.nodeVersion?.items" :thead="false"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Application Version -->
|
||||||
|
<!-- <TableParameter :tableItem="store.appVersion" /> -->
|
||||||
|
<!-- Node Information -->
|
||||||
|
<!-- <TableParameter :tableItem="store.nodeVersion" /> -->
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<route>
|
<route>
|
||||||
{
|
{
|
||||||
meta: {
|
meta: {
|
||||||
|
12
src/modules/[chain]/uptime/index.vue
Normal file
12
src/modules/[chain]/uptime/index.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<VCard>
|
||||||
|
UPTIME
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
<route>
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
i18n: 'uptime'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
@ -4,7 +4,8 @@
|
|||||||
"blocks": "区块和交易",
|
"blocks": "区块和交易",
|
||||||
"staking": "质押生息",
|
"staking": "质押生息",
|
||||||
"governance": "社区治理",
|
"governance": "社区治理",
|
||||||
"parameters": "参数"
|
"parameters": "参数",
|
||||||
|
"uptime": "状态"
|
||||||
},
|
},
|
||||||
"index": {
|
"index": {
|
||||||
"slogan": "Ping Dashboard 是一个区块链浏览器,也是一个网页钱包,还有更多 ... 🛠",
|
"slogan": "Ping Dashboard 是一个区块链浏览器,也是一个网页钱包,还有更多 ... 🛠",
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
"blocks": "Blocks&Transaction",
|
"blocks": "Blocks&Transaction",
|
||||||
"staking": "Staking",
|
"staking": "Staking",
|
||||||
"governance": "Governance",
|
"governance": "Governance",
|
||||||
"parameters": "Parameters"
|
"parameters": "Parameters",
|
||||||
|
"uptime": "Uptime"
|
||||||
},
|
},
|
||||||
"index": {
|
"index": {
|
||||||
"slogan": "Ping Dashboard is not just an explorer but also a wallet and more ... 🛠",
|
"slogan": "Ping Dashboard is not just an explorer but also a wallet and more ... 🛠",
|
||||||
|
@ -143,7 +143,7 @@ export const useParamStore = defineStore("paramstore", {
|
|||||||
value: value }))
|
value: value }))
|
||||||
this.nodeVersion.items = Object.entries(res.default_node_info).map(([key, value]) => ({ subtitle:key,
|
this.nodeVersion.items = Object.entries(res.default_node_info).map(([key, value]) => ({ subtitle:key,
|
||||||
value: value }))
|
value: value }))
|
||||||
console.log('handleAbciInfo', res)
|
console.log('handleAbciInfo', this.nodeVersion.items)
|
||||||
},
|
},
|
||||||
async getBaseTendermintBlockLatest() {
|
async getBaseTendermintBlockLatest() {
|
||||||
return await this.blockchain.rpc.getBaseBlockLatest()
|
return await this.blockchain.rpc.getBaseBlockLatest()
|
||||||
|
20
yarn.lock
20
yarn.lock
@ -6609,7 +6609,7 @@ postcss@^8.0.9, postcss@^8.4.23:
|
|||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
source-map-js "^1.0.2"
|
source-map-js "^1.0.2"
|
||||||
|
|
||||||
postcss@^8.1.10, postcss@^8.4.21:
|
postcss@^8.1.10:
|
||||||
version "8.4.21"
|
version "8.4.21"
|
||||||
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz"
|
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz"
|
||||||
integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==
|
integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==
|
||||||
@ -6886,9 +6886,9 @@ rimraf@3.0.2, rimraf@^3.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob "^7.1.3"
|
glob "^7.1.3"
|
||||||
|
|
||||||
rollup@^3.20.2:
|
rollup@^3.21.0:
|
||||||
version "3.21.0"
|
version "3.21.0"
|
||||||
resolved "https://registry.npmjs.org/rollup/-/rollup-3.21.0.tgz"
|
resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.21.0.tgz#0a71517db56e150222670f88e5e7acfa4fede7c8"
|
||||||
integrity sha512-ANPhVcyeHvYdQMUyCbczy33nbLzI7RzrBje4uvNiTDJGIMtlKoOStmympwr9OtS1LZxiDmE2wvxHyVhoLtf1KQ==
|
integrity sha512-ANPhVcyeHvYdQMUyCbczy33nbLzI7RzrBje4uvNiTDJGIMtlKoOStmympwr9OtS1LZxiDmE2wvxHyVhoLtf1KQ==
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
@ -7365,7 +7365,7 @@ symbol-observable@^2.0.3:
|
|||||||
|
|
||||||
tailwindcss@^3.3.1:
|
tailwindcss@^3.3.1:
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.1.tgz#b6662fab6a9b704779e48d083a9fef5a81d2b81e"
|
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz#b6662fab6a9b704779e48d083a9fef5a81d2b81e"
|
||||||
integrity sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==
|
integrity sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==
|
||||||
dependencies:
|
dependencies:
|
||||||
arg "^5.0.2"
|
arg "^5.0.2"
|
||||||
@ -7749,14 +7749,14 @@ vite-plugin-vuetify@^1.0.2:
|
|||||||
debug "^4.3.3"
|
debug "^4.3.3"
|
||||||
upath "^2.0.1"
|
upath "^2.0.1"
|
||||||
|
|
||||||
vite@^4.3.1:
|
vite@^4.3.3:
|
||||||
version "4.3.1"
|
version "4.3.3"
|
||||||
resolved "https://registry.npmjs.org/vite/-/vite-4.3.1.tgz"
|
resolved "https://registry.yarnpkg.com/vite/-/vite-4.3.3.tgz#26adb4aa01439fc4546c480ea547674d87289396"
|
||||||
integrity sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==
|
integrity sha512-MwFlLBO4udZXd+VBcezo3u8mC77YQk+ik+fbc0GZWGgzfbPP+8Kf0fldhARqvSYmtIWoAJ5BXPClUbMTlqFxrA==
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild "^0.17.5"
|
esbuild "^0.17.5"
|
||||||
postcss "^8.4.21"
|
postcss "^8.4.23"
|
||||||
rollup "^3.20.2"
|
rollup "^3.21.0"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user