added web-app and landing
56
web-app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
.pnpm-store
|
||||
|
||||
# Build output
|
||||
/dist
|
||||
/dist-ssr
|
||||
storybook-static
|
||||
coverage
|
||||
|
||||
# Cache & temp directories
|
||||
.cache
|
||||
.tmp
|
||||
.temp
|
||||
.vite
|
||||
.vite-inspect
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
|
||||
# OS metadata
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*.local
|
||||
.env.local
|
||||
*.local
|
||||
|
||||
# Cypress artifacts
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
/cypress/downloads/
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.iml
|
||||
*.sw?
|
||||
33
web-app/README.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# harb staking
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
1
web-app/env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
13
web-app/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KrAIken Staking Interface</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
8711
web-app/package-lock.json
generated
Normal file
44
web-app/package.json
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"name": "harb-staking",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build",
|
||||
"subtree": "git subtree push --prefix dist origin gh-pages"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/vue-query": "^5.64.2",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@wagmi/vue": "^0.1.11",
|
||||
"axios": "^1.7.9",
|
||||
"chart.js": "^4.4.7",
|
||||
"chartjs-plugin-zoom": "^2.2.0",
|
||||
"element-plus": "^2.9.3",
|
||||
"harb-lib": "^0.2.0",
|
||||
"sass": "^1.83.4",
|
||||
"viem": "^2.22.13",
|
||||
"vitest": "^3.0.4",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue-tippy": "^6.6.0",
|
||||
"vue-toastification": "^2.0.0-rc.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/vue": "^4.3.0",
|
||||
"@tsconfig/node22": "^22.0.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"gh-pages": "^6.1.1",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"typescript": "~5.7.3",
|
||||
"vite": "^6.0.11",
|
||||
"vite-plugin-vue-devtools": "^7.7.0",
|
||||
"vue-tsc": "^2.2.0"
|
||||
}
|
||||
}
|
||||
BIN
web-app/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
web-app/public/fonts/Audiowide-Regular.ttf
Normal file
BIN
web-app/public/fonts/Crypto-Scam-Regular.ttf
Normal file
BIN
web-app/public/fonts/DMSans-Bold.ttf
Normal file
BIN
web-app/public/fonts/DMSans-Medium.ttf
Normal file
BIN
web-app/public/fonts/DMSans-Regular.ttf
Normal file
BIN
web-app/public/fonts/DMSans-SemiBold.ttf
Normal file
BIN
web-app/public/fonts/DMSans-variable.ttf
Normal file
BIN
web-app/public/fonts/Play-Bold.ttf
Normal file
BIN
web-app/public/fonts/Play-Regular.ttf
Normal file
13
web-app/public/img/connectors/Coinbase Wallet.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_13571_129878)">
|
||||
<rect width="40" height="40" fill="#0052FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.3312 0H31.6672C36.2704 0 40 4.0128 40 8.9632V31.0368C40 35.9872 36.2704 40 31.6688 40H8.3312C3.7296 40 0 35.9872 0 31.0368V8.9632C0 4.0128 3.7296 0 8.3312 0Z" fill="#0052FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.9989 5.79443C27.8453 5.79443 34.2053 12.1544 34.2053 20.0008C34.2053 27.8472 27.8453 34.2072 19.9989 34.2072C12.1525 34.2072 5.79254 27.8472 5.79254 20.0008C5.79254 12.1544 12.1525 5.79443 19.9989 5.79443Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5005 15.459H23.4973C24.0733 15.459 24.5389 15.9614 24.5389 16.579V23.419C24.5389 24.0382 24.0717 24.539 23.4973 24.539H16.5005C15.9245 24.539 15.4589 24.0366 15.4589 23.419V16.579C15.4589 15.9614 15.9261 15.459 16.5005 15.459Z" fill="#0052FF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_13571_129878">
|
||||
<rect width="40" height="40" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
5
web-app/public/img/connectors/WalletConnect.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="40" height="40" fill="#3396FF"/>
|
||||
<path d="M20 40C31.0457 40 40 31.0457 40 20C40 8.9543 31.0457 0 20 0C8.9543 0 0 8.9543 0 20C0 31.0457 8.9543 40 20 40Z" fill="#3396FF"/>
|
||||
<path d="M12.2327 14.8836C16.5225 10.7003 23.4779 10.7003 27.7677 14.8836L28.284 15.3871C28.4985 15.5962 28.4985 15.9353 28.284 16.1445L26.5179 17.8668C26.4106 17.9713 26.2368 17.9713 26.1295 17.8668L25.419 17.1739C22.4263 14.2555 17.5741 14.2555 14.5813 17.1739L13.8204 17.9159C13.7132 18.0205 13.5393 18.0205 13.4321 17.9159L11.666 16.1936C11.4514 15.9845 11.4514 15.6453 11.666 15.4362L12.2327 14.8836ZM31.4203 18.4454L32.9922 19.9782C33.2067 20.1874 33.2067 20.5265 32.9922 20.7356L25.9045 27.6473C25.69 27.8565 25.3422 27.8565 25.1277 27.6473L20.0973 22.742C20.0437 22.6896 19.9568 22.6896 19.9031 22.742L14.8728 27.6473C14.6583 27.8565 14.3106 27.8565 14.096 27.6473L7.00816 20.7355C6.79367 20.5264 6.79367 20.1873 7.00816 19.9782L8.58004 18.4453C8.79454 18.2362 9.1423 18.2362 9.35679 18.4453L14.3873 23.3508C14.4409 23.4031 14.5278 23.4031 14.5814 23.3508L19.6117 18.4453C19.8262 18.2361 20.1739 18.2361 20.3885 18.4453L25.4189 23.3508C25.4726 23.4031 25.5595 23.4031 25.6131 23.3508L30.6436 18.4454C30.858 18.2362 31.2058 18.2362 31.4203 18.4454Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
38
web-app/src/App.vue
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
|
||||
<component :is="layoutComponent">
|
||||
<router-view />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouterView, useRoute, useRouter } from "vue-router";
|
||||
import type { LayoutName } from '@/types/router';
|
||||
|
||||
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.vue';
|
||||
import NavbarLayout from '@/layouts/NavbarLayout.vue';
|
||||
|
||||
const layouts = {
|
||||
DefaultLayout,
|
||||
NavbarLayout
|
||||
};
|
||||
|
||||
import { computed, defineAsyncComponent, provide, ref } from "vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
const layoutComponent = computed(() => {
|
||||
const layoutName: LayoutName = route.meta.layout ?? 'DefaultLayout';
|
||||
return layouts[layoutName]; // Jetzt kennt TypeScript den Typ!
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="sass">
|
||||
footer
|
||||
margin-top: auto
|
||||
</style>
|
||||
5
web-app/src/assets/arrows.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="68" height="24" viewBox="0 0 68 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.4508 11.9999L15.1008 4.64991C14.8508 4.39991 14.7298 4.10824 14.7378 3.77491C14.7458 3.44157 14.8751 3.14991 15.1258 2.89991C15.3764 2.64991 15.6681 2.52491 16.0008 2.52491C16.3334 2.52491 16.6251 2.64991 16.8758 2.89991L24.5758 10.5749C24.7758 10.7749 24.9258 10.9999 25.0258 11.2499C25.1258 11.4999 25.1758 11.7499 25.1758 11.9999C25.1758 12.2499 25.1258 12.4999 25.0258 12.7499C24.9258 12.9999 24.7758 13.2249 24.5758 13.4249L16.8758 21.1249C16.6258 21.3749 16.3298 21.4959 15.9878 21.4879C15.6458 21.4799 15.3501 21.3506 15.1008 21.0999C14.8514 20.8492 14.7264 20.5576 14.7258 20.2249C14.7251 19.8922 14.8501 19.6006 15.1008 19.3499L22.4508 11.9999Z" fill="#D6D6D6"/>
|
||||
<path d="M36.4508 11.9999L29.1008 4.64991C28.8508 4.39991 28.7298 4.10824 28.7378 3.77491C28.7458 3.44157 28.8751 3.14991 29.1258 2.89991C29.3764 2.64991 29.6681 2.52491 30.0008 2.52491C30.3334 2.52491 30.6251 2.64991 30.8758 2.89991L38.5758 10.5749C38.7758 10.7749 38.9258 10.9999 39.0258 11.2499C39.1258 11.4999 39.1758 11.7499 39.1758 11.9999C39.1758 12.2499 39.1258 12.4999 39.0258 12.7499C38.9258 12.9999 38.7758 13.2249 38.5758 13.4249L30.8758 21.1249C30.6258 21.3749 30.3298 21.4959 29.9878 21.4879C29.6458 21.4799 29.3501 21.3506 29.1008 21.0999C28.8514 20.8492 28.7264 20.5576 28.7258 20.2249C28.7251 19.8922 28.8501 19.6006 29.1008 19.3499L36.4508 11.9999Z" fill="#D6D6D6"/>
|
||||
<path d="M50.4508 11.9999L43.1008 4.64991C42.8508 4.39991 42.7298 4.10824 42.7378 3.77491C42.7458 3.44157 42.8751 3.14991 43.1258 2.89991C43.3764 2.64991 43.6681 2.52491 44.0008 2.52491C44.3334 2.52491 44.6251 2.64991 44.8758 2.89991L52.5758 10.5749C52.7758 10.7749 52.9258 10.9999 53.0258 11.2499C53.1258 11.4999 53.1758 11.7499 53.1758 11.9999C53.1758 12.2499 53.1258 12.4999 53.0258 12.7499C52.9258 12.9999 52.7758 13.2249 52.5758 13.4249L44.8758 21.1249C44.6258 21.3749 44.3298 21.4959 43.9878 21.4879C43.6458 21.4799 43.3501 21.3506 43.1008 21.0999C42.8514 20.8492 42.7264 20.5576 42.7258 20.2249C42.7251 19.8922 42.8501 19.6006 43.1008 19.3499L50.4508 11.9999Z" fill="#D6D6D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
13
web-app/src/assets/connectors/Coinbase Wallet.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_13571_129878)">
|
||||
<rect width="40" height="40" fill="#0052FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.3312 0H31.6672C36.2704 0 40 4.0128 40 8.9632V31.0368C40 35.9872 36.2704 40 31.6688 40H8.3312C3.7296 40 0 35.9872 0 31.0368V8.9632C0 4.0128 3.7296 0 8.3312 0Z" fill="#0052FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.9989 5.79443C27.8453 5.79443 34.2053 12.1544 34.2053 20.0008C34.2053 27.8472 27.8453 34.2072 19.9989 34.2072C12.1525 34.2072 5.79254 27.8472 5.79254 20.0008C5.79254 12.1544 12.1525 5.79443 19.9989 5.79443Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5005 15.459H23.4973C24.0733 15.459 24.5389 15.9614 24.5389 16.579V23.419C24.5389 24.0382 24.0717 24.539 23.4973 24.539H16.5005C15.9245 24.539 15.4589 24.0366 15.4589 23.419V16.579C15.4589 15.9614 15.9261 15.459 16.5005 15.459Z" fill="#0052FF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_13571_129878">
|
||||
<rect width="40" height="40" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
5
web-app/src/assets/connectors/WalletConnect.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="40" height="40" fill="#3396FF"/>
|
||||
<path d="M20 40C31.0457 40 40 31.0457 40 20C40 8.9543 31.0457 0 20 0C8.9543 0 0 8.9543 0 20C0 31.0457 8.9543 40 20 40Z" fill="#3396FF"/>
|
||||
<path d="M12.2327 14.8836C16.5225 10.7003 23.4779 10.7003 27.7677 14.8836L28.284 15.3871C28.4985 15.5962 28.4985 15.9353 28.284 16.1445L26.5179 17.8668C26.4106 17.9713 26.2368 17.9713 26.1295 17.8668L25.419 17.1739C22.4263 14.2555 17.5741 14.2555 14.5813 17.1739L13.8204 17.9159C13.7132 18.0205 13.5393 18.0205 13.4321 17.9159L11.666 16.1936C11.4514 15.9845 11.4514 15.6453 11.666 15.4362L12.2327 14.8836ZM31.4203 18.4454L32.9922 19.9782C33.2067 20.1874 33.2067 20.5265 32.9922 20.7356L25.9045 27.6473C25.69 27.8565 25.3422 27.8565 25.1277 27.6473L20.0973 22.742C20.0437 22.6896 19.9568 22.6896 19.9031 22.742L14.8728 27.6473C14.6583 27.8565 14.3106 27.8565 14.096 27.6473L7.00816 20.7355C6.79367 20.5264 6.79367 20.1873 7.00816 19.9782L8.58004 18.4453C8.79454 18.2362 9.1423 18.2362 9.35679 18.4453L14.3873 23.3508C14.4409 23.4031 14.5278 23.4031 14.5814 23.3508L19.6117 18.4453C19.8262 18.2361 20.1739 18.2361 20.3885 18.4453L25.4189 23.3508C25.4726 23.4031 25.5595 23.4031 25.6131 23.3508L30.6436 18.4454C30.858 18.2362 31.2058 18.2362 31.4203 18.4454Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
web-app/src/assets/contracts/harb.json
Normal file
1
web-app/src/assets/contracts/stake.json
Normal file
BIN
web-app/src/assets/img/header-image.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
web-app/src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
20
web-app/src/assets/logo.svg
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<svg width="49" height="50" viewBox="0 0 49 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3277_37470)">
|
||||
<path d="M49 25C49 38.531 38.031 49.5 24.5 49.5C10.969 49.5 0 38.531 0 25C0 11.469 10.969 0.5 24.5 0.5C38.031 0.5 49 11.469 49 25Z" fill="url(#paint0_linear_3277_37470)"/>
|
||||
<path d="M24.4216 18.4727L46.6369 50.0385H2.20625L24.4216 18.4727Z" fill="#1A54F4"/>
|
||||
<path d="M27.0679 26.1869L29.2195 26.7659L29.7988 26.1869L24.3369 18.4121L23.5921 19.4046L9.02702 40.1649L8.44773 40.992H10.1856L13.0821 38.5934L15.5648 38.2626L19.206 33.7962L18.7923 31.9766L20.7784 26.9313L24.0059 24.0364L27.0679 26.1869Z" fill="white"/>
|
||||
<path d="M30.1086 10.4087C29.8314 10.6748 29.5708 10.9456 29.3146 11.2119C28.0608 12.5151 26.9116 13.7097 24.4234 13.7097C22.8617 13.7097 21.8976 13.2834 21.1067 12.6834C20.6977 12.3731 20.3259 12.0093 19.9298 11.6019C19.8504 11.5202 19.7696 11.4364 19.6873 11.351C19.3941 11.0466 19.0818 10.7225 18.7439 10.406C19.0782 10.091 19.3802 9.77848 19.6701 9.47847C20.9318 8.173 21.9643 7.10465 24.4234 7.10465C26.8967 7.10465 27.9868 8.23505 29.2452 9.54003C29.5197 9.82458 29.8021 10.1174 30.1086 10.4087Z" stroke="white" stroke-width="1.22884"/>
|
||||
<circle cx="24.3315" cy="10.2992" r="2.87241" stroke="white" stroke-width="1.01379"/>
|
||||
<path d="M27.0342 8.27148H28.1474L29.3997 10.468L28.1474 12.6646H27.0342V8.27148Z" fill="white"/>
|
||||
<path d="M21.6279 12.666L20.5147 12.666L19.2624 10.4695L20.5147 8.27291L21.6279 8.27291L21.6279 12.666Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3277_37470" x1="24.331" y1="49.5" x2="24.331" y2="2.18965" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#1A54F4"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_3277_37470">
|
||||
<rect y="0.5" width="49" height="49" rx="24.5" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
8
web-app/src/assets/logout.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_2610_39819" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="28" height="28">
|
||||
<rect x="28" y="27.999" width="27.9984" height="27.9984" transform="rotate(-180 28 27.999)" fill="#D9D9D9"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2610_39819)">
|
||||
<path d="M14.0006 3.50019L14.0006 5.8334L5.83438 5.8334L5.83437 22.1658L14.0006 22.1658L14.0006 24.499L5.83437 24.499C5.19274 24.499 4.64347 24.2706 4.18655 23.8136C3.72963 23.3567 3.50117 22.8074 3.50117 22.1658L3.50117 5.8334C3.50117 5.19176 3.72963 4.64249 4.18655 4.18557C4.64347 3.72865 5.19274 3.50019 5.83438 3.50019L14.0006 3.50019ZM16.3338 8.1666L17.9379 9.85817L14.963 12.833L24.5 12.833L24.5 15.1662L14.963 15.1662L17.9379 18.141L16.3338 19.8326L10.5008 13.9996L16.3338 8.1666Z" fill="#F9F9FA"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 881 B |
305
web-app/src/assets/styles/global.sass
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
@font-face
|
||||
font-family: "DM Sans"
|
||||
src: url("/fonts/DMSans-variable.ttf")
|
||||
font-style: normal
|
||||
@font-face
|
||||
font-family: "DM Sans semi-bold"
|
||||
src: url("/fonts/DMSans-SemiBold.ttf")
|
||||
font-weight: 600
|
||||
font-style: normal
|
||||
|
||||
@font-face
|
||||
font-family: "Crypto Scam"
|
||||
src: url("/fonts/Crypto-Scam-Regular.ttf")
|
||||
font-weight: 400
|
||||
font-style: normal
|
||||
@font-face
|
||||
font-family: "Play"
|
||||
src: url("/fonts/Play-Regular.ttf")
|
||||
font-weight: 400
|
||||
font-style: normal
|
||||
@font-face
|
||||
font-family: "Play-Bold"
|
||||
src: url("/fonts/Play-Bold.ttf")
|
||||
@font-face
|
||||
font-family: "Audiowide"
|
||||
src: url("/fonts/Audiowide-Regular.ttf") format('truetype')
|
||||
|
||||
|
||||
:root, [data-theme="default"]
|
||||
font-family: "DM Sans", sans-serif
|
||||
color: var(--color-midnight-black)
|
||||
font-optical-sizing: auto
|
||||
font-size: 14px
|
||||
line-height: 20px
|
||||
letter-spacing: 0.17px
|
||||
// global
|
||||
--color-primary: #7550AE
|
||||
--color-secondary: #07111B
|
||||
--color-tertiary: #141414
|
||||
--color-input: #202020
|
||||
--color-white: #FFF
|
||||
--color-primary--hovered: var(--color-white-hovered)
|
||||
--color-primary-light: #FFF
|
||||
--color-white-hovered: #cccccc
|
||||
--color-black-hovered: #333333
|
||||
|
||||
--color-grey: #858585
|
||||
--color-quatenary: #FFF
|
||||
|
||||
--color-steel-blue: #4682B4
|
||||
--color-based-blue: #7550AE
|
||||
--color-based-blue--hovered: #5f4884
|
||||
|
||||
--color-bright-white: #FFFFFF
|
||||
--color-bright-white-hovered: #e6e6e6
|
||||
--color-very-light-grey: #F0F0F0
|
||||
--color-light-grey: #D6D6D6
|
||||
--color-border-main: #A1A3A7
|
||||
--color-midnight-black: #0F0F0F
|
||||
--color-midnight-black-hovered: #272727
|
||||
--color-font: var(--color-white)
|
||||
--color-bg: white
|
||||
--color-black: black
|
||||
--color-blood: #8B0000
|
||||
--color-white: white
|
||||
--color-text-primary: #121312
|
||||
--color-text-primary-hovered: #292b29
|
||||
--color-contrast-lower: hsl(0, 0%, 95%)
|
||||
--color-contrast-low: hsl(240, 1%, 83%)
|
||||
--color-contrast-medium: hsl(240, 1%, 48%)
|
||||
--color-contrast-high: hsl(240, 4%, 20%)
|
||||
--color-contrast-higher: black
|
||||
--color-disabled: #A1A3A7
|
||||
--color-bg-disabled: #D6D6D6
|
||||
--border-radius: 16px
|
||||
//buttons
|
||||
--btn-color: var(--color-primary)
|
||||
//todo btn-color hover
|
||||
--btn-bg-color: var(--color-black)
|
||||
--btn-padding: 16px 32px
|
||||
--btn-dense-padding: 8px 16px
|
||||
|
||||
--font-size-number-big: 24px
|
||||
--font-size-number-small: 14px
|
||||
--font-size-alert-title: 16px
|
||||
--font-size-navigation: 18px
|
||||
--font-size-button-small: 13px
|
||||
--font-size-button-medium: 14px
|
||||
--font-size-button-large: 20px
|
||||
|
||||
--color-button-bg: var(--color-based-blue)
|
||||
--color-button-font: var(--color-white)
|
||||
--color-button-bg--outlined: var(--color-white)
|
||||
--color-button-font--outlined: var(--color-based-blue)
|
||||
--color-button-border--outlined: var(--color-based-blue)
|
||||
--color-button-bg--hovered: var(--color-based-blue)
|
||||
--color-button-font--hovered: var(--color-based-blue)
|
||||
--color-button-bg--active: var(--color-based-blue)
|
||||
--color-button-font--active: var(--color-based-blue)
|
||||
--color-button-bg--light: var(--color-white)
|
||||
--color-button-font--light: var(--color-secondary)
|
||||
--color-button-border--light: var(--color-based-blue)
|
||||
|
||||
//card
|
||||
--color-card-font: var(--color-font)
|
||||
--color-card-bg: var(--color-tertiary)
|
||||
--color-card-box-shadow: 0px 0px 30px 0px rgba(0, 0, 0, 0.20)
|
||||
--card-border-color: var(--color-border-main)
|
||||
--card-border-radius: var(--border-radius)
|
||||
--card-border-width: 1px
|
||||
--card-border-style: solid
|
||||
|
||||
--color-tab-bg: var(--color-primary-light)
|
||||
--color-tab-font--active: var(--color-based-blue)
|
||||
--color-tab-border--active: var(--color-based-blue)
|
||||
|
||||
--color-navbar-bg: var(--color-secondary)
|
||||
--color-navbar-font: var(--font-color)
|
||||
--color-navbar-border: var(--color-black)
|
||||
|
||||
--color-font-main: var(--color-font)
|
||||
|
||||
--color-social-font: var(--color-black)
|
||||
--color-social-border: var(--color-black)
|
||||
|
||||
--color-collapse-font: var(--color-black)
|
||||
--color-collapse-bg: var(--color-white)
|
||||
--color-collapse-border: var(--color-black)
|
||||
|
||||
--color-output-bg: var(--color-very-light-grey)
|
||||
|
||||
--color-expand-icon: var(--color-black)
|
||||
|
||||
--color-icon: var(--color-black)
|
||||
|
||||
--chart-button-background-color: var(--color-based-blue)
|
||||
--chart-button-background-color--hovered: var(--color-based-blue--hovered)
|
||||
|
||||
// --color-tab-bg--active:
|
||||
// --color-tab-font--hovered:
|
||||
// --color-tab-bg--hovered:
|
||||
|
||||
// --color-tab-font:
|
||||
// --color-tab-bg:
|
||||
|
||||
//fonts
|
||||
--font-h1: 52px
|
||||
--font-h2: 44px
|
||||
--font-h3: 32px
|
||||
--font-h4: 27px
|
||||
--font-h5: 24px
|
||||
--font-h6: 20px
|
||||
--font-subheader1: 16px
|
||||
--font-subheader2: 14px
|
||||
--font-body0: 18px
|
||||
--font-body1: 16px
|
||||
--font-body2: 14px
|
||||
--font-caption: 12px
|
||||
--font-overline: 11px
|
||||
body
|
||||
color: var(--color-font-main)
|
||||
h1,h2,h3,h4,h5, h6
|
||||
text-align: center
|
||||
h1
|
||||
font-size: var(--font-h2)
|
||||
margin: 24px 0
|
||||
letter-spacing: -0.5px
|
||||
font-weight: 700
|
||||
line-height: 120%
|
||||
@media (min-width: 768px)
|
||||
font-size: var(--font-h1)
|
||||
h2
|
||||
font-size: var(--font-h2)
|
||||
letter-spacing: -0.5px
|
||||
font-weight: 700
|
||||
line-height: 120%
|
||||
h3
|
||||
font-size: var(--font-h3)
|
||||
h4
|
||||
font-size: var(--font-h4)
|
||||
h5
|
||||
font-size: var(--font-h5)
|
||||
text-transform: none
|
||||
font-weight: 700
|
||||
line-height: 133.4%
|
||||
h6
|
||||
font-size: var(--font-h6)
|
||||
|
||||
.subheader1
|
||||
font-size: var(--font-subheader1)
|
||||
font-weight: bold
|
||||
letter-spacing: 0.15px
|
||||
text-decoration: underline
|
||||
.subheader2
|
||||
font-size: var(--font-subheader2)
|
||||
font-weight: bold
|
||||
letter-spacing: 0.1px
|
||||
.number-big
|
||||
font-size: var(--font-size-number-big)
|
||||
letter-spacing: 4.8px
|
||||
.number-small
|
||||
font-size: var(--font-size-number-small)
|
||||
letter-spacing: 20%
|
||||
.navigation-font
|
||||
font-weight: 500
|
||||
font-size: var(--font-size-navigation)
|
||||
.button-font
|
||||
font-weight: 500
|
||||
font-size: var(--font-size-navigation)
|
||||
.body2
|
||||
font-size: var(--font-body2)
|
||||
font-weight: 400
|
||||
letter-spacing: 0.17px
|
||||
.caption
|
||||
font-size: var(--font-caption)
|
||||
letter-spacing: 1px
|
||||
|
||||
[data-theme="dark"]
|
||||
--color-primary: black
|
||||
--color-primary--hovered: var(--color-black-hovered)
|
||||
--color-secondary: black
|
||||
--color-steel-blue: #4682B4
|
||||
--color-based-blue: #1A54F4
|
||||
--color-bg: black
|
||||
--color-font: white
|
||||
--color-black: white
|
||||
--color-dark-grey: #1F1F1F
|
||||
--color-contrast-lower: hsl(0, 0%, 95%)
|
||||
--color-contrast-low: hsl(240, 1%, 83%)
|
||||
--color-contrast-medium: hsl(240, 1%, 48%)
|
||||
--color-contrast-high: hsl(240, 4%, 20%)
|
||||
--color-contrast-higher: white
|
||||
--color-disabled: #A1A3A7
|
||||
--color-bg-disabled: #D6D6D6
|
||||
--border-radius: 12px
|
||||
--color-navbar-bg: var(--color-midnight-black)
|
||||
--color-navbar-font: var(--color-white)
|
||||
--color-navbar-border: var(--color-white)
|
||||
--color-font-main: var(--color-white)
|
||||
|
||||
--color-card-bg: var(--color-midnight-black)
|
||||
--color-card-font: var(--color-white)
|
||||
--color-card-box-shadow: 0px 0px 30px 0px rgba(255, 255, 255, 0.2)
|
||||
|
||||
--color-tab-bg: var(--color-text-primary)
|
||||
--color-tab-bg--active: var(--color-text-primary)
|
||||
|
||||
--color-output-bg: var(--color-dark-grey)
|
||||
|
||||
--color-social-font: var(--color-white)
|
||||
--color-social-border: var(--color-white)
|
||||
|
||||
--color-collapse-font: var(--color-white)
|
||||
--color-collapse-bg: var(--color-dark-grey)
|
||||
--color-collapse-border: var(--color-white)
|
||||
|
||||
--color-expand-icon: var(--color-white)
|
||||
|
||||
--chart-button-background-color: var(--color-primary)
|
||||
--chart-button-background-color--hovered: var(--color-primary--hovered)
|
||||
|
||||
.container
|
||||
margin-right: auto
|
||||
margin-left: auto
|
||||
padding-right: 16px
|
||||
padding-left: 16px
|
||||
box-sizing: content-box
|
||||
// &.container--variant1
|
||||
// @media (min-width: 768px)
|
||||
// max-width: 800px
|
||||
// padding: 24px 64px
|
||||
@media (min-width: 768px)
|
||||
max-width: 500px
|
||||
|
||||
.container2
|
||||
@extend .container
|
||||
@media (min-width: 768px)
|
||||
max-width: 650px
|
||||
|
||||
.container3
|
||||
@extend .container
|
||||
@media (min-width: 768px)
|
||||
max-width: 750px
|
||||
.container4
|
||||
@extend .container
|
||||
@media (min-width: 768px)
|
||||
max-width: 1100px
|
||||
|
||||
.row
|
||||
display: flex
|
||||
flex-direction: row
|
||||
>*
|
||||
flex: 1 1 auto
|
||||
|
||||
body
|
||||
background-color: var(--color-primary)
|
||||
margin: 0
|
||||
|
||||
*
|
||||
box-sizing: border-box
|
||||
|
||||
|
||||
a
|
||||
color: inherit
|
||||
text-decoration: none
|
||||
37
web-app/src/assets/styles/main.sass
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
@use "./global.sass"
|
||||
|
||||
#app
|
||||
display: flex
|
||||
flex-direction: column
|
||||
min-height: 100vh
|
||||
gap: 64px
|
||||
position: relative
|
||||
margin-bottom: 80px
|
||||
@media (min-width: 768px)
|
||||
margin-bottom: 0
|
||||
|
||||
|
||||
footer
|
||||
margin-top: auto
|
||||
|
||||
.social-links
|
||||
display: flex
|
||||
gap: 24px
|
||||
|
||||
.Vue-Toastification__container
|
||||
&.harb-toast-container
|
||||
&.top-right
|
||||
@media (min-width: 768px)
|
||||
top: 80px
|
||||
// right: 20px
|
||||
// left: unset
|
||||
|
||||
.pointer
|
||||
&:hover, &:active, &:focus
|
||||
cursor: pointer
|
||||
|
||||
html
|
||||
scroll-behavior: smooth
|
||||
scroll-padding-top: 100px
|
||||
body
|
||||
background: radial-gradient(100.02% 132.74% at -4.63% 2.69%, rgba(117, 80, 174, 0.00) 46.5%, rgba(117, 80, 174, 0.50) 100%), radial-gradient(91.35% 111.13% at 12.96% 16.43%, rgba(117, 80, 174, 0.00) 46.5%, rgba(117, 80, 174, 0.50) 100%), var(--Color, #202020)
|
||||
41
web-app/src/components/HelloWorld.vue
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
msg: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="greetings">
|
||||
<h1 class="green">{{ msg }}</h1>
|
||||
<h3>
|
||||
You’ve successfully created a project with
|
||||
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
font-weight: 500;
|
||||
font-size: 2.6rem;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
487
web-app/src/components/StakeHolder.vue
Normal file
|
|
@ -0,0 +1,487 @@
|
|||
<template>
|
||||
<div class="hold-inner">
|
||||
<div class="stake-inner">
|
||||
<template v-if="!statCollection.initialized">
|
||||
<div>
|
||||
<f-loader></f-loader>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="subheader2">Token Amount</div>
|
||||
<FSlider :min="minStakeAmount" :max="maxStakeAmount" v-model="stake.stakingAmountNumber"></FSlider>
|
||||
<div class="formular">
|
||||
<div class="row row-1">
|
||||
<f-input label="Staking Amount" class="staking-amount" v-model="stake.stakingAmountNumber">
|
||||
<template v-slot:details>
|
||||
<div class="balance">Balance: {{ maxStakeAmount.toFixed(2) }} $KRK</div>
|
||||
<div @click="setMaxAmount" class="staking-amount-max">
|
||||
<b>Max</b>
|
||||
</div>
|
||||
</template>
|
||||
</f-input>
|
||||
<Icon class="stake-arrow" icon="mdi:chevron-triple-right"></Icon>
|
||||
<f-input
|
||||
label="Owner Slots"
|
||||
class="staking-amount"
|
||||
disabled
|
||||
:modelValue="`${stakeSlots}(${supplyFreeze?.toFixed(4)})`"
|
||||
>
|
||||
<template #info>
|
||||
Slots correspond to a percentage of ownership in the protocol.<br /><br />1,000 Slots =
|
||||
1% Ownership<br /><br />When you unstake you get the exact percentage of the current
|
||||
$KRK total supply. When the total supply increased since you staked you get more tokens
|
||||
back than before.
|
||||
</template>
|
||||
</f-input>
|
||||
<!-- <f-select :items="adjustTaxRate.taxRates" label="Tax" v-model="taxRate">
|
||||
<template v-slot:info>
|
||||
The tax you have to pay to keep your staking position open. The tax is
|
||||
calculated on a yearly basis but paid continuously.
|
||||
</template>
|
||||
</f-select> -->
|
||||
</div>
|
||||
<div class="row row-2">
|
||||
<f-select :items="adjustTaxRate.taxRates" label="Tax" v-model="taxRate">
|
||||
<template v-slot:info>
|
||||
The yearly tax you have to pay to keep your slots open. The tax is paid when unstaking
|
||||
or manually in the dashboard. If someone pays a higher tax they can buy you out.
|
||||
</template>
|
||||
</f-select>
|
||||
<f-input label="Floor Tax" disabled :modelValue="floorTax">
|
||||
<template v-slot:info>
|
||||
This is the current minimum tax you have to pay to claim owner slots from other owners.
|
||||
</template>
|
||||
</f-input>
|
||||
<f-input label="Positions Buyout" disabled :modelValue="snatchAblePositions.length">
|
||||
<template v-slot:info>
|
||||
This shows you the numbers of staking positions you buy out from current owners by
|
||||
paying a higher tax. If you get bought out yourself by new owners you get paid out the
|
||||
current market value of your position incl. your profits.
|
||||
</template>
|
||||
</f-input>
|
||||
</div>
|
||||
</div>
|
||||
<f-button size="large" disabled block v-if="stake.state === 'NoBalance'">Insufficient Balance</f-button>
|
||||
<f-button size="large" disabled block v-else-if="stake.stakingAmountNumber < minStakeAmount"
|
||||
>Stake amount too low</f-button
|
||||
>
|
||||
<f-button
|
||||
size="large"
|
||||
disabled
|
||||
block
|
||||
v-else-if="
|
||||
!openPositionsAvailable && stake.state === 'StakeAble' && snatchAblePositions.length === 0
|
||||
"
|
||||
>taxRate too low to snatch</f-button
|
||||
>
|
||||
<f-button
|
||||
size="large"
|
||||
block
|
||||
v-else-if="stake.state === 'StakeAble' && snatchAblePositions.length === 0"
|
||||
@click="stakeSnatch"
|
||||
>Stake</f-button
|
||||
>
|
||||
<f-button
|
||||
size="large"
|
||||
block
|
||||
v-else-if="stake.state === 'StakeAble' && snatchAblePositions.length > 0"
|
||||
@click="stakeSnatch"
|
||||
>Snatch and Stake</f-button
|
||||
>
|
||||
<f-button size="large" outlined block v-else-if="stake.state === 'SignTransaction'"
|
||||
>Sign Transaction ...</f-button
|
||||
>
|
||||
<f-button size="large" outlined block v-else-if="stake.state === 'Waiting'">Waiting ...</f-button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- <template v-if="myActivePositions.length > 0 && status === 'connected'">
|
||||
<h5>Your Active Positions</h5>
|
||||
<collapse-active
|
||||
v-for="position in myActivePositions"
|
||||
:taxRate="position.taxRatePercentage"
|
||||
:amount="position.amount"
|
||||
:treshold="tresholdValue"
|
||||
:id="position.positionId"
|
||||
:position="position"
|
||||
:key="position.id"
|
||||
></collapse-active>
|
||||
</template>
|
||||
<template v-if="myClosedPositions.length > 0 && status === 'connected'">
|
||||
<h5>History</h5>
|
||||
<collapse-history
|
||||
v-for="position in myClosedPositions"
|
||||
:taxRate="position.taxRatePercentage"
|
||||
:taxPaid="formatBigNumber(position.taxPaid, 18)"
|
||||
:amount="position.amount"
|
||||
:treshold="tresholdValue"
|
||||
:id="position.positionId"
|
||||
:key="position.id"
|
||||
:position="position"
|
||||
></collapse-history>
|
||||
</template> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FButton from "@/components/fcomponents/FButton.vue";
|
||||
import FInput from "@/components/fcomponents/FInput.vue";
|
||||
import FSelect from "@/components/fcomponents/FSelect.vue";
|
||||
import FLoader from "@/components/fcomponents/FLoader.vue";
|
||||
import FSlider from "@/components/fcomponents/FSlider.vue";
|
||||
import FOutput from "@/components/fcomponents/FOutput.vue";
|
||||
import { Icon } from "@iconify/vue";
|
||||
import { formatBigIntDivision, InsertCommaNumber, formatBigNumber, bigInt2Number } from "@/utils/helper";
|
||||
// import StatsOutput from "@/components/molecules/StatsOutput.vue";
|
||||
// import ChartJs from "@/components/ChartJs.vue";
|
||||
// import CollapseActive from "@/components/collapse/CollapseActive.vue";
|
||||
// import CollapseHistory from "@/components/collapse/CollapseHistory.vue";
|
||||
// import { bytesToUint256, uint256ToBytes } from "harb-lib";
|
||||
// import { getSnatchList } from "harb-lib/dist/";
|
||||
import { formatUnits } from "viem";
|
||||
import axios from "axios";
|
||||
import { useAccount } from "@wagmi/vue";
|
||||
import { loadActivePositions, usePositions, type Position } from "@/composables/usePositions";
|
||||
|
||||
import { useStake } from "@/composables/useStake";
|
||||
import { useClaim } from "@/composables/useClaim";
|
||||
import { useAdjustTaxRate } from "@/composables/useAdjustTaxRates";
|
||||
|
||||
import { assetsToShares } from "@/contracts/stake";
|
||||
import { getMinStake } from "@/contracts/harb";
|
||||
import { useWallet } from "@/composables/useWallet";
|
||||
import { ref, onMounted, watch, computed, inject, watchEffect } from "vue";
|
||||
import { useStatCollection, loadStats } from "@/composables/useStatCollection";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const demo = sessionStorage.getItem("demo") === "true";
|
||||
const route = useRoute();
|
||||
|
||||
const adjustTaxRate = useAdjustTaxRate();
|
||||
|
||||
const activeTab = ref("stake");
|
||||
const StakeMenuOpen = ref(false);
|
||||
// let minStakeAmount = ref();
|
||||
const taxRate = ref<number>(1.0);
|
||||
// const positions = ref<Array<any>>([]);
|
||||
const loading = ref<boolean>(true);
|
||||
const stakeSnatchLoading = ref<boolean>(false);
|
||||
const stake = useStake();
|
||||
const claim = useClaim();
|
||||
const wallet = useWallet();
|
||||
const statCollection = useStatCollection();
|
||||
|
||||
const { activePositions } = usePositions();
|
||||
|
||||
const floorTax = ref(1);
|
||||
const minStake = ref(0n);
|
||||
const stakeSlots = ref();
|
||||
const supplyFreeze = ref<number>(0);
|
||||
let debounceTimer: ReturnType<typeof setTimeout>;
|
||||
watchEffect(() => {
|
||||
console.log("supplyFreeze");
|
||||
|
||||
if (!stake.stakingAmount) {
|
||||
supplyFreeze.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(async () => {
|
||||
stake.stakingAmountShares = await assetsToShares(stake.stakingAmount);
|
||||
const stakingAmountSharesNumber = bigInt2Number(stake.stakingAmountShares, 18);
|
||||
const stakeableSupplyNumber = bigInt2Number(statCollection.stakeableSupply, 18);
|
||||
minStake.value = await getMinStake();
|
||||
|
||||
console.log(stakingAmountSharesNumber / stakeableSupplyNumber);
|
||||
supplyFreeze.value = stakingAmountSharesNumber / stakeableSupplyNumber;
|
||||
}, 500); // Verzögerung von 500ms
|
||||
});
|
||||
|
||||
const openPositionsAvailable = computed(() => {
|
||||
if (
|
||||
bigInt2Number(statCollection.outstandingStake, 18) + bigInt2Number(stake.stakingAmountShares, 18) <=
|
||||
bigInt2Number(statCollection.stakeTotalSupply, 18) * 0.2
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
console.log("stakeSlots");
|
||||
stakeSlots.value = (supplyFreeze.value * 1000)?.toFixed(2);
|
||||
});
|
||||
|
||||
// const stakeAbleHarbAmount = computed(() => statCollection.harbTotalSupply / 5n);
|
||||
//war das mal so, wurde das geändert --> funktioniert nicht mehr
|
||||
// const minStake = computed(() => stakeAbleHarbAmount.value / 600n);
|
||||
|
||||
const tokenIssuance = computed(() => {
|
||||
if (statCollection.harbTotalSupply === 0n) {
|
||||
return 0n;
|
||||
}
|
||||
|
||||
return (statCollection.nettoToken7d / statCollection.harbTotalSupply) * 100n;
|
||||
});
|
||||
|
||||
function getMinFloorTax() {
|
||||
const taxRate = activePositions.value.reduce((min, item) => {
|
||||
return item.taxRate < min ? item.taxRate : min;
|
||||
}, 1);
|
||||
return taxRate * 100;
|
||||
}
|
||||
|
||||
async function stakeSnatch() {
|
||||
if (snatchAblePositions.value.length === 0) {
|
||||
await stake.snatch(stake.stakingAmount, taxRate.value);
|
||||
} else {
|
||||
const snatchAblePositionsIds = snatchAblePositions.value.map((p: Position) => p.positionId);
|
||||
await stake.snatch(stake.stakingAmount, taxRate.value, snatchAblePositionsIds);
|
||||
}
|
||||
stakeSnatchLoading.value = true;
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
await loadActivePositions();
|
||||
await loadStats();
|
||||
stakeSnatchLoading.value = false;
|
||||
}
|
||||
|
||||
watch(
|
||||
route,
|
||||
async (to) => {
|
||||
console.log("to", to.hash);
|
||||
if (to.hash === "#stake") {
|
||||
console.log("StakeMenuOpen", StakeMenuOpen.value);
|
||||
|
||||
StakeMenuOpen.value = true;
|
||||
|
||||
// if(element){
|
||||
// element.scrollIntoView({behavior: "smooth"});
|
||||
// }
|
||||
|
||||
// if(ele){
|
||||
// window.scrollTo(ele.offsetLeft,ele.offsetTop);
|
||||
// }
|
||||
}
|
||||
},
|
||||
{ flush: "pre", immediate: true, deep: true }
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
//on AccountChange
|
||||
//TODO
|
||||
// await getTotalSupply();
|
||||
// await getTotalSupplyHarb();
|
||||
// await getOutstandingSupply();
|
||||
|
||||
minStake.value = await getMinStake();
|
||||
stake.stakingAmountNumber = minStakeAmount.value;
|
||||
} catch (error) {
|
||||
console.error("error", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
// async function getGraphData(): Promise<Array<any>> {
|
||||
// let res = await axios.post("https://api.studio.thegraph.com/query/47986/harb/version/latest", {
|
||||
// query: "query MyQuery {\n positions {\n id\n lastTaxTime\n owner\n share\n status\n taxRate\n creationTime\n }\n}",
|
||||
// });
|
||||
// return res.data.data.positions;
|
||||
// }
|
||||
|
||||
// const result = useReadContract({
|
||||
// abi: StakeContract.abi,
|
||||
// address: StakeContract.contractAddress,
|
||||
// functionName: "minStake",
|
||||
// args: [],
|
||||
// });
|
||||
// minStakeAmount = result.data;
|
||||
|
||||
const minStakeAmount = computed(() => {
|
||||
console.log("minStake", minStake.value);
|
||||
|
||||
return bigInt2Number(minStake.value, 18);
|
||||
});
|
||||
const maxStakeAmount = computed(() => {
|
||||
if (wallet.balance?.value) {
|
||||
// return Number(balance?.value?.value / 10n ** BigInt(balance?.value!.decimals));
|
||||
console.log("wallet.balance.value", wallet.balance);
|
||||
console.log("formatBigIntDivision(wallet.balance.value, 10n ** 18n)", bigInt2Number(wallet.balance.value, 18));
|
||||
|
||||
return bigInt2Number(wallet.balance.value, 18);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
minStakeAmount,
|
||||
async (newValue) => {
|
||||
console.log("newValue", newValue);
|
||||
if (newValue > stake.stakingAmountNumber && stake.stakingAmountNumber === 0) {
|
||||
stake.stakingAmountNumber = minStakeAmount.value;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function setMaxAmount() {
|
||||
console.log("maxStakeAmount.value", maxStakeAmount.value);
|
||||
|
||||
stake.stakingAmountNumber = maxStakeAmount.value;
|
||||
}
|
||||
const snatchAblePositions = ref<Array<any>>([]);
|
||||
|
||||
watchEffect(async () => {
|
||||
console.log("watchEffect - snatchAblePositions");
|
||||
|
||||
let outStandingStake = statCollection.outstandingStake;
|
||||
let positions = activePositions.value;
|
||||
let selectedTaxRate = taxRate.value;
|
||||
|
||||
// Prüfe, ob bereits offene Positionen verfügbar sind, dann kein Snatching erforderlich
|
||||
if (openPositionsAvailable.value) {
|
||||
snatchAblePositions.value = [];
|
||||
floorTax.value = getMinFloorTax();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("bigInt2Number(outStandingStake, 18)", bigInt2Number(outStandingStake, 18));
|
||||
console.log("bigInt2Number(stake.stakingAmountShares, 18)", bigInt2Number(stake.stakingAmountShares, 18));
|
||||
console.log(
|
||||
"(bigInt2Number(statCollection.stakeTotalSupply, 18) * 0.2)",
|
||||
bigInt2Number(statCollection.stakeTotalSupply, 18) * 0.2
|
||||
);
|
||||
|
||||
// **1. Berechnung der Differenz**
|
||||
const difference =
|
||||
bigInt2Number(outStandingStake, 18) +
|
||||
bigInt2Number(stake.stakingAmountShares, 18) -
|
||||
bigInt2Number(statCollection.stakeTotalSupply, 18) * 0.2;
|
||||
|
||||
console.log("difference", difference);
|
||||
|
||||
// **2. Potentiell snatchbare Positionen holen**
|
||||
const snatchAble = positions.filter((obj: Position) => {
|
||||
if (demo) {
|
||||
return obj.taxRatePercentage < selectedTaxRate;
|
||||
}
|
||||
return obj.taxRatePercentage < selectedTaxRate && !obj.iAmOwner;
|
||||
// return obj.taxRatePercentage < selectedTaxRate;
|
||||
});
|
||||
|
||||
console.log("snatchAblePositions", snatchAble);
|
||||
|
||||
if (snatchAble.length === 0) {
|
||||
snatchAblePositions.value = [];
|
||||
floorTax.value = getMinFloorTax();
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Positionen nach taxRate sortieren (aufsteigend)
|
||||
const sortedPositions = [...snatchAble].sort((a, b) => a.taxRatePercentage - b.taxRatePercentage);
|
||||
|
||||
// 4. Die 50 % der niedrigsten Positionen auswählen
|
||||
const halfIndex = Math.floor(sortedPositions.length / 2);
|
||||
let candidatePositions = sortedPositions.slice(0, halfIndex);
|
||||
|
||||
// Falls zu wenig vorhanden, nimm gleich alle
|
||||
if (candidatePositions.length === 0) {
|
||||
candidatePositions = sortedPositions;
|
||||
}
|
||||
|
||||
// 5. Zufällige Reihenfolge nur innerhalb der günstigeren Hälfte
|
||||
const shuffled = [...candidatePositions].sort(() => Math.random() - 0.5);
|
||||
|
||||
let selectedPositions: Position[] = [];
|
||||
let accumulatedStake = 0;
|
||||
|
||||
const minStakeShares = await assetsToShares(minStake.value);
|
||||
const minStakeShareNumber = bigInt2Number(minStakeShares, 18);
|
||||
|
||||
// Erstversuch: mit der günstigen Hälfte
|
||||
for (const position of shuffled) {
|
||||
const harbDepositShares = await assetsToShares(position.harbDeposit);
|
||||
const harbDepositNumber = bigInt2Number(harbDepositShares, 18);
|
||||
|
||||
if (accumulatedStake <= difference) {
|
||||
selectedPositions.push(position);
|
||||
accumulatedStake += harbDepositNumber;
|
||||
}
|
||||
|
||||
if (accumulatedStake >= difference) break;
|
||||
}
|
||||
|
||||
// Fallback: restliche Positionen – sortiert von günstig nach teuer
|
||||
if (accumulatedStake < difference && candidatePositions.length < sortedPositions.length) {
|
||||
const remainingPositions = sortedPositions.slice(halfIndex); // bereits sortiert!
|
||||
|
||||
for (const position of remainingPositions) {
|
||||
const harbDepositShares = await assetsToShares(position.harbDeposit);
|
||||
const harbDepositNumber = bigInt2Number(harbDepositShares, 18);
|
||||
|
||||
if (!selectedPositions.includes(position)) {
|
||||
selectedPositions.push(position);
|
||||
accumulatedStake += harbDepositNumber;
|
||||
}
|
||||
|
||||
if (accumulatedStake >= difference) break;
|
||||
}
|
||||
}
|
||||
console.log("selectedPositions", selectedPositions);
|
||||
snatchAblePositions.value = selectedPositions;
|
||||
|
||||
// **Berechnung von `floorTax.value`**
|
||||
if (selectedPositions.length > 0) {
|
||||
const taxRatePosition = Math.max(...selectedPositions.map((p) => p.taxRateIndex)) +1;
|
||||
floorTax.value = adjustTaxRate.taxRates[taxRatePosition].year;
|
||||
} else {
|
||||
floorTax.value = getMinFloorTax();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
|
||||
.hold-inner
|
||||
.stake-inner
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 24px
|
||||
.formular
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 8px
|
||||
.row
|
||||
>*
|
||||
flex: 1 1 auto
|
||||
.row-1
|
||||
gap: 12px
|
||||
>:nth-child(2)
|
||||
flex: 0 0 auto
|
||||
.staking-amount
|
||||
.f-input--details
|
||||
display: flex
|
||||
gap: 8px
|
||||
justify-content: flex-end
|
||||
color: #9A9898
|
||||
font-size: 14px
|
||||
.staking-amount-max
|
||||
font-weight: 600
|
||||
&:hover, &:active, &:focus
|
||||
cursor: pointer
|
||||
.row-2
|
||||
justify-content: space-between
|
||||
>*
|
||||
flex: 0 0 30%
|
||||
// >:nth-child(2)
|
||||
// flex: 0 0 22%
|
||||
// >:nth-child(3)
|
||||
// flex: 0 1 28%
|
||||
.stake-arrow
|
||||
align-self: center
|
||||
font-size: 30px
|
||||
</style>
|
||||
57
web-app/src/components/StatsOutput.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div class="stats-output" :styles="styles">
|
||||
<f-card>
|
||||
<h6>{{ props.headline }}</h6>
|
||||
<f-output :name="props.name" :price="props.price">
|
||||
<template #price>
|
||||
<slot name="price"></slot>
|
||||
</template>
|
||||
</f-output>
|
||||
</f-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import FCard from "@/components/fcomponents/FCard.vue"
|
||||
import FOutput from "@/components/fcomponents/FOutput.vue"
|
||||
interface Props {
|
||||
name?: string;
|
||||
headline: string;
|
||||
price: string | number;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
interface Styles {
|
||||
width?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const styles = computed(() => {
|
||||
const returnObject: Styles = {};
|
||||
if (props.width) {
|
||||
returnObject.width = `${props.width}px`;
|
||||
}
|
||||
return returnObject;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.stats-output
|
||||
.f-card
|
||||
background-color: var(--color-secondary)
|
||||
h6
|
||||
margin: 0
|
||||
font-size: 16px
|
||||
letter-spacing: 0.15px
|
||||
font-weight: 300
|
||||
.f-card__body
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 8px
|
||||
padding: 8px
|
||||
height: 100%
|
||||
.token-price-wrapper
|
||||
padding: 8px 16px
|
||||
</style>
|
||||
121
web-app/src/components/Toast.vue
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
<template>
|
||||
<f-card :bg-color="bgcolor" :border-width="0" box-shadow="unset">
|
||||
<div class="info-popup">
|
||||
<div class="info-popup__header">
|
||||
<h6>{{ props.header }}</h6>
|
||||
</div>
|
||||
<div class="info-popup__body">
|
||||
<div>
|
||||
{{ props.subheader }}
|
||||
</div>
|
||||
<div v-if="props.value">
|
||||
<span class="number-big">{{ props.value }}</span
|
||||
> <span>{{ props.token }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!props.value">
|
||||
<hr />
|
||||
</div>
|
||||
<div class="info-popup__body2" v-if="props.info" v-html="props.info"></div>
|
||||
<div class="info-popup__footer">
|
||||
<f-button light block small @click="closeToast">Okay</f-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="body">
|
||||
<h6 class="header">test{{ props.header }}</h6>
|
||||
<div class="subheader toast-header">test{{ props.subheader }}</div>
|
||||
<div class="amount-number">
|
||||
<div class="token-amount number-mobile">test{{ props.value }}</div>
|
||||
<div class="token-name">test{{ props.token }}</div>
|
||||
</div>
|
||||
<div class="info toast-body">test{{ props.info }}</div>
|
||||
</div> -->
|
||||
</f-card>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { getCurrentInstance, computed } from "vue";
|
||||
import FButton from "@/components/fcomponents/FButton.vue";
|
||||
import FCard from "@/components/fcomponents/FCard.vue";
|
||||
import { useToast } from "vue-toastification";
|
||||
import type { ToastID } from "vue-toastification/dist/types/types";
|
||||
|
||||
const props = defineProps({
|
||||
value: String,
|
||||
header: String,
|
||||
subheader: String,
|
||||
info: String,
|
||||
token: String,
|
||||
type: String,
|
||||
});
|
||||
|
||||
const bgcolor = computed(() => {
|
||||
let color = "white";
|
||||
console.log("props.type");
|
||||
console.log(props.type);
|
||||
switch (props.type) {
|
||||
case "info":
|
||||
color = "#5f4884";
|
||||
break;
|
||||
case "error":
|
||||
color = "#8B0000";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return color;
|
||||
});
|
||||
|
||||
// element.classList.add("toast-open");
|
||||
const toast = useToast();
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
instance!.parent!.parent!.vnode!.el!.classList.add("toast-open");
|
||||
const id = instance!.attrs["toast-id"] as ToastID;
|
||||
|
||||
console.log("instance", instance!.attrs["toast-id"]);
|
||||
|
||||
function closeToast() {
|
||||
instance!.parent!.parent!.vnode!.el!.classList.remove("toast-open");
|
||||
toast.dismiss(id);
|
||||
}
|
||||
</script>
|
||||
<style lang="sass">
|
||||
.info-popup
|
||||
width: 342px
|
||||
display: flex
|
||||
color: var(--color-white)
|
||||
flex-direction: column
|
||||
gap: 16px
|
||||
font-size: var(--font-body1)
|
||||
text-align: center
|
||||
hr
|
||||
border: 1px solid var(--color-grey)
|
||||
.info-popup__header
|
||||
h6
|
||||
margin: 0
|
||||
color: var(--color-white)
|
||||
.info-popup__body
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 8px
|
||||
|
||||
.Vue-Toastification__container
|
||||
&.toast-open
|
||||
background-color: rgb(15, 15, 15, 0.7)
|
||||
height: 100vh
|
||||
width: 100vw
|
||||
position: fixed
|
||||
left: 0
|
||||
bottom: 0
|
||||
z-index: 10
|
||||
@media (min-width: 992px)
|
||||
background-color: unset
|
||||
&.top-center
|
||||
@media (min-width: 600px)
|
||||
left: unset
|
||||
margin-left: unset
|
||||
.Vue-Toastification__toast
|
||||
box-shadow: unset
|
||||
&.modal-overlay
|
||||
background-color: unset
|
||||
</style>
|
||||
87
web-app/src/components/WelcomeItem.vue
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<template>
|
||||
<div class="item">
|
||||
<i>
|
||||
<slot name="icon"></slot>
|
||||
</i>
|
||||
<div class="details">
|
||||
<h3>
|
||||
<slot name="heading"></slot>
|
||||
</h3>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.details {
|
||||
flex: 1;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
i {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.item {
|
||||
margin-top: 0;
|
||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
i {
|
||||
top: calc(50% - 25px);
|
||||
left: -26px;
|
||||
position: absolute;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.item:before {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:after {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:first-of-type:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.item:last-of-type:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
68
web-app/src/components/chart/ChartComplete.vue
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<chart-js
|
||||
:snatchedPositions="snatchPositions.map((obj) => obj.id)"
|
||||
:positions="activePositions"
|
||||
:dark="darkTheme"
|
||||
></chart-js>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ChartJs from "@/components/chart/ChartJs.vue";
|
||||
import {bigInt2Number, formatBigIntDivision} from "@/utils/helper";
|
||||
import { computed, ref } from "vue";
|
||||
import { useStatCollection } from "@/composables/useStatCollection";
|
||||
import { useStake } from "@/composables/useStake";
|
||||
import { loadActivePositions, usePositions, type Position } from "@/composables/usePositions";
|
||||
import { useDark } from "@/composables/useDark";
|
||||
const { darkTheme } = useDark();
|
||||
|
||||
const { activePositions, myActivePositions, tresholdValue, myClosedPositions, createRandomPosition } = usePositions();
|
||||
const ignoreOwner = ref(false);
|
||||
|
||||
const taxRate = ref<number>(1.0);
|
||||
|
||||
const minStakeAmount = computed(() => {
|
||||
console.log("minStake", minStake.value);
|
||||
|
||||
return formatBigIntDivision(minStake.value, 10n ** 18n);
|
||||
});
|
||||
|
||||
const stakeAbleHarbAmount = computed(() => statCollection.harbTotalSupply / 5n);
|
||||
|
||||
const minStake = computed(() => stakeAbleHarbAmount.value / 600n);
|
||||
|
||||
const stake = useStake();
|
||||
const statCollection = useStatCollection();
|
||||
const snatchPositions = computed(() => {
|
||||
if (
|
||||
bigInt2Number(statCollection.outstandingStake, 18) + stake.stakingAmountNumber <=
|
||||
bigInt2Number(statCollection.harbTotalSupply, 18) * 0.2
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
//Differenz aus outstandingSupply und totalSupply bestimmen, wie viel HARB kann zum Snatch verwendet werden
|
||||
const difference =
|
||||
bigInt2Number(statCollection.outstandingStake, 18) +
|
||||
stake.stakingAmountNumber -
|
||||
bigInt2Number(statCollection.harbTotalSupply, 18) * 0.2;
|
||||
console.log("difference", difference);
|
||||
|
||||
//Division ohne Rest, um zu schauen wie viele Positionen gesnatched werden könnten
|
||||
const snatchAblePositionsCount = Math.floor(difference / minStakeAmount.value);
|
||||
|
||||
//wenn mehr als 0 Positionen gesnatched werden könnten, wird geschaut wie viele Positionen in Frage kommen
|
||||
if (snatchAblePositionsCount > 0) {
|
||||
const snatchAblePositions = activePositions.value.filter((obj: Position) => {
|
||||
if (ignoreOwner.value) {
|
||||
return obj.taxRatePercentage < taxRate.value;
|
||||
}
|
||||
return obj.taxRatePercentage < taxRate.value && !obj.iAmOwner;
|
||||
});
|
||||
const slicedArray = snatchAblePositions.slice(0, snatchAblePositionsCount);
|
||||
return slicedArray;
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
</script>
|
||||
464
web-app/src/components/chart/ChartJs.vue
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
<template>
|
||||
<div class="positions-graph">
|
||||
<div class="chart-modal" :class="{ 'chart--fullscreen': fullscreenOpen }" @click="toggleFullscreen">
|
||||
<div class="chart-inner" @click.stop>
|
||||
<div class="chart--header">
|
||||
<div class="chart--actions" v-if="props.positions?.length > 0">
|
||||
<reset-zoom-button @click="resetZoom"></reset-zoom-button>
|
||||
<fullscreen-button v-if="isMobile" @click="toggleFullscreen"></fullscreen-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="chart--body"
|
||||
:class="{ 'disable-actions': props.positions?.length === 0, dark: props.dark }"
|
||||
>
|
||||
<canvas ref="Chart1"></canvas>
|
||||
<template v-if="props.positions?.length === 0">
|
||||
<p class="chart--no-positions">No positions</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tooltipRef" ref="tooltipRef" :class="{ iAmOwner: activePosition?.iAmOwner }">
|
||||
<template v-if="activePosition?.iAmOwner">
|
||||
<p>Your staking position</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>Staking position</p>
|
||||
</template>
|
||||
<b>ID {{ activePosition?.id }}</b>
|
||||
<b>{{ activePosition?.amount }} $KRK</b>
|
||||
<b>Tax {{ activePosition?.taxRatePercentage }} %</b>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch, shallowRef, computed } from "vue";
|
||||
import {
|
||||
BarController,
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
Chart,
|
||||
LineController,
|
||||
LineElement,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
Tooltip,
|
||||
} from "chart.js";
|
||||
// import { Chart } from "chart.js";
|
||||
import zoomPlugin from "chartjs-plugin-zoom";
|
||||
import { useAccount } from "@wagmi/vue";
|
||||
import { useMobile } from "@/composables/useMobile";
|
||||
import type { Position } from "@/composables/usePositions";
|
||||
import FullscreenButton from "@/components/chart/FullscreenButton.vue";
|
||||
import ResetZoomButton from "@/components/chart/ResetZoomButton.vue";
|
||||
Chart.register(
|
||||
zoomPlugin,
|
||||
LinearScale,
|
||||
CategoryScale,
|
||||
BarController,
|
||||
BarElement,
|
||||
LineController,
|
||||
LineElement,
|
||||
PointElement,
|
||||
Tooltip
|
||||
);
|
||||
|
||||
interface Props {
|
||||
positions: Array<Position>;
|
||||
snatchedPositions: Array<string>;
|
||||
dark?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
dark: false,
|
||||
});
|
||||
|
||||
const Chart1 = ref();
|
||||
const fullscreenOpen = ref(false);
|
||||
const myChart = ref();
|
||||
const activePosition = ref();
|
||||
const tooltipRef = ref();
|
||||
const positionSnatched = ref();
|
||||
|
||||
const account = useAccount();
|
||||
const isMobile = useMobile();
|
||||
|
||||
function resetZoom() {
|
||||
console.log("resetZoom", { Chart1, myChart });
|
||||
myChart.value.value.resetZoom();
|
||||
}
|
||||
|
||||
function toggleFullscreen() {
|
||||
if (fullscreenOpen.value) {
|
||||
document.body.style.position = "unset";
|
||||
document.body.style.overflow = "unset";
|
||||
} else {
|
||||
document.body.style.overflow = "hidden";
|
||||
document.body.style.position = "relative";
|
||||
}
|
||||
fullscreenOpen.value = !fullscreenOpen.value;
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.positions,
|
||||
(newData) => {
|
||||
console.log("props.positions", props.positions);
|
||||
|
||||
myChart.value.value.destroy();
|
||||
renderChart(props.positions);
|
||||
|
||||
// myChart.value.value.update();
|
||||
// myChart.value.datasets[0].bars[0].fillColor = "green"; //bar 1
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.dark,
|
||||
(newData) => {
|
||||
myChart.value.value.destroy();
|
||||
renderChart(props.positions);
|
||||
|
||||
// myChart.value.value.update();
|
||||
// myChart.value.datasets[0].bars[0].fillColor = "green"; //bar 1
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.snatchedPositions,
|
||||
(newData) => {
|
||||
// myChart.value.value.destroy();
|
||||
// renderChart(props.positions);
|
||||
let positionIndex = 0;
|
||||
if (myChart.value.value.data?.datasets[0]) {
|
||||
let backgroundColorArray = myChart.value.value.data.datasets[0].backgroundColor;
|
||||
|
||||
for (let index = 0; index < backgroundColorArray.length; index++) {
|
||||
const position: Position = myChart.value.value.data.datasets[0].data[index];
|
||||
if (!position) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!position.iAmOwner) {
|
||||
positionIndex++;
|
||||
}
|
||||
if (positionIndex <= props.snatchedPositions.length && props.snatchedPositions.includes(position.id)) {
|
||||
backgroundColorArray[index] = "##7550AE";
|
||||
} else {
|
||||
backgroundColorArray[index] = "##7550AE";
|
||||
// const position = myChart.value.value.data.datasets[0].data[index];
|
||||
if (position.iAmOwner) {
|
||||
backgroundColorArray[index] = "#7550AE";
|
||||
} else if (props.dark) {
|
||||
backgroundColorArray[index] = "white";
|
||||
} else {
|
||||
backgroundColorArray[index] = "black";
|
||||
}
|
||||
}
|
||||
}
|
||||
myChart.value.value.data.datasets[0].backgroundColor = backgroundColorArray;
|
||||
|
||||
myChart.value.value.ctx.save();
|
||||
myChart.value.value?.update();
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
const externalTooltipHandler = (context: any) => {
|
||||
const { chart, tooltip } = context;
|
||||
const tooltipEl = tooltipRef.value;
|
||||
|
||||
// Tooltip ausblenden, wenn keine Daten angezeigt werden sollen
|
||||
if (!tooltip.opacity) {
|
||||
tooltipEl.style.opacity = "0";
|
||||
tooltipEl.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
// Aktive Position setzen (Daten des angezeigten Punktes)
|
||||
activePosition.value = tooltip.dataPoints[0].element.$context.raw;
|
||||
|
||||
// Positionierung des Tooltips
|
||||
const { offsetLeft: chartX, offsetTop: chartY } = chart.canvas;
|
||||
|
||||
// Tooltip anpassen
|
||||
tooltipEl.style.opacity = "1";
|
||||
tooltipEl.style.display = "flex";
|
||||
tooltipEl.style.position = "absolute";
|
||||
|
||||
// Tooltip mittig über dem Punkt platzieren
|
||||
tooltipEl.style.left = `${chartX + tooltip.caretX}px`;
|
||||
tooltipEl.style.top = `${chartY + tooltip.y}px`;
|
||||
|
||||
// Tooltip für saubere Mitte ausrichten
|
||||
tooltipEl.style.transform = "translateX(-50%)";
|
||||
tooltipEl.style.pointerEvents = "none";
|
||||
};
|
||||
|
||||
function renderChart(data: any) {
|
||||
console.log("renderChart");
|
||||
|
||||
const backgroundColors = [];
|
||||
const data1 = data.map((obj: any) => {
|
||||
return {
|
||||
...obj,
|
||||
// taxRatePercentage: obj.taxRate * 100,
|
||||
iAmOwner: obj.owner?.toLowerCase() === account.address.value?.toLowerCase(),
|
||||
};
|
||||
});
|
||||
console.log("data1", data1);
|
||||
|
||||
for (let index = 0; index < data1.length; index++) {
|
||||
const position = data1[index];
|
||||
|
||||
// if(index < props.snatchedPositions){
|
||||
// backgroundColors.push("rgba(26,84,244, 0.5)");
|
||||
// positionSnatched.value = index;
|
||||
|
||||
// }
|
||||
if (position.iAmOwner) {
|
||||
backgroundColors.push("rgba(117,80,174, 0.8)");
|
||||
} else if (props.dark) {
|
||||
backgroundColors[index] = "white";
|
||||
} else {
|
||||
backgroundColors[index] = "black";
|
||||
}
|
||||
}
|
||||
|
||||
console.log("backgroundColors", backgroundColors);
|
||||
|
||||
myChart.value = shallowRef(
|
||||
new Chart(Chart1.value, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: data1.map((row: any) => row.id),
|
||||
datasets: [
|
||||
{
|
||||
type: "line",
|
||||
label: "TaxRate",
|
||||
data: data1,
|
||||
backgroundColor: ["#7550AE"],
|
||||
borderColor: ["#7550AE"],
|
||||
yAxisID: "y",
|
||||
parsing: {
|
||||
yAxisKey: "taxRatePercentage",
|
||||
xAxisKey: "id",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "bar",
|
||||
label: "Amount",
|
||||
data: data1,
|
||||
backgroundColor: backgroundColors,
|
||||
yAxisID: "y1",
|
||||
parsing: {
|
||||
yAxisKey: "amount",
|
||||
xAxisKey: "id",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: {
|
||||
duration: 1,
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: "index",
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
position: "nearest",
|
||||
external: externalTooltipHandler,
|
||||
},
|
||||
zoom: {
|
||||
pan: {
|
||||
enabled: true,
|
||||
mode: "xy",
|
||||
},
|
||||
limits: {
|
||||
y: { min: 0 },
|
||||
},
|
||||
zoom: {
|
||||
wheel: {
|
||||
enabled: true,
|
||||
},
|
||||
pinch: {
|
||||
enabled: true,
|
||||
},
|
||||
mode: "xy",
|
||||
onZoomComplete(object: any) {
|
||||
// This update is needed to display up to date zoom level in the title.
|
||||
// Without this, previous zoom level is displayed.
|
||||
// The reason is: title uses the same beforeUpdate hook, and is evaluated before zoom.
|
||||
object.chart?.update("none");
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
// type: "linear",
|
||||
display: true,
|
||||
position: "left",
|
||||
max: Math.max(...data.map((o: any) => o.taxRate)) * 100 * 1.5,
|
||||
// min: 0,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Tax",
|
||||
color: "#7550AE",
|
||||
font: {
|
||||
size: 16, // Hier die Schriftgröße ändern (z. B. 16px)
|
||||
weight: "bold", // Falls du den Text fett machen möchtest
|
||||
},
|
||||
},
|
||||
},
|
||||
y1: {
|
||||
// type: "linear",
|
||||
display: true,
|
||||
position: "right",
|
||||
max: Math.max(...data.map((o: any) => o.amount)) * 1.5,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Slots",
|
||||
color: "white",
|
||||
font: {
|
||||
size: 16, // Hier die Schriftgröße ändern (z. B. 16px)
|
||||
weight: "bold", // Falls du den Text fett machen möchtest
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
// min: 0,
|
||||
},
|
||||
x: {
|
||||
display: true,
|
||||
ticks: {
|
||||
display: false,
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "Positions",
|
||||
color: "white",
|
||||
font: {
|
||||
size: 16, // Hier die Schriftgröße ändern (z. B. 16px)
|
||||
weight: "bold", // Falls du den Text fett machen möchtest
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
} as any)
|
||||
);
|
||||
}
|
||||
onMounted(() => {
|
||||
renderChart(props.positions);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
$margin: 72px
|
||||
.positions-graph
|
||||
width: 100%
|
||||
.tooltipRef
|
||||
position: absolute
|
||||
background-color: rgba(133, 133, 133, 1)
|
||||
padding: 8px
|
||||
border-radius: 12px
|
||||
display: flex
|
||||
flex-direction: column
|
||||
opacity: 0
|
||||
display: none
|
||||
pointer-events: none
|
||||
z-index: 20
|
||||
color: white
|
||||
&.iAmOwner
|
||||
background-color: var(--color-based-blue)
|
||||
|
||||
.chart-modal
|
||||
&.chart--fullscreen
|
||||
background-color: rgba(0, 0, 0, 0.32)
|
||||
position: absolute
|
||||
top: 0
|
||||
height: 100vh
|
||||
width: 100%
|
||||
left: 0
|
||||
overflow: hidden
|
||||
z-index: 11
|
||||
.chart-inner
|
||||
background-color: var(--color-card-bg)
|
||||
height: auto
|
||||
display: flex
|
||||
flex-direction: column
|
||||
transition: all 200ms ease-in-out
|
||||
@media (min-width: 992px)
|
||||
//height: calc( 100% - $margin*2)
|
||||
width: 90%
|
||||
margin: auto
|
||||
min-height: 100%
|
||||
//margin: $margin
|
||||
.chart--body
|
||||
flex: 1 1 auto
|
||||
height: 100%
|
||||
canvas
|
||||
height: auto
|
||||
.chart-inner
|
||||
.chart--header
|
||||
flex: 0 0 50px
|
||||
color: red
|
||||
padding: 8px
|
||||
display: flex
|
||||
.chart--actions
|
||||
gap: 12px
|
||||
margin-left: auto
|
||||
display: flex
|
||||
.chart--body
|
||||
height: 300px !important
|
||||
position: relative
|
||||
&.disable-actions
|
||||
pointer-events: none
|
||||
&::before
|
||||
content: ''
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
background-color: rgba(255, 255, 255, 0.5)
|
||||
z-index: 1
|
||||
|
||||
&.dark
|
||||
&::before
|
||||
background-color: rgba(0, 0, 0, 0.5)
|
||||
.chart--no-positions
|
||||
color: var(--color-font)
|
||||
font-size: 30px
|
||||
margin: auto
|
||||
padding: 16px
|
||||
text-align: center
|
||||
position: absolute
|
||||
left: 50%
|
||||
top: 50%
|
||||
transform: translate(-50%, -50%)
|
||||
z-index: 2 /* Ensure this is above the background overlay */
|
||||
</style>
|
||||
30
web-app/src/components/chart/FullscreenButton.vue
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<button class="chart-button">
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
class="stroke"
|
||||
d="M10.8027 1.00195H16.002C16.3433 1.00195 16.6201 1.27869 16.6201 1.62006V6.81927"
|
||||
stroke-width="1.85431"
|
||||
/>
|
||||
<path
|
||||
class="stroke"
|
||||
d="M16.6191 10.5273L16.6191 15.7266C16.6191 16.0679 16.3424 16.3447 16.001 16.3447L10.8018 16.3447"
|
||||
stroke-width="1.85431"
|
||||
/>
|
||||
<path
|
||||
class="stroke"
|
||||
d="M7.0918 16.3457L1.89258 16.3457C1.55121 16.3457 1.27448 16.069 1.27448 15.7276L1.27448 10.5284"
|
||||
stroke-width="1.85431"
|
||||
/>
|
||||
<path
|
||||
class="stroke"
|
||||
d="M1.27539 6.81836L1.27539 1.61914C1.27539 1.27777 1.55213 1.00104 1.8935 1.00104L7.09271 1.00104"
|
||||
stroke-width="1.85431"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
@use 'style'
|
||||
</style>
|
||||
22
web-app/src/components/chart/ResetZoomButton.vue
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<template>
|
||||
<button class="chart-button">
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="path-1-inside-1_3412_52485" fill="white">
|
||||
<path
|
||||
d="M12.9797 15.2305C11.7286 16.2335 10.2045 16.8366 8.60589 16.9613C7.00726 17.086 5.4081 16.7265 4.01664 15.9296C2.62518 15.1327 1.50588 13.9353 0.804464 12.4934C0.103046 11.0515 -0.14799 9.43173 0.0840395 7.84511C0.316069 6.2585 1.02041 4.77849 2.10537 3.59778C3.19033 2.41708 4.60564 1.59038 6.16702 1.22532C7.72841 0.860254 9.36354 0.973741 10.8595 1.551C12.3555 2.12826 13.643 3.14256 14.5545 4.46182L13.109 5.46048C12.3981 4.4315 11.3938 3.64038 10.227 3.19013C9.0602 2.73989 7.78485 2.65137 6.56702 2.93611C5.34918 3.22084 4.24529 3.86565 3.39905 4.78656C2.55282 5.70747 2.00345 6.86183 1.82248 8.09935C1.6415 9.33686 1.8373 10.6002 2.38439 11.7249C2.93147 12.8495 3.80449 13.7835 4.88979 14.405C5.97508 15.0266 7.22237 15.307 8.46926 15.2097C9.71615 15.1124 10.9049 14.642 11.8807 13.8597L12.9797 15.2305Z"
|
||||
/>
|
||||
</mask>
|
||||
<path
|
||||
d="M12.9797 15.2305C11.7286 16.2335 10.2045 16.8366 8.60589 16.9613C7.00726 17.086 5.4081 16.7265 4.01664 15.9296C2.62518 15.1327 1.50588 13.9353 0.804464 12.4934C0.103046 11.0515 -0.14799 9.43173 0.0840395 7.84511C0.316069 6.2585 1.02041 4.77849 2.10537 3.59778C3.19033 2.41708 4.60564 1.59038 6.16702 1.22532C7.72841 0.860254 9.36354 0.973741 10.8595 1.551C12.3555 2.12826 13.643 3.14256 14.5545 4.46182L13.109 5.46048C12.3981 4.4315 11.3938 3.64038 10.227 3.19013C9.0602 2.73989 7.78485 2.65137 6.56702 2.93611C5.34918 3.22084 4.24529 3.86565 3.39905 4.78656C2.55282 5.70747 2.00345 6.86183 1.82248 8.09935C1.6415 9.33686 1.8373 10.6002 2.38439 11.7249C2.93147 12.8495 3.80449 13.7835 4.88979 14.405C5.97508 15.0266 7.22237 15.307 8.46926 15.2097C9.71615 15.1124 10.9049 14.642 11.8807 13.8597L12.9797 15.2305Z"
|
||||
stroke="white"
|
||||
stroke-width="3.54886"
|
||||
mask="url(#path-1-inside-1_3412_52485)"
|
||||
/>
|
||||
<path d="M16.0005 6.89955L13.89 0.404049L9.32 5.47956L16.0005 6.89955Z" fill="white" />
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
@use 'style'
|
||||
</style>
|
||||
18
web-app/src/components/chart/style.sass
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
.chart-button
|
||||
padding: 0 18px
|
||||
border-radius: 12px
|
||||
display: flex
|
||||
align-items: center
|
||||
border: none
|
||||
height: 32px
|
||||
background-color: var(--chart-button-background-color)
|
||||
transition: .3s ease
|
||||
svg
|
||||
path
|
||||
&.stroke
|
||||
stroke: white
|
||||
&.fill
|
||||
fill: white
|
||||
&:hover, &:active
|
||||
background-color: var(--chart-button-background-color--hovered)
|
||||
cursor: pointer
|
||||
261
web-app/src/components/collapse/CollapseActive.vue
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
<template>
|
||||
<f-collapse class="f-collapse-active" @collapse:opened="loadActivePositionData" :loading="loading">
|
||||
<template v-slot:header>
|
||||
<div class="collapse-header">
|
||||
<div class="collapse-header-row1">
|
||||
<div><span class="subheader2">Tax</span> {{ props.taxRate }} %</div>
|
||||
<f-button size="tiny" @click="payTax(props.id)">Pay Tax</f-button>
|
||||
<div class="position-id">
|
||||
<span class="subheader2">ID</span> <span class="number-small">{{ props.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-header-row2">
|
||||
<div>
|
||||
<div class="profit-stats-item">
|
||||
<div><b>Initial Stake</b></div>
|
||||
<div>{{ compactNumber(props.amount) }} $KRK</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tags-list">
|
||||
<f-tag v-if="tag">{{ tag }}</f-tag>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="collapse-amount">
|
||||
<span class="number-small">{{ compactNumber(props.amount) }}</span>
|
||||
<span class="caption"> $KRK</span>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
<div class="collapsed-body">
|
||||
<div class="profit-stats-wrapper">
|
||||
<div class="profit-stats-item">
|
||||
<div><b>Tax Paid</b></div>
|
||||
<div>{{ taxPaidGes }} $KRK</div>
|
||||
</div>
|
||||
<div class="profit-stats-item">
|
||||
<div><b>Issuance Earned</b></div>
|
||||
<div>{{ profit }} $KRK</div>
|
||||
</div>
|
||||
<div class="profit-stats-item profit-stats-total">
|
||||
<div><b>Total</b></div>
|
||||
<div>{{ total.toFixed(5) }} $KRK</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapsed-body--actions">
|
||||
<div :class="{ 'collapse-menu-open': showTaxMenu }">
|
||||
<f-button size="small" dense block outlined v-if="adjustTaxRate.state === 'SignTransaction'"
|
||||
>Sign Transaction ...</f-button
|
||||
>
|
||||
<f-button
|
||||
size="small"
|
||||
dense
|
||||
outlined
|
||||
block
|
||||
v-else-if="adjustTaxRate.state === 'Waiting'"
|
||||
@click="unstakePosition"
|
||||
>Waiting ...</f-button
|
||||
>
|
||||
<f-button
|
||||
size="small"
|
||||
dense
|
||||
block
|
||||
v-else-if="adjustTaxRate.state === 'Action' && !showTaxMenu"
|
||||
@click="showTaxMenu = true"
|
||||
>Adjust Tax Rate</f-button
|
||||
>
|
||||
<template v-else>
|
||||
<div class="collapse-menu-input">
|
||||
<f-select :items="filteredTaxRates" v-model="newTaxRate"> </f-select>
|
||||
</div>
|
||||
<div>
|
||||
<f-button size="small" dense @click="changeTax(props.id, newTaxRate)">Confirm</f-button>
|
||||
<f-button size="small" dense outlined @click="showTaxMenu = false">Cancel</f-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div></div>
|
||||
<div>
|
||||
<f-button size="small" dense block outlined v-if="unstake.state === 'SignTransaction'"
|
||||
>Sign Transaction ...</f-button
|
||||
>
|
||||
<f-button size="small" dense outlined block v-else-if="unstake.state === 'Waiting'"
|
||||
>Waiting ...</f-button
|
||||
>
|
||||
<f-button size="small" dense block v-else-if="unstake.state === 'Unstakeable'" @click="unstakePosition"
|
||||
>Unstake</f-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</f-collapse>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FButton from "@/components/fcomponents/FButton.vue";
|
||||
import FTag from "@/components/fcomponents/FTag.vue";
|
||||
import FSelect from "@/components/fcomponents/FSelect.vue";
|
||||
import FCollapse from "@/components/fcomponents/FCollapse.vue";
|
||||
import { compactNumber, formatBigNumber } from "@/utils/helper";
|
||||
import { useUnstake } from "@/composables/useUnstake";
|
||||
import { useAdjustTaxRate } from "@/composables/useAdjustTaxRates";
|
||||
import { computed, ref, onMounted } from "vue";
|
||||
import { getTaxDue, payTax } from "@/contracts/stake";
|
||||
import { type Position, loadPositions } from "@/composables/usePositions";
|
||||
import { useStatCollection } from "@/composables/useStatCollection";
|
||||
|
||||
import { formatUnits } from "viem";
|
||||
|
||||
const unstake = useUnstake();
|
||||
const adjustTaxRate = useAdjustTaxRate();
|
||||
const statCollection = useStatCollection();
|
||||
|
||||
const props = defineProps<{
|
||||
taxRate: number;
|
||||
treshold: number;
|
||||
id: bigint;
|
||||
amount: number;
|
||||
position: Position;
|
||||
}>();
|
||||
|
||||
const showTaxMenu = ref(false);
|
||||
const newTaxRate = ref<number>(1);
|
||||
const taxDue = ref<bigint>();
|
||||
const taxPaidGes = ref<string>();
|
||||
const profit = ref<number>();
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
const tag = computed(() => {
|
||||
if (props.taxRate < props.treshold) {
|
||||
return "Low Tax!";
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
const total = computed(() => props.amount + profit.value! + -taxPaidGes.value!);
|
||||
|
||||
async function changeTax(id: bigint, newTaxRate: number) {
|
||||
await adjustTaxRate.changeTax(id, newTaxRate);
|
||||
showTaxMenu.value = false;
|
||||
}
|
||||
|
||||
async function unstakePosition() {
|
||||
console.log(props.id);
|
||||
await unstake.exitPosition(props.id);
|
||||
loading.value = true;
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
console.log("loadPositions begin");
|
||||
await loadPositions();
|
||||
console.log("loadPositions end");
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
async function loadActivePositionData() {
|
||||
console.log("loadActivePositionData", props.position);
|
||||
|
||||
//loadTaxDue
|
||||
taxDue.value = await getTaxDue(props.id);
|
||||
console.log("taxDue", taxDue.value);
|
||||
taxPaidGes.value = formatBigNumber(taxDue.value + BigInt(props.position.taxPaid), 18);
|
||||
console.log("loadActivePositionData", taxPaidGes.value);
|
||||
|
||||
//loadTotalSupply
|
||||
|
||||
const multiplier =
|
||||
Number(formatUnits(props.position.totalSupplyInit, 18)) /
|
||||
Number(formatUnits(statCollection.harbTotalSupply, 18));
|
||||
console.log("props.position.totalSupplyInit", props.position.totalSupplyInit);
|
||||
|
||||
console.log("multiplier", multiplier);
|
||||
|
||||
profit.value =
|
||||
Number(formatUnits(statCollection.harbTotalSupply, 18)) * multiplier -
|
||||
Number(formatUnits(statCollection.harbTotalSupply, 18));
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const taxRate = adjustTaxRate.taxRates.find((obj) => obj.index === props.position.taxRateIndex + 1);
|
||||
|
||||
if (taxRate) {
|
||||
newTaxRate.value = taxRate.year;
|
||||
}
|
||||
});
|
||||
|
||||
const filteredTaxRates = computed(() => adjustTaxRate.taxRates.filter((obj) => obj.year > props.taxRate));
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
@use 'collapse'
|
||||
.f-collapse
|
||||
&.f-collapse-active
|
||||
background-color: #07111B
|
||||
.collapse-header
|
||||
width: 100%
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
flex-direction: column
|
||||
position: relative
|
||||
gap: 16px
|
||||
.collapse-header-row1
|
||||
display: flex
|
||||
flex-direction: row
|
||||
gap: 16px
|
||||
align-items: center
|
||||
// margin-right: 32px
|
||||
|
||||
.position-id
|
||||
margin-left: auto
|
||||
.collapse-header-row2
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
>div
|
||||
width: 50%
|
||||
min-height: 22px
|
||||
.profit-stats-item
|
||||
display: grid
|
||||
grid-template-columns: 1fr 1fr
|
||||
gap: 8px
|
||||
div
|
||||
&:last-child
|
||||
text-align: right
|
||||
.tags-list
|
||||
margin-right: 32px
|
||||
text-align: right
|
||||
.collapsableDiv
|
||||
.collapsed-body
|
||||
display: flex
|
||||
flex-direction: column
|
||||
margin-right: 32px
|
||||
.collapsed-body--header
|
||||
justify-content: space-between
|
||||
.profit-stats-wrapper
|
||||
display: flex
|
||||
|
||||
flex-direction: column
|
||||
gap: 4px
|
||||
margin: 0
|
||||
margin-top: 4px
|
||||
width: 100%
|
||||
color: white
|
||||
@media (min-width: 768px)
|
||||
width: 50%
|
||||
margin: 4px auto 0 0
|
||||
.profit-stats-item
|
||||
display: grid
|
||||
grid-template-columns: 1fr 1fr
|
||||
gap: 8px
|
||||
div
|
||||
&:last-child
|
||||
text-align: right
|
||||
&.profit-stats-total
|
||||
border-top: 2px solid var(--color-font)
|
||||
padding-top: 2px
|
||||
.collapsed-body--actions
|
||||
.collapse-menu-open
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 4px
|
||||
flex: 1 1 auto
|
||||
.collapse-menu-input
|
||||
display: flex
|
||||
align-items: center
|
||||
</style>
|
||||
63
web-app/src/components/collapse/CollapseHistory.vue
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<f-collapse class="f-collapse-history">
|
||||
<template v-slot:header>
|
||||
<div class="collapse-header">
|
||||
<div><span class="subheader2">Tax</span> {{ props.taxRate }} %</div>
|
||||
<div>
|
||||
<span class="subheader2">ID</span> <span class="number-small">{{ props.id }}</span>
|
||||
</div>
|
||||
<div class="collapse-amount">
|
||||
<span class="number-small">{{ compactNumber(props.amount) }}</span>
|
||||
<span class="caption"> $KRK</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="collapsed-body history">
|
||||
<div>
|
||||
<span class="subheader2">Tax paid</span><span class="number-small">{{ props.taxPaid }}</span
|
||||
><span class="caption"> $KRK</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="subheader2">Profit</span><span class="number-small">{{ profit }}</span
|
||||
><span class="caption"> $KRK</span>
|
||||
</div>
|
||||
</div>
|
||||
</f-collapse>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Position } from "@/composables/usePositions";
|
||||
import FCollapse from "@/components/fcomponents/FCollapse.vue";
|
||||
import { compactNumber } from "@/utils/helper";
|
||||
import { formatUnits } from "viem";
|
||||
|
||||
import { computed } from "vue";
|
||||
const props = defineProps<{
|
||||
taxRate: number;
|
||||
taxPaid: string;
|
||||
treshold: number;
|
||||
id: bigint;
|
||||
amount: number;
|
||||
position: Position;
|
||||
}>();
|
||||
|
||||
const profit = computed(() => {
|
||||
const multiplier =
|
||||
Number(formatUnits(props.position.totalSupplyInit, 18)) /
|
||||
Number(formatUnits(props.position.totalSupplyEnd!, 18));
|
||||
console.log("multiplier", multiplier);
|
||||
console.log("props.position.amount", props.position.amount);
|
||||
return (
|
||||
Number(formatUnits(props.position.totalSupplyEnd!, 18)) * multiplier -
|
||||
Number(formatUnits(props.position.totalSupplyEnd!, 18))
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
@use 'collapse'
|
||||
.f-collapse
|
||||
&.f-collapse-history
|
||||
.collapse-header, .collapsableDiv
|
||||
margin-right: 32px
|
||||
</style>
|
||||
27
web-app/src/components/collapse/collapse.sass
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
.f-collapse-inner
|
||||
gap: 12px
|
||||
.collapse-header
|
||||
display: grid
|
||||
width: 100%
|
||||
grid-template-columns: auto auto auto auto auto
|
||||
@media (min-width: 768px)
|
||||
grid-template-columns: 20% 30% 30% auto 20%
|
||||
.collapse-amount
|
||||
grid-column: 5
|
||||
text-align: right
|
||||
.collapsableDiv
|
||||
.collapsed-body
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
>div
|
||||
display: flex
|
||||
gap: 8px
|
||||
&.history
|
||||
flex-direction: row
|
||||
.collapsed-body--actions
|
||||
margin-top: 8px
|
||||
display: flex
|
||||
>div
|
||||
flex: 1 1 auto
|
||||
@media (min-width: 768px)
|
||||
flex: 1 1 25%
|
||||
112
web-app/src/components/fcomponents/FButton.vue
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<template>
|
||||
<button class="f-btn" :class="classObject" :style="styleObject">
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
size?: string;
|
||||
dense?: boolean;
|
||||
disabled?: boolean;
|
||||
invert?: boolean;
|
||||
block?: boolean;
|
||||
outlined?: boolean;
|
||||
bgColor?: string;
|
||||
light?: boolean;
|
||||
dark?: boolean;
|
||||
}
|
||||
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: "medium",
|
||||
});
|
||||
|
||||
const classObject = computed(() => ({
|
||||
"f-btn--tiny": props.size === "tiny",
|
||||
"f-btn--small": props.size === "small",
|
||||
"f-btn--medium": props.size === "medium",
|
||||
"f-btn--large": props.size === "large",
|
||||
"f-btn--dense": props.dense,
|
||||
"f-btn--disabled": props.disabled,
|
||||
"f-btn--invert": props.invert,
|
||||
"f-btn--block": props.block,
|
||||
"f-btn--outlined": props.outlined,
|
||||
"f-btn--light": props.light,
|
||||
"f-btn--dark": props.dark,
|
||||
}));
|
||||
|
||||
const styleObject = computed(() => {
|
||||
const returnObject: any = {};
|
||||
if (props.bgColor) {
|
||||
returnObject["background-color"] = props.bgColor;
|
||||
}
|
||||
return returnObject;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
@use 'sass:color'
|
||||
.f-btn
|
||||
padding: var(--btn-padding, 16px 32px)
|
||||
gap: 8px
|
||||
border-radius: var(--border-radius, 12px)
|
||||
border: none
|
||||
display: inline-flex
|
||||
text-decoration: none
|
||||
background-color: var(--color-button-bg)
|
||||
color: var(--color-button-font, white)
|
||||
letter-spacing: 0.15px
|
||||
transition: color .25s
|
||||
transition: background-color .25s
|
||||
justify-content: center
|
||||
font-weight: bold
|
||||
font-size: var(--font-size-button-medium)
|
||||
&.f-btn--tiny
|
||||
font-size: 13px
|
||||
letter-spacing: 0.46px
|
||||
padding: 4px 16px
|
||||
&.f-btn--small
|
||||
font-size: var(--font-size-button-small)
|
||||
letter-spacing: 0.46px
|
||||
&.f-btn--large
|
||||
font-size: var(--font-size-button-large)
|
||||
&.f-btn--disabled
|
||||
background-color: var(--color-bg-disabled, #D6D6D6)
|
||||
color: var(--color-disabled, #A1A3A7)
|
||||
&:hover,&:focus,&:active
|
||||
background-color: var(--color-bg-disabled, #D6D6D6)
|
||||
color: var(--color-disabled, #A1A3A7)
|
||||
cursor: not-allowed
|
||||
&.f-btn--dense
|
||||
padding: var(--btn-dense-padding, 8px 16px)
|
||||
&.f-btn--invert
|
||||
background-color: var(--btn-color, white)
|
||||
color: var(--color-button-bg)
|
||||
border: 2px solid var(--color-button-bg)
|
||||
&:hover,&:focus,&:active
|
||||
background-color: color.adjust(white, $lightness: -20%)
|
||||
&.f-btn--light
|
||||
background-color: var(--color-white, white)
|
||||
color: var(--color-secondary)
|
||||
border: 2px solid var(--color-white)
|
||||
&:hover,&:focus,&:active
|
||||
background-color: color.adjust(white, $lightness: -20%)
|
||||
border: 2px solid var(--color-white)
|
||||
&.f-btn--block
|
||||
display: flex
|
||||
flex: 1 0 auto
|
||||
min-width: 100%
|
||||
&.f-btn--outlined
|
||||
background-color: transparent
|
||||
color: var(--color-button-bg)
|
||||
border: 1px solid var(--color-button-border--outlined)
|
||||
|
||||
&:hover,&:focus,&:active
|
||||
cursor: pointer
|
||||
background-color: var(--color)
|
||||
&:hover,&:focus,&:active
|
||||
cursor: pointer
|
||||
background-color: #5f4884
|
||||
</style>
|
||||
57
web-app/src/components/fcomponents/FCard.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div class="f-card" ref="fCard" :style="computedStyles">
|
||||
<div v-if="props.title" class="f-card__title">
|
||||
<h5>
|
||||
{{ props.title }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="f-card__body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, computed } from "vue";
|
||||
|
||||
const fCard = ref();
|
||||
|
||||
interface Props {
|
||||
bgColor?: string;
|
||||
borderWidth?: number;
|
||||
boxShadow?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const computedStyles = computed(() => ({
|
||||
"border-width": props.borderWidth,
|
||||
"background-color": props.bgColor,
|
||||
"box-shadow": props.boxShadow,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
// if (props.bgcolor) {
|
||||
// fCard.value.style["background-color"] = props.bgcolor;
|
||||
// }
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.f-card
|
||||
border-radius: var(--card-border-radius)
|
||||
overflow: hidden
|
||||
height: 100%
|
||||
box-shadow: var(--color-card-box-shadow)
|
||||
background: var(--color-card-bg)
|
||||
color: var(--color-card-font)
|
||||
h6
|
||||
color: var(--color-card-font)
|
||||
.f-card__title
|
||||
border-bottom: 2px solid var(--color-based-blue)
|
||||
color: var(--color-based-blue)
|
||||
padding: 12px
|
||||
.f-card__body
|
||||
padding: 16px
|
||||
</style>
|
||||
97
web-app/src/components/fcomponents/FCollapse.vue
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<template>
|
||||
<div class="f-collapse">
|
||||
<div class="f-collapse-wrapper" :class="{loading: props.loading}">
|
||||
<template v-if="loading">
|
||||
<f-loader></f-loader>
|
||||
</template>
|
||||
<div class="f-collapse-inner">
|
||||
<slot name="header"></slot>
|
||||
<!-- <img class="toggle-collapse" src="../assets/expand-less.svg?url" alt="expand less" /> -->
|
||||
<Icon v-if="isShow" class="toggle-collapse" icon="mdi:chevron-down" @click="openClose"></Icon>
|
||||
<Icon v-else icon="mdi:chevron-up" class="toggle-collapse" @click="openClose"></Icon>
|
||||
<!-- <img
|
||||
class="toggle-collapse"
|
||||
src="../assets/expand-more.svg?url"
|
||||
alt="expand more"
|
||||
v-else
|
||||
|
||||
/> -->
|
||||
</div>
|
||||
<Transition name="collapse">
|
||||
<div v-if="isShow" class="collapsableDiv">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import FLoader from "@/components/fcomponents/FLoader.vue";
|
||||
|
||||
const emit = defineEmits(["collapse:opened", "collapse:closed"]);
|
||||
|
||||
const props = defineProps<{
|
||||
loading?: boolean;
|
||||
}>();
|
||||
|
||||
const isShow = ref(false);
|
||||
const openClose = () => {
|
||||
isShow.value = !isShow.value;
|
||||
if(isShow.value){
|
||||
emit("collapse:opened")
|
||||
} else{
|
||||
emit("collapse:closed")
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="sass">
|
||||
.f-collapse
|
||||
border-radius: var(--border-radius)
|
||||
border: 1px solid var(--color-collapse-border)
|
||||
background-color: var(--color-collapse-bg)
|
||||
.f-collapse-wrapper
|
||||
padding: 8px 16px
|
||||
&.loading
|
||||
opacity: 0.5
|
||||
position: relative
|
||||
pointer-events: none
|
||||
.cube
|
||||
position: absolute
|
||||
left: 50%
|
||||
top: 50%
|
||||
transform: translate(-50%, -50%)
|
||||
.f-collapse-inner
|
||||
display: flex
|
||||
position: relative
|
||||
align-items: center
|
||||
.toggle-collapse
|
||||
font-size: 25px
|
||||
align-self: flex-start
|
||||
g
|
||||
path
|
||||
fill: var(--color-expand-icon)
|
||||
&:hover, &:active, &:focus
|
||||
cursor: pointer
|
||||
.collapsableDiv
|
||||
color: var(--color-collapse-font)
|
||||
|
||||
.collapse-enter-active
|
||||
animation: collapse reverse 500ms ease
|
||||
|
||||
.collapse-leave-active
|
||||
animation: collapse 500ms ease
|
||||
|
||||
@keyframes collapse
|
||||
from
|
||||
max-height: 200px
|
||||
overflow: hidden
|
||||
|
||||
to
|
||||
max-height: 0px
|
||||
overflow: hidden
|
||||
|
||||
|
||||
</style>
|
||||
135
web-app/src/components/fcomponents/FInput.vue
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<template>
|
||||
<div class="f-input" :class="classObject">
|
||||
<div class="f-input-label subheader2">
|
||||
<label v-if="props.label" :for="name">{{ props.label }}</label>
|
||||
<icon>
|
||||
<template v-slot:text v-if="slots.info">
|
||||
<slot name="info"></slot>
|
||||
</template>
|
||||
</icon>
|
||||
</div>
|
||||
<div class="f-input__wrapper" ref="inputWrapper" @click="setFocus">
|
||||
<input
|
||||
:disabled="props.disabled"
|
||||
:readonly="props.readonly"
|
||||
:type="props.type"
|
||||
:name="name"
|
||||
:id="name"
|
||||
@input="updateModelValue"
|
||||
:value="props.modelValue"
|
||||
/>
|
||||
<div class="f-input--suffix" v-if="slots.suffix">
|
||||
<slot name="suffix" >test </slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="f-input--details" v-if="slots.details">
|
||||
<slot name="details"> </slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, getCurrentInstance, useSlots, ref } from "vue";
|
||||
import useClickOutside from "@/composables/useClickOutside"
|
||||
|
||||
import Icon from "@/components/icons/IconInfo.vue";
|
||||
interface Props {
|
||||
size?: string;
|
||||
info?: string;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
modelValue?: any;
|
||||
type?: string;
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const slots = useSlots();
|
||||
|
||||
const inputWrapper = ref();
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const name = `f-input-${instance!.uid}`;
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: "normal",
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
type: "string",
|
||||
});
|
||||
|
||||
useClickOutside(inputWrapper, () => {
|
||||
removeFocus();
|
||||
})
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
function updateModelValue($event: any) {
|
||||
emit("update:modelValue", $event.target.value);
|
||||
}
|
||||
|
||||
const classObject = computed(() => ({
|
||||
"f-input--normal": props.size === "normal",
|
||||
"f-input--big": props.size === "big",
|
||||
"f-input--disabled": props.disabled,
|
||||
}));
|
||||
|
||||
function removeFocus(){
|
||||
const target = inputWrapper.value as HTMLElement
|
||||
target.classList.remove("f-input__wrapper--focused")
|
||||
}
|
||||
|
||||
function setFocus(event:MouseEvent){
|
||||
console.log("setFocus");
|
||||
if(props.disabled){
|
||||
return
|
||||
}
|
||||
const target = inputWrapper.value as HTMLElement
|
||||
target.classList.add("f-input__wrapper--focused")
|
||||
}
|
||||
</script>
|
||||
<style lang="sass">
|
||||
.f-input
|
||||
display: flex
|
||||
flex-direction: column
|
||||
.info-icon
|
||||
position: static
|
||||
@media (min-width: 768px)
|
||||
position: relative
|
||||
.f-input-label
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 8px
|
||||
.f-input__wrapper
|
||||
display: flex
|
||||
border: 1px solid black
|
||||
border-radius: 12px
|
||||
width: 100%
|
||||
background-color: #2D2D2D
|
||||
&.f-input__wrapper--focused
|
||||
outline: none
|
||||
border: 1px solid #7550AE
|
||||
box-shadow: 0 0 5px #7550AE
|
||||
input
|
||||
border: 0
|
||||
border-radius: 12px
|
||||
padding: 8px 12px
|
||||
width: 100%
|
||||
background-color: #2D2D2D
|
||||
color: #FFFFFF
|
||||
outline: none
|
||||
&.f-input--big
|
||||
font-size: 18px
|
||||
&.f-input--normal
|
||||
font-size: 16px
|
||||
&.f-input--disabled
|
||||
input
|
||||
background-color: #202020
|
||||
color: #F0F0F0
|
||||
.f-input--suffix
|
||||
display: flex
|
||||
align-items: center
|
||||
margin-right: 10px
|
||||
|
||||
</style>
|
||||
77
web-app/src/components/fcomponents/FLoader.vue
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<div class="cube">
|
||||
<div class="side"></div>
|
||||
<div class="side"></div>
|
||||
<div class="side"></div>
|
||||
<div class="side"></div>
|
||||
<div class="side"></div>
|
||||
<div class="side"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
.cube
|
||||
margin: auto
|
||||
font-size: 24px
|
||||
height: 1em
|
||||
width: 1em
|
||||
position: relative
|
||||
transform: rotatex(30deg) rotatey(45deg)
|
||||
transform-style: preserve-3d
|
||||
animation: cube-spin 1.5s infinite ease-in-out alternate
|
||||
|
||||
.side
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
transform-style: preserve-3d
|
||||
|
||||
&::before
|
||||
content: ""
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
background-color: currentcolor
|
||||
transform: translatez(0.5em)
|
||||
animation: cube-explode 1.5s infinite ease-in-out
|
||||
opacity: 0.5
|
||||
|
||||
&:nth-child(1)
|
||||
transform: rotatey(90deg)
|
||||
|
||||
&:nth-child(2)
|
||||
transform: rotatey(180deg)
|
||||
|
||||
&:nth-child(3)
|
||||
transform: rotatey(270deg)
|
||||
|
||||
&:nth-child(4)
|
||||
transform: rotatey(360deg)
|
||||
|
||||
&:nth-child(5)
|
||||
transform: rotatex(90deg)
|
||||
|
||||
&:nth-child(6)
|
||||
transform: rotatex(270deg)
|
||||
|
||||
@keyframes cube-spin
|
||||
0%
|
||||
transform: rotatex(30deg) rotatey(45deg)
|
||||
|
||||
100%
|
||||
transform: rotatex(30deg) rotatey(405deg)
|
||||
|
||||
@keyframes cube-explode
|
||||
0%
|
||||
transform: translatez(0.5em)
|
||||
|
||||
50%
|
||||
transform: translatez(0.75em)
|
||||
|
||||
100%
|
||||
transform: translatez(0.5em)
|
||||
</style>
|
||||
73
web-app/src/components/fcomponents/FOutput.vue
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<div
|
||||
class="output-wrapper"
|
||||
:class="{
|
||||
'no-name': !props.name,
|
||||
'output--variant-1': props.variant === 1,
|
||||
'output--variant-2': props.variant === 2,
|
||||
'output--variant-3': props.variant === 3,
|
||||
}"
|
||||
>
|
||||
<div class="output-left">
|
||||
<div class="output-text">{{ props.name }}</div>
|
||||
<div class="output-price">
|
||||
<slot name="price">
|
||||
{{ props.price }}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="output-right" v-if="slots.end">
|
||||
<slot name="end"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from "vue";
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
price: [String, Number],
|
||||
variant: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.output-wrapper
|
||||
height: 100%
|
||||
display: flex
|
||||
flex-direction: row
|
||||
&.no-name
|
||||
gap: 0
|
||||
align-items: center
|
||||
justify-content: center
|
||||
&.output--variant-1
|
||||
padding: 16px
|
||||
border: 1px solid #2D2D2D
|
||||
border-radius: var(--border-radius, 12px)
|
||||
background-color: var(--color-input)
|
||||
&.output--variant-2
|
||||
border-bottom: 1px solid var(--color-grey)
|
||||
padding: 8px
|
||||
&.output--variant-3
|
||||
padding: 8px
|
||||
.output-left
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 8px
|
||||
text-align: left
|
||||
.output-price
|
||||
font-size: 18px
|
||||
letter-spacing: 3px
|
||||
.output-text
|
||||
font-size: 14px
|
||||
font-weight: bold
|
||||
.output-right
|
||||
margin-left: auto
|
||||
align-self: center
|
||||
</style>
|
||||
234
web-app/src/components/fcomponents/FSelect.vue
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
<template>
|
||||
<div class="o-select" @click="clickSelect" ref="componentRef">
|
||||
<f-input hide-details :clearable="props.clearable" :label="props.label" :selectedable="false"
|
||||
:focus="showList" :modelValue="`${year} % yearly`" readonly>
|
||||
<template #info v-if="slots.info">
|
||||
<slot name="info"></slot>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<Icon class="toggle-collapse" v-if="showList" icon="mdi:chevron-down"></Icon>
|
||||
<Icon class="toggle-collapse" v-else icon="mdi:chevron-up"></Icon>
|
||||
</template>
|
||||
</f-input>
|
||||
<div class="select-list-wrapper" v-show="showList" ref="selectList" >
|
||||
<div class="select-list-inner" @click.stop>
|
||||
<div class="select-list-item" v-for="(item, index) in props.items" :key="item.year" :class="{'active': year === item.year, 'hovered': activeIndex === index}"
|
||||
@click.stop="clickItem(item)" @mouseenter="mouseEnter($event, index)" @mouseleave="mouseLeave($event, index)">
|
||||
<div class="circle">
|
||||
<div class="active" v-if="year === item.year"></div>
|
||||
<div class="hovered" v-else-if="activeIndex === index"></div>
|
||||
</div>
|
||||
<div class="yearly">
|
||||
<div class="value">{{ item.year }} %</div>
|
||||
<div class="label">yearly</div>
|
||||
</div>
|
||||
<div class="daily">
|
||||
<div class="value">{{ item.daily.toFixed(4) }} %</div>
|
||||
<div class="label">daily</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot :style="[!props.editor ? {display: 'none'} : {}]"></slot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, getCurrentInstance, type ComputedRef, useSlots } from "vue"
|
||||
import FInput from "@/components/fcomponents/FInput.vue"
|
||||
import useClickOutside from "@/composables/useClickOutside"
|
||||
import {Icon} from "@iconify/vue";
|
||||
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const slots = useSlots();
|
||||
|
||||
interface Item {
|
||||
year: number;
|
||||
daily: number;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: [Array<Item>],
|
||||
required: false,
|
||||
default: []
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
itemTitle: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "label"
|
||||
},
|
||||
itemValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "value"
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
modelValue: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
editor: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const showList = ref(<boolean>false);
|
||||
const slotElements = ref(<any[]>[])
|
||||
const componentRef = ref(<any>null);
|
||||
const selectList = ref();
|
||||
const activeIndex= ref();
|
||||
|
||||
useClickOutside(componentRef, () => {
|
||||
|
||||
showList.value = false
|
||||
})
|
||||
|
||||
|
||||
const year= computed({
|
||||
// getter
|
||||
get() {
|
||||
return props.modelValue || props.items[0].year
|
||||
},
|
||||
// setter
|
||||
set(newValue: number) {
|
||||
|
||||
emit("update:modelValue", newValue)
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
function mouseEnter(event:MouseEvent , index:number){
|
||||
const target = event. target as HTMLElement;
|
||||
activeIndex.value = index;
|
||||
target.classList.add("active")
|
||||
}
|
||||
|
||||
function mouseLeave(event:MouseEvent, index:number){
|
||||
const elements = selectList.value.querySelectorAll('.select-list-item');
|
||||
elements.forEach((element:HTMLElement) => {
|
||||
element.classList.remove("active")
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function clickSelect(event: any) {
|
||||
showList.value = !showList.value;
|
||||
}
|
||||
|
||||
|
||||
function clickItem(item: any) {
|
||||
console.log("item", item);
|
||||
year.value = item.year
|
||||
showList.value = false;
|
||||
console.log("showList.value", showList.value);
|
||||
// emit('input', item)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="sass">
|
||||
.o-select
|
||||
position: relative
|
||||
display: inline-block
|
||||
.toggle-collapse
|
||||
color: black
|
||||
font-size: 20px
|
||||
&:active, &:focus, &:hover
|
||||
cursor: pointer
|
||||
.select-list-wrapper
|
||||
position: absolute
|
||||
box-shadow: 0 1px 6px 0 #20212447
|
||||
background-color: var(--color-card-bg)
|
||||
z-index: 10
|
||||
padding: 16px
|
||||
border-radius: 16px
|
||||
border: 1px solid black
|
||||
right: 0
|
||||
margin-top: 4px
|
||||
.select-list-inner
|
||||
height: 330px
|
||||
overflow-y: scroll
|
||||
display: grid
|
||||
.select-list-item
|
||||
display: grid
|
||||
grid-template-columns: 15px 110px 126px
|
||||
place-content: end
|
||||
margin-right: 8px
|
||||
-webkit-touch-callout: none
|
||||
-webkit-user-select: none
|
||||
-khtml-user-select: none
|
||||
-moz-user-select: none
|
||||
-ms-user-select: none
|
||||
user-select: none
|
||||
padding: 4px 0
|
||||
&:active, &:focus, &:hover
|
||||
cursor: pointer
|
||||
.daily, .yearly
|
||||
display: flex
|
||||
gap: 4px
|
||||
justify-content: end
|
||||
.daily
|
||||
color: var(--color-grey)
|
||||
.circle
|
||||
height: 14px
|
||||
width: 14px
|
||||
.active
|
||||
height: 14px
|
||||
width: 14px
|
||||
background-color: var(--color-based-blue)
|
||||
border-radius: 14px
|
||||
.hovered
|
||||
height: 14px
|
||||
width: 14px
|
||||
background-color: var(--color-based-blue)
|
||||
opacity: .5
|
||||
border-radius: 14px
|
||||
|
||||
|
||||
|
||||
.select-list-inner
|
||||
&::-webkit-scrollbar-track
|
||||
border-radius: 10px
|
||||
|
||||
&::-webkit-scrollbar
|
||||
width: 5px
|
||||
|
||||
&::-webkit-scrollbar-thumb
|
||||
border-radius: 10px
|
||||
background-color: lightgrey
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
259
web-app/src/components/fcomponents/FSlider.vue
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="slider-wrapper">
|
||||
<div class="disabled-slider" :style="{ 'flex-basis': minPercentage + '%' }">
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
<div class="range-slider">
|
||||
<input
|
||||
class="range-slider__range"
|
||||
type="range"
|
||||
ref="sliderInput"
|
||||
:style="{
|
||||
background: `linear-gradient(90deg, ${settings.fill} ${percentageDot}%, ${settings.background} ${percentageDot + 0.1}%)`,
|
||||
}"
|
||||
:value="props.modelValue"
|
||||
:min="props.min"
|
||||
:max="props.max"
|
||||
@input="updateModelValue"
|
||||
/>
|
||||
<div
|
||||
class="testbla"
|
||||
@mousemove="testMove"
|
||||
:style="{
|
||||
left: percentageDot + '%',
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref, computed } from "vue";
|
||||
|
||||
const sliderInput = ref();
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: number;
|
||||
min: number;
|
||||
max: number;
|
||||
}>();
|
||||
|
||||
const minPercentage = computed(() => {
|
||||
if (!props.min || !props.max) {
|
||||
return 0n;
|
||||
}
|
||||
return (props.min * 100) / props.max;
|
||||
});
|
||||
|
||||
function updateModelValue(event: any) {
|
||||
emit("update:modelValue", event.target.valueAsNumber);
|
||||
}
|
||||
|
||||
|
||||
const sliderValue = computed({
|
||||
// getter
|
||||
get() {
|
||||
return props.modelValue || props.min;
|
||||
},
|
||||
// setter
|
||||
set(newValue) {
|
||||
emit("update:modelValue", newValue);
|
||||
},
|
||||
});
|
||||
|
||||
const percentageDot = computed(() => {
|
||||
let percentage = (100 * (sliderValue.value - props.min)) / (props.max - props.min);
|
||||
|
||||
if(percentage < 20){
|
||||
percentage = percentage + 1;
|
||||
} else if(percentage > 50){
|
||||
percentage = percentage - 2;
|
||||
} else if(percentage > 80){
|
||||
percentage = percentage - 3;
|
||||
}
|
||||
|
||||
return percentage
|
||||
});
|
||||
|
||||
const settings = {
|
||||
fill: "#7550AE",
|
||||
background: "#000000",
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
sliderInput.value.addEventListener("input", setSliderValue);
|
||||
});
|
||||
|
||||
function setSliderValue(event: any){
|
||||
sliderValue.value = event.target.value;
|
||||
}
|
||||
|
||||
function testMove(event: any) {
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
console.log("sliderInput.value", sliderInput.value);
|
||||
if(sliderInput.value){
|
||||
sliderInput.value.removeEventListener("input", setSliderValue);
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.slider-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
.testbla {
|
||||
position: absolute;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
background-color: var(--color-based-blue);
|
||||
user-select: none;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
right: 50%;
|
||||
border-radius: 25px;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
.disabled-slider {
|
||||
width: 60px;
|
||||
height: 7px;
|
||||
background-color: var(--color-border-main);
|
||||
/* margin-top: auto; */
|
||||
align-self: center;
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
}
|
||||
.dot {
|
||||
border-radius: 50px;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
background-color: var(--color-border-main);
|
||||
position: absolute;
|
||||
right: -14px;
|
||||
/* bottom: 50%; */
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
// Base Colors
|
||||
$shade-10: #4682b4 !default;
|
||||
$shade-1: #d7dcdf !default;
|
||||
$shade-0: #fff !default;
|
||||
$teal: #4682b4 !default;
|
||||
|
||||
// Range Slider
|
||||
$range-width: 100% !default;
|
||||
|
||||
$range-handle-color: $shade-10 !default;
|
||||
$range-handle-color-hover: $teal !default;
|
||||
$range-handle-size: 15px !default;
|
||||
|
||||
$range-track-color: $shade-1 !default;
|
||||
$range-track-height: 7px !default;
|
||||
|
||||
$range-label-color: $shade-10 !default;
|
||||
$range-label-width: 60px !default;
|
||||
|
||||
.range-slider {
|
||||
width: $range-width;
|
||||
position: relative;
|
||||
accent-color: #7550AE;
|
||||
}
|
||||
|
||||
.range-slider__range {
|
||||
// width: calc(100% - (#{$range-label-width + 13px}));
|
||||
width: 100%;
|
||||
height: $range-track-height;
|
||||
border-radius: 5px;
|
||||
background: $range-track-color;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
// Range Handle
|
||||
&::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
opacity: 0;
|
||||
padding: 5px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:active::-webkit-slider-thumb {
|
||||
background: $range-handle-color-hover;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
width: $range-handle-size;
|
||||
height: $range-handle-size;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: $range-handle-color;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background: $range-handle-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&:active::-moz-range-thumb {
|
||||
background: $range-handle-color-hover;
|
||||
}
|
||||
|
||||
// Focus state
|
||||
&:focus {
|
||||
&::-webkit-slider-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Range Label
|
||||
.range-slider__value {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: $range-label-width;
|
||||
color: $shade-0;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
background: $range-label-color;
|
||||
padding: 5px 10px;
|
||||
margin-left: 8px;
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: -7px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 7px solid transparent;
|
||||
border-right: 7px solid $range-label-color;
|
||||
border-bottom: 7px solid transparent;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
// Firefox Overrides
|
||||
::-moz-range-track {
|
||||
background: $range-track-color;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
input::-moz-focus-inner,
|
||||
input::-moz-focus-outer {
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
14
web-app/src/components/fcomponents/FTag.vue
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<div class="f-tag">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
.f-tag
|
||||
border: 1px solid var(--color-font)
|
||||
border-radius: var(--border-radius, 12px)
|
||||
padding: 2px 16px
|
||||
font-size: 12px
|
||||
display: inline
|
||||
</style>
|
||||
64
web-app/src/components/fcomponents/tabs/FTab.vue
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<section v-show="value === props.name" ref="tab" role="tabpanel" tabindex="-1">
|
||||
<slot></slot>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onBeforeMount, getCurrentInstance, ref, computed, watch, inject } from "vue";
|
||||
const instance = ref<any>();
|
||||
|
||||
const isActive = ref<any>(null);
|
||||
|
||||
const value = inject("value");
|
||||
var updateTab: any = inject("updateTab");
|
||||
|
||||
defineExpose({
|
||||
isActive,
|
||||
});
|
||||
|
||||
onBeforeMount(() => {});
|
||||
onMounted(() => {
|
||||
instance.value = getCurrentInstance();
|
||||
|
||||
var addTab: any = inject("addTab");
|
||||
addTab({
|
||||
uid: instance.value!.uid,
|
||||
name: props.name,
|
||||
label: props.label,
|
||||
});
|
||||
|
||||
// resolved = {"test123": 123, "blub": "abc"}
|
||||
});
|
||||
|
||||
// const test = computed(() => {
|
||||
// console.log(instance.value);
|
||||
|
||||
// return instance.value?.parent
|
||||
// })
|
||||
|
||||
//Props
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: null,
|
||||
},
|
||||
name: {
|
||||
type: [String],
|
||||
required: true,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
watch(props, (newValue, oldValue) => {
|
||||
console.log("newValue", newValue);
|
||||
console.log("instance", instance.value);
|
||||
|
||||
updateTab({
|
||||
uid: instance.value!.uid,
|
||||
name: props.name,
|
||||
label: props.label,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
126
web-app/src/components/fcomponents/tabs/FTabs.vue
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<template>
|
||||
<div class="f-tabs" ref="tabsRef">
|
||||
<div class="f-tabs__header" ref="headerRef">
|
||||
<div
|
||||
class="f-tab"
|
||||
ref="tabsRef1"
|
||||
:id="`f-tab-${tab.uid}`"
|
||||
v-for="tab in tabs"
|
||||
:key="tab.uid"
|
||||
@click="setActive(tab)"
|
||||
>
|
||||
<h5>{{ tab.label }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="f-tabs__content">
|
||||
<slot ref="tabsTest"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick, h, computed, useSlots, provide, getCurrentInstance } from "vue";
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const slots1 = useSlots();
|
||||
|
||||
const tabsRef = ref<HTMLElement>();
|
||||
const tabsRef1 = ref<HTMLElement>();
|
||||
const headerRef = ref<HTMLElement>();
|
||||
const tabsArray = ref<Array<HTMLElement>>();
|
||||
const tabsComponents = ref<Array<any>>();
|
||||
const content = ref<any>();
|
||||
const target = ref<any>();
|
||||
const slots = ref<any>();
|
||||
const value = ref<any>();
|
||||
const tabsTest = ref<any>();
|
||||
const tabs = ref<Array<any>>([]);
|
||||
provide("value", value);
|
||||
provide("addTab", (tab: any) => {
|
||||
tabs.value.push(tab);
|
||||
});
|
||||
|
||||
provide("updateTab", (tab: any) => {
|
||||
let changedTabIndex = tabs.value.findIndex((obj) => obj.uid === tab.uid);
|
||||
if (changedTabIndex > -1) {
|
||||
tabs.value[changedTabIndex] = tab;
|
||||
}
|
||||
});
|
||||
// provide('addTab', tabs.value)
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if(props.modelValue){
|
||||
const tab = tabs.value.find((obj) => obj.name === props.modelValue)
|
||||
setActive(tab);
|
||||
|
||||
} else {
|
||||
setActive(tabs.value[0]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function setActive(tab: any) {
|
||||
|
||||
nextTick(() => {
|
||||
const tabElement: any = headerRef.value?.querySelector(`#f-tab-${tab.uid}`);
|
||||
var array = Array.prototype.slice.call(tabsRef.value?.children[0].children);
|
||||
|
||||
array.forEach((element: HTMLElement) => {
|
||||
element.classList.remove("f-tab--active");
|
||||
});
|
||||
tabElement.classList.add("f-tab--active");
|
||||
value.value = tab.name;
|
||||
emit("update:modelValue", tab.name);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.f-tabs
|
||||
display: flex
|
||||
flex-direction: column
|
||||
width: 100%
|
||||
.f-tabs__header
|
||||
display: flex
|
||||
.f-tab, label
|
||||
cursor: pointer
|
||||
.f-tab
|
||||
padding: 0.6em 1em
|
||||
transition: background-color .2s,color .2s
|
||||
align-items: center
|
||||
justify-content: center
|
||||
font-weight: 400
|
||||
text-transform: uppercase
|
||||
font-size: 24px
|
||||
font-weight: bold
|
||||
border-width: 0 0 2px 0
|
||||
border-style: solid
|
||||
border-color: transparent
|
||||
flex: 1 1 auto
|
||||
text-align: center
|
||||
height: 50px
|
||||
border-bottom: 2px solid var(--color-light-grey)
|
||||
h5
|
||||
color: var(--color-tab-font)
|
||||
margin: 0
|
||||
&.f-tab--active
|
||||
border-bottom: 3px solid var(--color-tab-border--active)
|
||||
h5
|
||||
color: var(--color-tab-border--active)
|
||||
.f-tabs__content
|
||||
padding: 16px 12px
|
||||
@media (min-width: 768px)
|
||||
padding: 16px 24px
|
||||
</style>
|
||||
72
web-app/src/components/fcomponents/tabs/Tabs.spec.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import FTabs from './FTabs.vue';
|
||||
import FTab from './FTab.vue';
|
||||
|
||||
describe('FTabs.vue', () => {
|
||||
it('renders tabs correctly', () => {
|
||||
const wrapper = mount(FTabs, {
|
||||
slots: {
|
||||
default: `
|
||||
<f-tab label="Tab 1" name="tab1">Tab 1</f-tab>
|
||||
<f-tab label="Tab 2" name="tab2">Tab 2</f-tab>
|
||||
`,
|
||||
},
|
||||
global: {
|
||||
components: { FTab },
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Tab 1');
|
||||
expect(wrapper.text()).toContain('Tab 2');
|
||||
});
|
||||
|
||||
it('activates the first tab by default via v-model', async () => {
|
||||
const wrapper = mount(FTabs, {
|
||||
slots: {
|
||||
default: `
|
||||
<f-tab label="Tab 1" name="tab1">Tab 1</f-tab>
|
||||
<f-tab label="Tab 2" name="tab2">Tab 2</f-tab>
|
||||
`,
|
||||
},
|
||||
props: {
|
||||
modelValue: 'tab1', // v-model initial value
|
||||
},
|
||||
global: {
|
||||
components: { FTab },
|
||||
},
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const sections = wrapper.findAll('section');
|
||||
|
||||
expect(sections[0].attributes('style')).not.toContain('display: none');
|
||||
expect(sections[1].attributes('style')).toContain('display: none');
|
||||
|
||||
});
|
||||
|
||||
it('switches to the correct tab on click', async () => {
|
||||
const wrapper = mount(FTabs, {
|
||||
slots: {
|
||||
default: `
|
||||
<f-tab label="Tab 1" name="tab1">Tab 1</f-tab>
|
||||
<f-tab label="Tab 2" name="tab2">Tab 2</f-tab>
|
||||
`,
|
||||
},
|
||||
global: {
|
||||
components: { FTab },
|
||||
},
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
const tabs = wrapper.findAll('.f-tab');
|
||||
await tabs[1].trigger('click');
|
||||
|
||||
const sections = wrapper.findAll('section');
|
||||
expect(sections[0].attributes('style')).toContain('display: none');
|
||||
expect(sections[1].attributes('style')).not.toContain('display: none');
|
||||
});
|
||||
});
|
||||
23
web-app/src/components/icons/IconBase.vue
Normal file
29
web-app/src/components/icons/IconDiscord.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<svg width="23" height="18" viewBox="0 0 23 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2590_494)">
|
||||
<path
|
||||
ref="svgPath"
|
||||
d="M19.419 3.40166C16.7691 1.45664 14.2483 1.51053 14.2483 1.51053L13.9906 1.79865C17.1188 2.73512 18.5723 4.08593 18.5723 4.08593C16.6585 3.05941 14.7817 2.55501 13.0336 2.35694C11.7088 2.21276 10.4391 2.24892 9.3167 2.39287C9.20634 2.39287 9.11433 2.41083 9.00397 2.42879C8.35994 2.48292 6.79584 2.71692 4.82702 3.56357C4.14628 3.86966 3.74131 4.08593 3.74131 4.08593C3.74131 4.08593 5.2687 2.66303 8.58065 1.72656L8.39664 1.51053C8.39664 1.51053 5.87579 1.4564 3.22598 3.40166C3.22598 3.40166 0.576172 8.10242 0.576172 13.9018C0.576172 13.9018 2.12191 16.5134 6.18851 16.6393C6.18851 16.6393 6.86925 15.8289 7.42129 15.1446C5.08444 14.4601 4.20109 13.0195 4.20109 13.0195C4.20109 13.0195 4.3851 13.1454 4.71642 13.3256C4.73477 13.3435 4.75313 13.3615 4.79007 13.3797C4.84537 13.4156 4.90043 13.4338 4.95573 13.4697C5.41576 13.7219 5.87579 13.92 6.29911 14.0821C7.05351 14.3703 7.95521 14.6584 9.00397 14.8565C10.3841 15.1087 12.0032 15.1987 13.7697 14.8744C14.6344 14.7303 15.5178 14.4783 16.4378 14.0999C17.0819 13.8656 17.7996 13.5236 18.554 13.0372C18.554 13.0372 17.6339 14.514 15.2234 15.1805C15.7754 15.865 16.4378 16.6393 16.4378 16.6393C20.5047 16.5131 22.0688 13.9018 22.0688 13.9018C22.0688 8.10242 19.419 3.40166 19.419 3.40166ZM7.8818 12.2267C6.85138 12.2267 6.00498 11.3262 6.00498 10.2276C6.00498 9.12894 6.83303 8.22841 7.8818 8.22841C8.93056 8.22841 9.77697 9.12894 9.75861 10.2276C9.75861 11.3262 8.93056 12.2267 7.8818 12.2267ZM14.598 12.2267C13.5675 12.2267 12.7211 11.3262 12.7211 10.2276C12.7211 9.12894 13.5492 8.22841 14.598 8.22841C15.6467 8.22841 16.4748 9.12894 16.4748 10.2276C16.4748 11.3262 15.647 12.2267 14.598 12.2267Z"
|
||||
:fill="props.color"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2590_494">
|
||||
<rect width="22" height="17" fill="white" transform="translate(0.333984 0.58667)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
const svgPath = ref();
|
||||
|
||||
interface Props {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
</script>
|
||||
8
web-app/src/components/icons/IconDocs.vue
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
||||
<!-- Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc. -->
|
||||
<path
|
||||
d="M96 0C43 0 0 43 0 96L0 416c0 53 43 96 96 96l288 0 32 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l0-64c17.7 0 32-14.3 32-32l0-320c0-17.7-14.3-32-32-32L384 0 96 0zm0 384l256 0 0 64L96 448c-17.7 0-32-14.3-32-32s14.3-32 32-32zm32-240c0-8.8 7.2-16 16-16l192 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-192 0c-8.8 0-16-7.2-16-16zm16 48l192 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-192 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
7
web-app/src/components/icons/IconHome.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
|
||||
<path
|
||||
d="M575.8 255.5c0 18-15 32.1-32 32.1h-32l.7 160.2c0 2.7-.2 5.4-.5 8.1V472c0 22.1-17.9 40-40 40H456c-1.1 0-2.2 0-3.3-.1c-1.4 .1-2.8 .1-4.2 .1H416 392c-22.1 0-40-17.9-40-40V448 384c0-17.7-14.3-32-32-32H256c-17.7 0-32 14.3-32 32v64 24c0 22.1-17.9 40-40 40H160 128.1c-1.5 0-3-.1-4.5-.2c-1.2 .1-2.4 .2-3.6 .2H104c-22.1 0-40-17.9-40-40V360c0-.9 0-1.9 .1-2.8V287.6H32c-18 0-32-14-32-32.1c0-9 3-17 10-24L266.4 8c7-7 15-8 22-8s15 2 21 7L564.8 231.5c8 7 12 15 11 24z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
62
web-app/src/components/icons/IconInfo.vue
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<template>
|
||||
<tippy v-if="slots.text" theme="my-theme" trigger="click">
|
||||
<div class="info-icon">
|
||||
<svg :width="props.size" :height="props.size" viewBox="0 0 14 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M6.2 6.26416C6.2 5.84995 6.53579 5.51416 6.95 5.51416C7.36421 5.51416 7.7 5.84995 7.7 6.26416V9.76416C7.7 10.1784 7.36421 10.5142 6.95 10.5142C6.53579 10.5142 6.2 10.1784 6.2 9.76416V6.26416Z"
|
||||
/>
|
||||
<path
|
||||
d="M7 3.01416C7.55228 3.01416 8 3.46188 8 4.01416C8 4.56644 7.55228 5.01416 7 5.01416C6.44772 5.01416 6 4.56644 6 4.01416C6 3.46188 6.44772 3.01416 7 3.01416Z"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14 7.01416C14 10.8802 10.866 14.0142 7 14.0142C3.13401 14.0142 0 10.8802 0 7.01416C0 3.14817 3.13401 0.0141602 7 0.0141602C10.866 0.0141602 14 3.14817 14 7.01416ZM1.5 7.01416C1.5 10.0517 3.96243 12.5142 7 12.5142C10.0376 12.5142 12.5 10.0517 12.5 7.01416C12.5 3.97659 10.0376 1.51416 7 1.51416C3.96243 1.51416 1.5 3.97659 1.5 7.01416Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<template #content>
|
||||
<slot name="text"></slot>
|
||||
|
||||
</template>
|
||||
</tippy>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSlots, ref, onMounted } from "vue";
|
||||
import { Tippy } from "vue-tippy";
|
||||
import "tippy.js/dist/tippy.css"; // optional for styling
|
||||
const slots = useSlots();
|
||||
|
||||
interface Props {
|
||||
size?: string;
|
||||
}
|
||||
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: "15px",
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.info-icon
|
||||
vertical-align: middle
|
||||
display: inline-flex
|
||||
position: relative
|
||||
svg
|
||||
path
|
||||
fill: var(--color-font)
|
||||
.tippy-arrow
|
||||
color: rgba(15, 15, 15, 0.9) !important
|
||||
.tippy-box[data-theme~='my-theme']
|
||||
background-color: rgba(15, 15, 15, 0.9)
|
||||
height: fit-content
|
||||
color: white
|
||||
font-size: 12px
|
||||
max-width: 200px !important
|
||||
border-radius: var(--border-radius)
|
||||
// @media (min-width: 768px)
|
||||
// max-width: 400px
|
||||
|
||||
</style>
|
||||
14
web-app/src/components/icons/IconLogin.vue
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="login-icon" viewBox="0 0 512 512">
|
||||
<path
|
||||
d="M352 96l64 0c17.7 0 32 14.3 32 32l0 256c0 17.7-14.3 32-32 32l-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0c53 0 96-43 96-96l0-256c0-53-43-96-96-96l-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32zm-9.4 182.6c12.5-12.5 12.5-32.8 0-45.3l-128-128c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L242.7 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l210.7 0-73.4 73.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l128-128z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
.login-icon
|
||||
width: 35px
|
||||
path
|
||||
fill: var(--color-font)
|
||||
</style>
|
||||
33
web-app/src/components/icons/IconTelegram.vue
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2590_499)">
|
||||
<path
|
||||
ref="svgPath"
|
||||
d="M17.058 0.932576L1.74462 6.83727C0.699293 7.25659 0.705631 7.83977 1.55407 8.09947L5.48317 9.32593L6.83449 13.7733C7.01217 14.2637 6.92458 14.4583 7.43975 14.4583C7.83718 14.4583 8.01274 14.2765 8.2346 14.0608L10.1441 12.2041L14.1166 15.1392C14.8477 15.5426 15.3754 15.3336 15.5575 14.4606L18.1654 2.17133C18.4324 1.10065 17.7574 0.615059 17.058 0.932576Z"
|
||||
:fill="props.color"
|
||||
/>
|
||||
<path
|
||||
d="M7.45616 13.2757L6.16016 9.01067L16.1361 3.09253L8.76406 10.3509L7.45616 13.2757Z"
|
||||
:fill="(props.color === 'black' ? 'white' : 'black')"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2590_499">
|
||||
<rect width="18.125" height="14.5" fill="white" transform="translate(0.9375 0.83667)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
const svgPath = ref();
|
||||
|
||||
interface Props {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
</script>
|
||||
30
web-app/src/components/icons/IconTwitter.vue
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="20px"
|
||||
height="18px"
|
||||
viewBox="0 0 19 18"
|
||||
version="1.1"
|
||||
>
|
||||
<g id="surface1">
|
||||
<path
|
||||
ref="svgPath"
|
||||
d="M 14.945312 0 L 17.859375 0 L 11.464844 7.636719 L 18.9375 18 L 13.070312 18 L 8.480469 11.703125 L 3.222656 18 L 0.308594 18 L 7.085938 9.832031 L -0.0703125 0 L 5.941406 0 L 10.089844 5.753906 Z M 13.925781 16.207031 L 15.542969 16.207031 L 5.09375 1.726562 L 3.355469 1.726562 Z M 13.925781 16.207031 "
|
||||
:fill="props.color"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
const svgPath = ref();
|
||||
|
||||
interface Props {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
</script>
|
||||
58
web-app/src/components/layouts/ConnectButton.vue
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<template v-if="status === 'disconnected'">
|
||||
<f-button class="connect-button--disconnected">Connect</f-button>
|
||||
</template>
|
||||
<template v-else-if="status === 'connected'">
|
||||
<f-button class="connect-button connect-button--connected">
|
||||
<img :src="getBlocky(address!)" alt="avatar" />
|
||||
<div>{{ getAddressShortName(address!) }}</div>
|
||||
</f-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<f-button class="connect-button--loading">loading</f-button>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FButton from "@/components/fcomponents/FButton.vue";
|
||||
import { getAddressShortName } from "@/utils/helper";
|
||||
import { useAccount } from "@wagmi/vue";
|
||||
import { getBlocky } from "@/utils/blockies";
|
||||
|
||||
const { address, status } = useAccount();
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
:root,[data-theme="default"]
|
||||
--color-connect-button--disconnected: var(--color-primary)
|
||||
--font-color-connect-button--disconnected: var(--color-bright-white)
|
||||
--color-connect-button--connected: unset
|
||||
--font-color-connect-button--connected: var(--color-bright-white)
|
||||
--color-connect-button-border--connected: var(--color-grey)
|
||||
--color-connect-button-hovered--connected: #060f18
|
||||
|
||||
[data-theme="dark"]
|
||||
--color-connect-button--disconnected: var(--color-based-blue)
|
||||
--font-color-connect-button--disconnected: var(--color-bright-white)
|
||||
--color-connect-button--connected: var(--color-midnight-black)
|
||||
--font-color-connect-button--connected: var(--color-bright-white)
|
||||
--color-connect-button-border--connected: var(--color-very-light-grey)
|
||||
--color-connect-button-hovered--connected: var(--color-midnight-black-hovered)
|
||||
|
||||
.f-btn
|
||||
&.connect-button
|
||||
padding: 8px
|
||||
align-items: center
|
||||
gap: 12px
|
||||
&.connect-button--disconnected
|
||||
// background-color: var(--color-midnight-black)
|
||||
background-color: var(--color-connect-button--disconnected)
|
||||
color: var(--font-color-connect-button--disconnected)
|
||||
&.connect-button--connected
|
||||
background-color: var(--color-connect-button--connected)
|
||||
border: 1px solid var(--color-connect-button-border--connected)
|
||||
color: var(--font-color-connect-button--connected)
|
||||
&:hover, &:focus, &:active
|
||||
background-color: #5f4884
|
||||
cursor: pointer
|
||||
</style>
|
||||
236
web-app/src/components/layouts/ConnectWallet.vue
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
<template>
|
||||
<div class="connect-wallet-wrapper">
|
||||
<template v-if="status === 'connected'">
|
||||
<div class="connected-header">
|
||||
<div class="connected-header-avatar">
|
||||
<img :src="getBlocky(address!)" alt="avatar" />
|
||||
</div>
|
||||
<div class="connected-header-address" :title="address">
|
||||
{{ getAddressShortName(address!) }}
|
||||
</div>
|
||||
<div class="connected-header-logout" @click="() => disconnect()">
|
||||
<img src="@/assets/logout.svg" alt="Logout" />
|
||||
</div>
|
||||
</div>
|
||||
<h6 class="connected-tokens-headline">Your Tokens</h6>
|
||||
<div class="connected-tokens">
|
||||
<f-output name="$KRK" :price="harbAmount" :variant="3">
|
||||
<template #end>
|
||||
<f-button size="small" dense
|
||||
><a :href="chain.chainData?.uniswap" target="_blank">Buy</a></f-button
|
||||
>
|
||||
</template>
|
||||
</f-output>
|
||||
<f-output name="Staked $KRK" :price="compactNumber(stakedAmount)" :variant="3">
|
||||
<template #end>
|
||||
<f-button size="small" dense @click="stakeLink">Stake</f-button>
|
||||
</template>
|
||||
</f-output>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="status === 'disconnected'">
|
||||
<h6 class="connect-wallet-headline low-mid-text">Connect your wallet</h6>
|
||||
<div class="connectors-wrapper">
|
||||
<div
|
||||
class="connectors-element card"
|
||||
:class="[[connector.id]]"
|
||||
v-for="connector in connectors"
|
||||
:key="connector.id"
|
||||
@click="connectWallet(connector, chainId)"
|
||||
>
|
||||
<div class="connector-icon">
|
||||
<img :src="loadConnectorImage(connector)" :alt="`${connector.name} Logo`" />
|
||||
</div>
|
||||
<div class="connector-name">
|
||||
{{ connector.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>Are you coming from Binance, Bybit, Kucoin or any other crypto exchange?</div>
|
||||
<div><u>Follow these instructions first.</u></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div>loading</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getCurrentInstance, computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getAddressShortName, compactNumber, formatBigIntDivision } from "@/utils/helper";
|
||||
import { getBlocky } from "@/utils/blockies";
|
||||
import FButton from "@/components/fcomponents/FButton.vue";
|
||||
import FOutput from "@/components/fcomponents/FOutput.vue";
|
||||
|
||||
// import { usePositions } from "@/composables/usePositions";
|
||||
// import { useWallet } from "@/composables/useWallet";
|
||||
import { usePositions } from "@/composables/usePositions";
|
||||
import { useWallet } from "@/composables/useWallet";
|
||||
import { useChain } from "@/composables/useChain";
|
||||
|
||||
import {
|
||||
useAccount,
|
||||
useDisconnect,
|
||||
useConnect,
|
||||
useChainId,
|
||||
useBalance,
|
||||
type CreateConnectorFn,
|
||||
type Connector,
|
||||
} from "@wagmi/vue";
|
||||
|
||||
const { address, status } = useAccount();
|
||||
const { disconnect } = useDisconnect();
|
||||
const { connectors, connect, error } = useConnect();
|
||||
const router = useRouter();
|
||||
const chainId = useChainId();
|
||||
const { myActivePositions } = usePositions();
|
||||
const wallet = useWallet();
|
||||
const chain = useChain();
|
||||
function loadConnectorImage(connector: Connector) {
|
||||
if (connector.icon) {
|
||||
return connector.icon;
|
||||
} else {
|
||||
return `./img/connectors/${connector.name}.svg`;
|
||||
}
|
||||
}
|
||||
// const goerliKey = import.meta.env.VITE_GOERLI_KEY;
|
||||
|
||||
const harbAmount = computed(() => {
|
||||
if (wallet.balance?.value) {
|
||||
return compactNumber(formatBigIntDivision(wallet.balance?.value, 10n ** BigInt(wallet.balance.decimals)));
|
||||
} else {
|
||||
return "0";
|
||||
}
|
||||
});
|
||||
|
||||
const stakedAmount = computed(() =>
|
||||
myActivePositions.value.reduce(function (a, b) {
|
||||
return a + b.amount;
|
||||
}, 0)
|
||||
);
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
// const connectors = computed(() => {
|
||||
// console.log();
|
||||
// return wallet.connectors.filter((obj) => obj.id !== "injected");
|
||||
// });
|
||||
|
||||
// async function disconnect() {
|
||||
// console.log("disconnectWallet");
|
||||
// try {
|
||||
// await wallet.disconnectWallet();
|
||||
// instance.parent.emit("update:modelValue", false);
|
||||
// } catch (error) {
|
||||
// console.log("error", error);
|
||||
// }
|
||||
// }
|
||||
|
||||
// function badgeClick(token) {
|
||||
// var tokenData = tokenStore.getToken(token.contractAddress);
|
||||
// if (tokenStore.memeToken.contractAddress === token.contractAddress) {
|
||||
// //buy
|
||||
// window.open(
|
||||
// `https://app.uniswap.org/swap?inputCurrency=ETH&outputCurrency=${token.contractAddress}&chain=goerli`,
|
||||
// "_blank"
|
||||
// );
|
||||
// } else {
|
||||
// router.push({ name: "home" });
|
||||
// instance.parent.emit("update:modelValue", false);
|
||||
// }
|
||||
// }
|
||||
|
||||
function closeModal() {
|
||||
instance!.parent!.emit("update:modelValue", false);
|
||||
}
|
||||
|
||||
// //special case for metaMask, but I think that is the most used wallet
|
||||
async function connectWallet(connector: CreateConnectorFn | Connector, chainId: any) {
|
||||
console.log("connector", connector);
|
||||
console.log("connector", connector.name);
|
||||
connect({ connector, chainId });
|
||||
closeModal();
|
||||
}
|
||||
|
||||
function stakeLink() {
|
||||
router.push("/dashboard#stake");
|
||||
closeModal();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
@use 'sass:color'
|
||||
.connect-wallet-wrapper
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 24px
|
||||
color: white
|
||||
.connect-wallet-headline
|
||||
text-align: left
|
||||
.connectors-wrapper
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 8px
|
||||
.connectors-element
|
||||
display: flex
|
||||
padding: 12px
|
||||
border: 1px solid rgba(152, 161, 192, 0.24)
|
||||
background-color: white
|
||||
color: var(--midnight-black, #0F0F0F)
|
||||
&:hover, &:active, &:focus
|
||||
background-color: color.adjust(white, $lightness: -20%)
|
||||
cursor: pointer
|
||||
.connector-icon
|
||||
border-radius: 12px
|
||||
width: 40px
|
||||
overflow: hidden
|
||||
height: 40px
|
||||
img
|
||||
width: 40px
|
||||
.connector-name
|
||||
padding: 0 8px
|
||||
align-self: center
|
||||
font-size: 16px
|
||||
@media (min-width: 768px)
|
||||
font-size: 18px
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.connected-header
|
||||
display: flex
|
||||
gap: 16px
|
||||
align-items: center
|
||||
.connected-header-avatar
|
||||
img
|
||||
border-radius: 8px
|
||||
.connected-header-logout
|
||||
margin-left: auto
|
||||
&:hover, &:active, &:focus
|
||||
cursor: pointer
|
||||
svg
|
||||
font-size: 25px
|
||||
|
||||
.connected-tokens-headline
|
||||
text-align: left
|
||||
.connected-tokens
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 16px
|
||||
margin-bottom: 24px
|
||||
.connected-token
|
||||
display: flex
|
||||
gap: 16px
|
||||
align-items: center
|
||||
.connected-token-button
|
||||
margin-left: auto
|
||||
.bloodline-badge
|
||||
padding: 4px 16px
|
||||
color: black
|
||||
font-size: 14px
|
||||
&:hover, &:active, &:focus
|
||||
cursor: pointer
|
||||
background-color: var(--grey-light)
|
||||
</style>
|
||||
57
web-app/src/components/layouts/FFooter.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div class="footer" :style="{ color: (props.dark) ? 'white': 'black' }">
|
||||
<div class="footer-inner">
|
||||
<div class="footer-headline subheader2">Follow HARBERG Protocol</div>
|
||||
<div class="social-links">
|
||||
<social-button :dark="props.dark" type="discord" :href="discord"></social-button>
|
||||
<social-button :dark="props.dark" type="telegram" :href="telegram"></social-button>
|
||||
<social-button :dark="props.dark" type="twitter" :href="twitter"></social-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import socialButton from '@/components/socialButton.vue';
|
||||
|
||||
interface Props {
|
||||
dark?: boolean;
|
||||
}
|
||||
|
||||
const discord = import.meta.env.VITE_DISCORD
|
||||
const telegram = import.meta.env.VITE_TELEGRAM;
|
||||
const twitter = import.meta.env.VITE_TWITTER;
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.footer
|
||||
height: auto
|
||||
background-color: var(--midnight-black)
|
||||
border-top: 0.5px solid
|
||||
margin-top: auto
|
||||
@media (min-width: 768px)
|
||||
height: 150px
|
||||
min-height: 150px
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
padding-bottom: unset
|
||||
padding-bottom: 72px
|
||||
.footer-inner
|
||||
padding: 24px 0
|
||||
font-size: 24px
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 16px
|
||||
align-items: center
|
||||
border-top: 1px solid #000
|
||||
@media (min-width: 768px)
|
||||
padding: 16px 0 24px 0
|
||||
justify-content: center
|
||||
border-top: unset
|
||||
background-color: transparent
|
||||
.footer-headline subheader2
|
||||
font-size: 16px
|
||||
</style>
|
||||
197
web-app/src/components/layouts/Navbar.vue
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
<template>
|
||||
<header>
|
||||
<div class="navbar">
|
||||
<div class="navbar-inner navigation-font">
|
||||
<div class="navbar-left" @click="router.push('/')">
|
||||
<div class="navbar-title">
|
||||
<span>K</span>
|
||||
<span class="big-spacing">r</span>
|
||||
<span class="small-spacing">A</span>
|
||||
<span class="big-spacing">I</span>
|
||||
<span>ken</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-center"></div>
|
||||
<div class="navbar-end">
|
||||
<nav v-if="!isMobile">
|
||||
<RouterLink v-for="navbarRoute in navbarRoutes" :key="navbarRoute.name" :to="navbarRoute.path">
|
||||
{{ navbarRoute.title }}
|
||||
</RouterLink>
|
||||
<a href="https://emberspirit007.github.io/KraikenLanding/#/docs/Introduction" target="_blank">Docs</a>
|
||||
</nav>
|
||||
<template v-if="!isMobile">
|
||||
<div class="vertical-line"></div>
|
||||
<network-changer></network-changer>
|
||||
<div class="vertical-line"></div>
|
||||
</template>
|
||||
<connect-button @click="showPanel = true" v-if="!isMobile"></connect-button>
|
||||
<icon-login @click="showPanel = true" v-else-if="isMobile && !address"></icon-login>
|
||||
<img
|
||||
@click="showPanel = true"
|
||||
v-else-if="isMobile && address"
|
||||
:src="getBlocky(address)"
|
||||
alt="avatar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getBlocky } from "@/utils/blockies";
|
||||
import { RouterLink, useRouter } from "vue-router";
|
||||
import IconLogin from "@/components/icons/IconLogin.vue";
|
||||
import ThemeToggle from "@/components/layouts/ThemeToggle.vue";
|
||||
import NetworkChanger from "@/components/layouts/NetworkChanger.vue";
|
||||
import ConnectButton from "@/components/layouts/ConnectButton.vue";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { useAccount } from "@wagmi/vue";
|
||||
import { useDark } from "@/composables/useDark";
|
||||
const {darkTheme} = useDark();
|
||||
const {address} = useAccount();
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
const showPanel = inject<boolean>("showPanel", false);
|
||||
const isMobile = inject<boolean>("isMobile", false);
|
||||
|
||||
const dark1 = ref(false)
|
||||
const routes = router.getRoutes();
|
||||
const navbarRoutes = routes.flatMap((obj) => {
|
||||
if (obj.meta.group === "navbar") {
|
||||
return { title: obj.meta.title, name: obj.name, path: obj.path };
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
header
|
||||
z-index: 10
|
||||
position: fixed
|
||||
width: 100%
|
||||
.navbar
|
||||
height: 80px
|
||||
z-index: 3
|
||||
box-sizing: border-box
|
||||
transform: translate3d(0,0,0)
|
||||
background-color: var(--color-navbar-bg)
|
||||
transition: transform .3s ease,visibility 0s .3s linear
|
||||
border-bottom: 1px solid var(--color-navbar-border)
|
||||
.navbar-inner, .navbar-end, .navbar-center, .navbar-text
|
||||
align-items: center
|
||||
.navbar-inner
|
||||
display: flex
|
||||
padding: 16px 24px
|
||||
box-sizing: border-box
|
||||
height: 100%
|
||||
align-items: center
|
||||
@media (min-width: 768px)
|
||||
padding: 16px 24px
|
||||
.navbar-icon
|
||||
margin-right: 16px
|
||||
img
|
||||
height: 50px
|
||||
width: 50px
|
||||
@media (min-width: 768px)
|
||||
height: 56px
|
||||
width: 56px
|
||||
|
||||
.navbar-text
|
||||
font-size: 25px
|
||||
letter-spacing: 0.138em
|
||||
font-family: "Play-Bold"
|
||||
font-weight: bold
|
||||
|
||||
@media (min-width: 768px)
|
||||
font-size: 40px
|
||||
a
|
||||
text-decoration: none
|
||||
color: white
|
||||
.navbar-left
|
||||
display: flex
|
||||
gap: 8px
|
||||
letter-spacing: 3.6px
|
||||
align-items: center
|
||||
font-size: 32px
|
||||
font-weight: 400
|
||||
&:hover, &:active, &:focus
|
||||
cursor: pointer
|
||||
.navbar-title
|
||||
font-family: 'Audiowide', sans-serif
|
||||
*>div
|
||||
font-family: 'Audiowide', sans-serif
|
||||
@media (min-width: 768px)
|
||||
display: block
|
||||
.big-spacing
|
||||
letter-spacing: 5.76px
|
||||
.small-spacing
|
||||
letter-spacing: 1.8px
|
||||
.navbar-center
|
||||
display: flex
|
||||
margin-left: auto
|
||||
.navbar-end
|
||||
display: flex
|
||||
margin-left: auto
|
||||
height: 100%
|
||||
nav
|
||||
display: flex
|
||||
gap: 50px
|
||||
a
|
||||
&.router-link-active
|
||||
border-bottom: 2px solid var(--color-based-blue)
|
||||
|
||||
.navbar-navigation
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 48px
|
||||
.navbar-navigation-link
|
||||
display: flex
|
||||
gap: 8px
|
||||
border-bottom: 2px solid transparent
|
||||
&.active
|
||||
border-bottom: 2px solid white
|
||||
&:active, &:hover, &:focus
|
||||
cursor: pointer
|
||||
.icon
|
||||
img
|
||||
width: 20px
|
||||
height: 20px
|
||||
|
||||
|
||||
|
||||
.login-icon
|
||||
color: var(--color-navbar-font)
|
||||
padding: 8px
|
||||
font-size: 25px
|
||||
border-radius: 8px
|
||||
width: 44px
|
||||
height: 44px
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
img
|
||||
width: 32px
|
||||
height: 32px
|
||||
border-radius: 8px
|
||||
.login-icon-desktop
|
||||
display: flex
|
||||
padding: 8px
|
||||
align-items: center
|
||||
gap: 16px
|
||||
border-radius: 16px
|
||||
background: var(--Background-grey, #2D2D2D)
|
||||
&:hover,&:active,&:focus
|
||||
cursor: pointer
|
||||
img
|
||||
width: 32px
|
||||
height: 32px
|
||||
|
||||
.vertical-line
|
||||
width: 1px
|
||||
background-color: var(--color-border-main)
|
||||
height: 100%
|
||||
margin: 0 32px
|
||||
</style>
|
||||
127
web-app/src/components/layouts/NetworkChanger.vue
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<template>
|
||||
<div class="network-changer" v-click-outside="closeMenu">
|
||||
<div class="network-changer-inner" @click="showMenu = !showMenu">
|
||||
<icon-base></icon-base>
|
||||
<Icon v-if="showMenu" class="toggle-icon" icon="mdi:chevron-down"></Icon>
|
||||
<Icon v-else class="toggle-icon" icon="mdi:chevron-up"></Icon>
|
||||
</div>
|
||||
<Transition name="collapse-network">
|
||||
<div v-show="showMenu" class="network-changer--list">
|
||||
<div class="list-inner" ref="listInner">
|
||||
<div class="list--element" v-for="chain in chains" :key="chain.id">
|
||||
<template v-if="chain.id === wallet.account.chainId">
|
||||
<icon-base></icon-base>
|
||||
<div>{{ chain.name }}</div>
|
||||
<div>Connected</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<icon-base></icon-base>
|
||||
<div>{{ chain.name }}</div>
|
||||
<f-button outlined dense @click="switchNetwork(chain)">Switch Network</f-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconBase from "@/components/icons/IconBase.vue";
|
||||
import { ref } from "vue";
|
||||
import { useChains, useChainId, useSwitchChain } from "@wagmi/vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import FButton from "@/components/fcomponents/FButton.vue";
|
||||
import { useWallet } from "@/composables/useWallet";
|
||||
const chains = useChains();
|
||||
const wallet = useWallet();
|
||||
const { switchChain } = useSwitchChain();
|
||||
|
||||
const showMenu = ref<boolean>(false);
|
||||
const listInner = ref();
|
||||
|
||||
function closeMenu() {
|
||||
if (showMenu.value) {
|
||||
console.log("tesat");
|
||||
showMenu.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function switchNetwork(chain: any) {
|
||||
console.log("chain", chain);
|
||||
switchChain({ chainId: chain.id });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
:root,[data-theme="default"]
|
||||
--color-network-changer: unset
|
||||
--color-network-changer-hovered: #060f18
|
||||
--color-network-changer-toggle-icon: white
|
||||
|
||||
[data-theme="dark"]
|
||||
--color-network-changer: var(--color-text-primary)
|
||||
--color-network-changer-hovered: var(--color-text-primary-hovered)
|
||||
--color-network-changer-toggle-icon: var(--color-bright-white)
|
||||
.network-changer
|
||||
position: relative
|
||||
.network-changer-inner
|
||||
display: flex
|
||||
background-color: var(--color-network-changer)
|
||||
padding: 8px
|
||||
border-radius: 16px
|
||||
align-items: center
|
||||
gap: 8px
|
||||
&:hover, &:focus, &:active
|
||||
background-color: var(--color-network-changer-hovered)
|
||||
cursor: pointer
|
||||
.toggle-icon
|
||||
pointer-events: none
|
||||
path
|
||||
fill: var(--color-network-changer-toggle-icon)
|
||||
.network-changer--list
|
||||
margin-top: 8px
|
||||
position: absolute
|
||||
// padding: 8px
|
||||
color: var(--color-white)
|
||||
background-color: var(--color-text-primary)
|
||||
// bottom: calc(-100% - 8px)
|
||||
overflow: hidden
|
||||
right: 0
|
||||
width: 300px
|
||||
// height: 108px
|
||||
border-radius: 16px
|
||||
.list-inner
|
||||
padding: 8px 0px
|
||||
font-size: var(--font-body1)
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0
|
||||
height: 100%
|
||||
.list--element
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 8px
|
||||
padding: 8px 16px
|
||||
:last-child
|
||||
margin-left: auto
|
||||
.f-btn
|
||||
padding: 6px 8px
|
||||
|
||||
|
||||
|
||||
.collapse-network-enter-active
|
||||
animation: collapse reverse 200ms ease
|
||||
|
||||
.collapse-network-leave-active
|
||||
animation: collapse 200ms ease
|
||||
|
||||
@keyframes collapse-network
|
||||
from
|
||||
max-height: 100%
|
||||
// bottom: calc(-108px - 8px)
|
||||
bottom: calc(-100% - 8px)
|
||||
to
|
||||
max-height: 0px
|
||||
bottom: -8px
|
||||
</style>
|
||||
71
web-app/src/components/layouts/SlideoutPanel.vue
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<div class="slideout-overlay" :class="{ open: props.modelValue }" @click="showPanel = false"></div>
|
||||
<div :class="{ 'slideout-wrapper': true, open: props.modelValue }">
|
||||
<div class="slideout-panel">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, watchEffect } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
});
|
||||
|
||||
defineEmits(["update:modelValue"]);
|
||||
|
||||
const showPanel = inject("showPanel");
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
@use 'sass:color'
|
||||
.slideout-overlay
|
||||
position: fixed
|
||||
z-index: 100
|
||||
&.open
|
||||
height: 100%
|
||||
width: 100%
|
||||
background-color: rgb(15, 15, 15, 0.7)
|
||||
.slideout-wrapper
|
||||
z-index: 100
|
||||
height: 100svh
|
||||
position: fixed
|
||||
display: flex
|
||||
background-color: var(--midnight-black, #0F0F0F)
|
||||
border-radius: 16px
|
||||
transition: margin-bottom 250ms ease 0s
|
||||
width: 100%
|
||||
margin-bottom: -100vh
|
||||
bottom: 0
|
||||
overflow: auto
|
||||
@media (min-width: 768px)
|
||||
margin-right: -400px
|
||||
transition: margin-right 250ms ease 0s
|
||||
width: auto
|
||||
right: -30px
|
||||
top: 0
|
||||
&.open
|
||||
margin-bottom: -82px
|
||||
@media (min-width: 768px)
|
||||
margin-right: 30px
|
||||
.slideout-panel
|
||||
width: 100%
|
||||
padding: 24px 24px 16px 24px
|
||||
overflow: scroll
|
||||
// height: calc( 100svh - 82px )
|
||||
display: flex
|
||||
flex-direction: column
|
||||
height: calc( 100vh - 80px)
|
||||
@media (min-width: 768px)
|
||||
width: 400px
|
||||
overflow: unset
|
||||
height: 100%
|
||||
.slideout-icon
|
||||
cursor: pointer
|
||||
&:hover, &:active, &:focus
|
||||
background-color: color.adjust(#2D2D2D, $lightness: 20%)
|
||||
svg
|
||||
font-size: 30px
|
||||
</style>
|
||||
217
web-app/src/components/layouts/ThemeToggle.vue
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
<template>
|
||||
<div class="toggle-dark-light">
|
||||
<input type="checkbox" id="darkmode-toggle" @click="toggleTheme" :checked="props.modelValue" />
|
||||
<label for="darkmode-toggle">
|
||||
<svg
|
||||
version="1.1"
|
||||
class="sun"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 496 496"
|
||||
style="enable-background: new 0 0 496 496"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<rect
|
||||
x="152.994"
|
||||
y="58.921"
|
||||
transform="matrix(0.3827 0.9239 -0.9239 0.3827 168.6176 -118.5145)"
|
||||
width="40.001"
|
||||
height="16"
|
||||
/>
|
||||
<rect
|
||||
x="46.9"
|
||||
y="164.979"
|
||||
transform="matrix(0.9239 0.3827 -0.3827 0.9239 71.29 -12.4346)"
|
||||
width="40.001"
|
||||
height="16"
|
||||
/>
|
||||
<rect
|
||||
x="46.947"
|
||||
y="315.048"
|
||||
transform="matrix(0.9239 -0.3827 0.3827 0.9239 -118.531 50.2116)"
|
||||
width="40.001"
|
||||
height="16"
|
||||
/>
|
||||
|
||||
<rect
|
||||
x="164.966"
|
||||
y="409.112"
|
||||
transform="matrix(-0.9238 -0.3828 0.3828 -0.9238 168.4872 891.7491)"
|
||||
width="16"
|
||||
height="39.999"
|
||||
/>
|
||||
|
||||
<rect
|
||||
x="303.031"
|
||||
y="421.036"
|
||||
transform="matrix(-0.3827 -0.9239 0.9239 -0.3827 50.2758 891.6655)"
|
||||
width="40.001"
|
||||
height="16"
|
||||
/>
|
||||
|
||||
<rect
|
||||
x="409.088"
|
||||
y="315.018"
|
||||
transform="matrix(-0.9239 -0.3827 0.3827 -0.9239 701.898 785.6559)"
|
||||
width="40.001"
|
||||
height="16"
|
||||
/>
|
||||
|
||||
<rect
|
||||
x="409.054"
|
||||
y="165.011"
|
||||
transform="matrix(-0.9239 0.3827 -0.3827 -0.9239 891.6585 168.6574)"
|
||||
width="40.001"
|
||||
height="16"
|
||||
/>
|
||||
<rect
|
||||
x="315.001"
|
||||
y="46.895"
|
||||
transform="matrix(0.9238 0.3828 -0.3828 0.9238 50.212 -118.5529)"
|
||||
width="16"
|
||||
height="39.999"
|
||||
/>
|
||||
<path
|
||||
d="M248,88c-88.224,0-160,71.776-160,160s71.776,160,160,160s160-71.776,160-160S336.224,88,248,88z M248,392
|
||||
c-79.4,0-144-64.6-144-144s64.6-144,144-144s144,64.6,144,144S327.4,392,248,392z"
|
||||
/>
|
||||
<rect x="240" width="16" height="72" />
|
||||
<rect
|
||||
x="62.097"
|
||||
y="90.096"
|
||||
transform="matrix(0.7071 0.7071 -0.7071 0.7071 98.0963 -40.6334)"
|
||||
width="71.999"
|
||||
height="16"
|
||||
/>
|
||||
<rect y="240" width="72" height="16" />
|
||||
|
||||
<rect
|
||||
x="90.091"
|
||||
y="361.915"
|
||||
transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 -113.9157 748.643)"
|
||||
width="16"
|
||||
height="71.999"
|
||||
/>
|
||||
<rect x="240" y="424" width="16" height="72" />
|
||||
|
||||
<rect
|
||||
x="361.881"
|
||||
y="389.915"
|
||||
transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 397.8562 960.6281)"
|
||||
width="71.999"
|
||||
height="16"
|
||||
/>
|
||||
<rect x="424" y="240" width="72" height="16" />
|
||||
<rect
|
||||
x="389.911"
|
||||
y="62.091"
|
||||
transform="matrix(0.7071 0.7071 -0.7071 0.7071 185.9067 -252.6357)"
|
||||
width="16"
|
||||
height="71.999"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
version="1.1"
|
||||
class="moon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 49.739 49.739"
|
||||
style="enable-background: new 0 0 49.739 49.739"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<path
|
||||
d="M25.068,48.889c-9.173,0-18.017-5.06-22.396-13.804C-3.373,23.008,1.164,8.467,13.003,1.979l2.061-1.129l-0.615,2.268
|
||||
c-1.479,5.459-0.899,11.25,1.633,16.306c2.75,5.493,7.476,9.587,13.305,11.526c5.831,1.939,12.065,1.492,17.559-1.258v0
|
||||
c0.25-0.125,0.492-0.258,0.734-0.391l2.061-1.13l-0.585,2.252c-1.863,6.873-6.577,12.639-12.933,15.822
|
||||
C32.639,48.039,28.825,48.888,25.068,48.889z M12.002,4.936c-9.413,6.428-12.756,18.837-7.54,29.253
|
||||
c5.678,11.34,19.522,15.945,30.864,10.268c5.154-2.582,9.136-7.012,11.181-12.357c-5.632,2.427-11.882,2.702-17.752,0.748
|
||||
c-6.337-2.108-11.473-6.557-14.463-12.528C11.899,15.541,11.11,10.16,12.002,4.936z"
|
||||
/>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
}>();
|
||||
|
||||
function toggleTheme(event: any) {
|
||||
emit("update:modelValue", event.target.checked);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.toggle-dark-light
|
||||
display: flex
|
||||
label
|
||||
width: 62.5px
|
||||
height: 25px
|
||||
position: relative
|
||||
display: block
|
||||
background: #ebebeb
|
||||
border-radius: 50vh
|
||||
box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.4), inset 0px -1px 3px rgba(255, 255, 255, 0.4)
|
||||
cursor: pointer
|
||||
transition: 0.3s
|
||||
&:after
|
||||
content: ""
|
||||
width: 36%
|
||||
height: 90%
|
||||
position: absolute
|
||||
top: 5%
|
||||
left: 2%
|
||||
background: linear-gradient(180deg, #ffcc89, #d8860b)
|
||||
border-radius: 180px
|
||||
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.2)
|
||||
transition: 0.3s
|
||||
|
||||
input
|
||||
width: 0
|
||||
height: 0
|
||||
visibility: hidden
|
||||
|
||||
&:checked + label
|
||||
background: #242424
|
||||
|
||||
&:after
|
||||
left: 98%
|
||||
transform: translateX(-100%)
|
||||
background: linear-gradient(180deg, #777, #3a3a3a)
|
||||
|
||||
label:active:after
|
||||
width: 52%
|
||||
|
||||
input:checked + label + .background
|
||||
background: #242424
|
||||
|
||||
label svg
|
||||
position: absolute
|
||||
width: 24%
|
||||
top: 20%
|
||||
z-index: 100
|
||||
|
||||
&.sun
|
||||
left: 8%
|
||||
fill: #fff
|
||||
transition: 0.3s
|
||||
|
||||
&.moon
|
||||
left: 68%
|
||||
fill: #7e7e7e
|
||||
transition: 0.3s
|
||||
|
||||
input:checked + label svg
|
||||
&.sun
|
||||
fill: #7e7e7e
|
||||
|
||||
&.moon
|
||||
fill: #fff
|
||||
</style>
|
||||
97
web-app/src/components/socialButton.spec.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { defineAsyncComponent } from "vue";
|
||||
import { mount, flushPromises } from "@vue/test-utils";
|
||||
import SocialBadge from "./socialButton.vue";
|
||||
|
||||
|
||||
const mockIconComponent = (name:string) => ({
|
||||
default: {
|
||||
name,
|
||||
template: `<svg class='${name.toLowerCase()}'></svg>`,
|
||||
},
|
||||
});
|
||||
|
||||
vi.mock("../components/icons/IconDiscord.vue", () => mockIconComponent("IconDiscord"));
|
||||
vi.mock("../components/icons/IconTwitter.vue", () => mockIconComponent("IconTwitter"));
|
||||
vi.mock("../components/icons/IconTelegram.vue", () => mockIconComponent("IconTelegram"));
|
||||
|
||||
|
||||
|
||||
describe("SocialBadge.vue", () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
it("renders the correct link", () => {
|
||||
wrapper = mount(SocialBadge, {
|
||||
props: {
|
||||
href: "https://example.com",
|
||||
},
|
||||
});
|
||||
|
||||
const link = wrapper.find("a");
|
||||
expect(link.exists()).toBe(true);
|
||||
expect(link.attributes("href")).toBe("https://example.com");
|
||||
expect(link.attributes("target")).toBe("_blank");
|
||||
});
|
||||
|
||||
it("applies the correct color for light mode", () => {
|
||||
wrapper = mount(SocialBadge, {
|
||||
props: {
|
||||
dark: false,
|
||||
},
|
||||
});
|
||||
|
||||
const badge = wrapper.find(".social-badge");
|
||||
expect(badge.attributes("style")).toContain("color: black");
|
||||
expect(badge.attributes("style")).toContain("border-color: black");
|
||||
});
|
||||
|
||||
it("applies the correct color for dark mode", () => {
|
||||
wrapper = mount(SocialBadge, {
|
||||
props: {
|
||||
dark: true,
|
||||
},
|
||||
});
|
||||
|
||||
const badge = wrapper.find(".social-badge");
|
||||
expect(badge.attributes("style")).toContain("color: white");
|
||||
expect(badge.attributes("style")).toContain("border-color: white");
|
||||
});
|
||||
|
||||
// it("renders the correct icon based on the type prop", async () => {
|
||||
// wrapper = mount(SocialBadge, {
|
||||
// props: {
|
||||
// type: "discord",
|
||||
// },
|
||||
// });
|
||||
// await flushPromises();
|
||||
// const current = wrapper.getCurrentComponent()?.setupState?.img.__asyncResolved
|
||||
// console.log("current", current.default.name);
|
||||
|
||||
// expect(current.default.name).toBe("IconDiscord");
|
||||
|
||||
// // expect(icon.exists()).toBe(true);
|
||||
// });
|
||||
|
||||
it("does not render an icon if the type is unsupported", async () => {
|
||||
wrapper = mount(SocialBadge, {
|
||||
props: {
|
||||
type: "unsupported",
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const icon = wrapper.find(".social-badge-icon").find("component");
|
||||
expect(icon.exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("renders without crashing when no props are provided", () => {
|
||||
wrapper = mount(SocialBadge);
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
64
web-app/src/components/socialButton.vue
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<a :href="props.href" target="_blank">
|
||||
<div
|
||||
class="social-badge"
|
||||
:style="{ color: color, 'border-color': color }"
|
||||
:class="{ 'social-badge--dark': props.dark }"
|
||||
>
|
||||
<div class="social-badge-icon">
|
||||
<component :color="color" :is="img" />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, computed } from "vue";
|
||||
|
||||
interface Props {
|
||||
type?: string;
|
||||
dark?: boolean;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const color = computed(() => (props.dark ? "white" : "black"));
|
||||
|
||||
const img = computed(() => {
|
||||
let img;
|
||||
switch (props.type) {
|
||||
case "discord":
|
||||
img = defineAsyncComponent(() => import(`../components/icons/IconDiscord.vue`));
|
||||
break;
|
||||
case "twitter":
|
||||
img = defineAsyncComponent(() => import(`../components/icons/IconTwitter.vue`));
|
||||
break;
|
||||
case "telegram":
|
||||
img = defineAsyncComponent(() => import(`../components/icons/IconTelegram.vue`));
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return img;
|
||||
});
|
||||
</script>
|
||||
<style lang="sass">
|
||||
.social-badge
|
||||
border-radius: 14px
|
||||
display: flex
|
||||
border: 1px solid var(--color-social-border)
|
||||
padding: 6px 20px
|
||||
align-items: center
|
||||
flex: 0 1 0
|
||||
color: black
|
||||
&:hover,&:active,&:focus
|
||||
background-color: var(--color-white-hovered)
|
||||
cursor: pointer
|
||||
&.social-badge--dark
|
||||
&:hover, &:active, &:focus
|
||||
background-color: var(--color-black-hovered)
|
||||
// font-size: 0
|
||||
</style>
|
||||
98
web-app/src/composables/useAdjustTaxRates.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { ref, onMounted, onUnmounted, reactive, computed, type ComputedRef } from "vue";
|
||||
import * as StakeContract from "@/contracts/stake";
|
||||
import { waitForTransactionReceipt } from "@wagmi/core";
|
||||
import { config } from "@/wagmi";
|
||||
import { compactNumber, formatBigIntDivision } from "@/utils/helper";
|
||||
import { useContractToast } from "./useContractToast";
|
||||
|
||||
const contractToast = useContractToast();
|
||||
|
||||
enum State {
|
||||
SignTransaction = "SignTransaction",
|
||||
Waiting = "Waiting",
|
||||
Action = "Action",
|
||||
}
|
||||
|
||||
export const taxRates = [
|
||||
{ index: 0,year: 1, daily: 0.00274 },
|
||||
{ index: 1, year: 3, daily: 0.00822 },
|
||||
{ index: 2, year: 5, daily: 0.0137 },
|
||||
{ index: 3, year: 8, daily: 0.02192 },
|
||||
{ index: 4, year: 12, daily: 0.03288 },
|
||||
{ index: 5, year: 18, daily: 0.04932 },
|
||||
{ index: 6, year: 24, daily: 0.06575 },
|
||||
{ index: 7, year: 30, daily: 0.08219 },
|
||||
{ index: 8, year: 40, daily: 0.10959 },
|
||||
{ index: 9, year: 50, daily: 0.13699 },
|
||||
{ index: 10, year: 60, daily: 0.16438 },
|
||||
{ index: 11, year: 80, daily: 0.21918 },
|
||||
{ index: 12, year: 100, daily: 0.27397 },
|
||||
{ index: 13, year: 130, daily: 0.35616 },
|
||||
{ index: 14, year: 180, daily: 0.49315 },
|
||||
{ index: 15, year: 250, daily: 0.68493 },
|
||||
{ index: 16, year: 320, daily: 0.87671 },
|
||||
{ index: 17, year: 420, daily: 1.15068 },
|
||||
{ index: 18, year: 540, daily: 1.47945 },
|
||||
{ index: 19, year: 700, daily: 1.91781 },
|
||||
{ index: 20, year: 920, daily: 2.52055 },
|
||||
{ index: 21, year: 1200, daily: 3.28767 },
|
||||
{ index: 22, year: 1600, daily: 4.38356 },
|
||||
{ index: 23, year: 2000, daily: 5.47945 },
|
||||
{ index: 24, year: 2600, daily: 7.12329 },
|
||||
{ index: 25, year: 3400, daily: 9.31507 },
|
||||
{ index: 26, year: 4400, daily: 12.05479 },
|
||||
{ index: 27, year: 5700, daily: 15.61644 },
|
||||
{ index: 28, year: 7500, daily: 20.54795 },
|
||||
{ index: 29, year: 9700, daily: 26.57534 },
|
||||
];
|
||||
|
||||
export function useAdjustTaxRate() {
|
||||
const loading = ref();
|
||||
const waiting = ref();
|
||||
|
||||
|
||||
const state: ComputedRef<State> = computed(() => {
|
||||
if (loading.value) {
|
||||
return State.SignTransaction;
|
||||
} else if (waiting.value) {
|
||||
return State.Waiting;
|
||||
} else {
|
||||
return State.Action;
|
||||
}
|
||||
});
|
||||
|
||||
async function changeTax(positionId: bigint, taxRate: number) {
|
||||
try {
|
||||
console.log("changeTax", { positionId, taxRate });
|
||||
|
||||
loading.value = true;
|
||||
const index = taxRates.findIndex((obj) => obj.year === taxRate)
|
||||
const hash = await StakeContract.changeTax(positionId, index);
|
||||
console.log("hash", hash);
|
||||
loading.value = false;
|
||||
waiting.value = true;
|
||||
const data = await waitForTransactionReceipt(config as any, {
|
||||
hash: hash,
|
||||
});
|
||||
|
||||
contractToast.showSuccessToast(
|
||||
taxRate.toString(),
|
||||
"Success!",
|
||||
"You adjusted your position tax to",
|
||||
"",
|
||||
"%"
|
||||
);
|
||||
waiting.value = false;
|
||||
} catch (error: any) {
|
||||
console.error("error", error);
|
||||
console.log(JSON.stringify(error, (_, v) => (typeof v === "bigint" ? v.toString() : v)));
|
||||
|
||||
contractToast.showFailToast(error.shortMessage);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
waiting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return reactive({ state, taxRates, changeTax });
|
||||
}
|
||||
51
web-app/src/composables/useChain.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { ref, reactive, computed } from "vue";
|
||||
import { getChainId, watchChainId, getAccount, watchAccount } from "@wagmi/core";
|
||||
import { config } from "@/wagmi";
|
||||
import { setHarbContract } from "@/contracts/harb";
|
||||
import { setStakeContract } from "@/contracts/stake";
|
||||
import {chainsData} from "@/config"
|
||||
import logger from "@/utils/logger";
|
||||
const activeChain = ref()
|
||||
let unwatch: any = null;
|
||||
|
||||
|
||||
|
||||
export const chainData = computed(() => {
|
||||
return chainsData.find((obj) => obj.id === activeChain.value)
|
||||
})
|
||||
|
||||
export function useChain() {
|
||||
|
||||
|
||||
// if (!unwatch) {
|
||||
// console.log("useChain function");
|
||||
// const chain = getChainId(config as any)
|
||||
// activeChain.value = chain
|
||||
// unwatch = watchChainId(config as any, {
|
||||
// async onChange(chainId) {
|
||||
// console.log("Chain changed", chainId);
|
||||
// activeChain.value = chainId
|
||||
// setHarbContract()
|
||||
// setStakeContract()
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (!unwatch) {
|
||||
// console.log("useWallet function");
|
||||
|
||||
// unwatch = watchAccount(config as any, {
|
||||
// async onChange(data) {
|
||||
// console.log("watchaccount-useChain", data);
|
||||
|
||||
// if(!data.address) {
|
||||
// } else if (activeChain.value !== data.chainId) {
|
||||
// logger.info(`Chain changed!:`, data.chainId);
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
|
||||
return reactive({ chainData });
|
||||
}
|
||||
82
web-app/src/composables/useClaim.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { ref, onMounted, onUnmounted, reactive, computed } from "vue";
|
||||
import { type ComputedRef } from "vue";
|
||||
import { config } from "@/wagmi";
|
||||
import { AbiEncodingArrayLengthMismatchError, type WatchEventReturnType } from "viem";
|
||||
import axios from "axios";
|
||||
import { getAccount, watchContractEvent, readContract, waitForTransactionReceipt, watchAccount } from "@wagmi/core";
|
||||
import { type WatchAccountReturnType } from "@wagmi/core";
|
||||
import * as HarbContract from "@/contracts/harb";
|
||||
// import HarbJson from "@/assets/contracts/harb.json";
|
||||
import { type Abi, type Address } from "viem";
|
||||
import { useWallet } from "./useWallet";
|
||||
import { compactNumber, formatBigIntDivision } from "@/utils/helper";
|
||||
|
||||
const wallet = useWallet();
|
||||
|
||||
const loading = ref(false);
|
||||
const waiting = ref(false);
|
||||
const ubiDue = HarbContract.ubiDue;
|
||||
let unwatch: WatchAccountReturnType;
|
||||
|
||||
enum ClaimState {
|
||||
NothingToClaim = "NothingToClaim",
|
||||
StakeAble = "StakeAble",
|
||||
SignTransaction = "SignTransaction",
|
||||
Waiting = "Waiting",
|
||||
NotEnoughApproval = "NotEnoughApproval",
|
||||
}
|
||||
|
||||
export function useClaim() {
|
||||
|
||||
const state: ComputedRef<ClaimState> = computed(() => {
|
||||
if (loading.value) {
|
||||
return ClaimState.SignTransaction;
|
||||
} else if (ubiDue.value === 0n) {
|
||||
return ClaimState.NothingToClaim;
|
||||
} else if (waiting.value) {
|
||||
return ClaimState.Waiting;
|
||||
} else {
|
||||
return ClaimState.StakeAble;
|
||||
}
|
||||
});
|
||||
|
||||
async function claimUbi() {
|
||||
try {
|
||||
const address = wallet.account.address!;
|
||||
|
||||
loading.value = true;
|
||||
const hash = await HarbContract.claimUbi(address);
|
||||
console.log("hash", hash);
|
||||
loading.value = false;
|
||||
waiting.value = true;
|
||||
const data = await waitForTransactionReceipt(config as any, {
|
||||
hash: hash,
|
||||
});
|
||||
console.log("data.logs", data.logs);
|
||||
|
||||
} catch (error) {
|
||||
console.error("error", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
waiting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {});
|
||||
|
||||
if (!unwatch) {
|
||||
console.log("useClaim function");
|
||||
|
||||
unwatch = watchAccount(config as any, {
|
||||
async onChange(data) {
|
||||
console.log("watchAccount", data);
|
||||
|
||||
if (data.address) {
|
||||
await HarbContract.setHarbContract();
|
||||
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
return reactive({ claimUbi, ubiDue, state });
|
||||
}
|
||||
20
web-app/src/composables/useClickOutside.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import {onBeforeUnmount, onMounted} from 'vue'
|
||||
|
||||
export default function useClickOutside(component: any, callback: any) {
|
||||
|
||||
if (!component) return
|
||||
const listener = (event: any) => {
|
||||
|
||||
if (event.target !== component.value && event.composedPath().includes(component.value)) {
|
||||
|
||||
return
|
||||
}
|
||||
if (typeof callback === 'function') {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
onMounted(() => { window.addEventListener('click', listener) })
|
||||
onBeforeUnmount(() => {window.removeEventListener('click', listener)})
|
||||
|
||||
return {listener}
|
||||
}
|
||||
62
web-app/src/composables/useContractToast.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import Toast from "@/components/Toast.vue";
|
||||
import { POSITION, useToast } from "vue-toastification";
|
||||
import { reactive } from "vue";
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
export function useContractToast() {
|
||||
function showFailToast(name?: string) {
|
||||
console.log("name", name);
|
||||
if (name === "UserRejectedRequestError") {
|
||||
//
|
||||
} else {
|
||||
showSuccessToast(
|
||||
"",
|
||||
"Failed!",
|
||||
"Your transaction didn’t go through. Please try again.",
|
||||
"If the issue persists, please send a message in #helpdesk on our Discord.",
|
||||
"",
|
||||
"error"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function showSuccessToast(
|
||||
value: string,
|
||||
header: string,
|
||||
subheader: string,
|
||||
info: string,
|
||||
unit: string,
|
||||
type: string = "info"
|
||||
) {
|
||||
// Define the content object with the component, props and listeners
|
||||
const content = {
|
||||
component: Toast,
|
||||
// Any prop can be passed, but don't expect them to be reactive
|
||||
props: {
|
||||
value: value,
|
||||
header: header,
|
||||
subheader: subheader,
|
||||
info: info,
|
||||
token: unit,
|
||||
type: type,
|
||||
},
|
||||
// Listen and react to events using callbacks. In this case we listen for
|
||||
// the "click" event emitted when clicking the toast button
|
||||
listeners: {},
|
||||
};
|
||||
|
||||
// Render the toast and its contents
|
||||
toast(content, {
|
||||
position: POSITION.TOP_RIGHT,
|
||||
icon: false,
|
||||
closeOnClick: false,
|
||||
toastClassName: "modal-overlay",
|
||||
closeButton: false,
|
||||
hideProgressBar: true,
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
return reactive({ showSuccessToast, showFailToast });
|
||||
}
|
||||
38
web-app/src/composables/useDark.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
|
||||
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
// by convention, composable function names start with "use"
|
||||
const darkTheme = ref(false)
|
||||
export function useDark() {
|
||||
onMounted(() => {
|
||||
if(localStorage.getItem("theme") === "dark") {
|
||||
document.documentElement.classList.add("dark")
|
||||
darkTheme.value = true
|
||||
} else {
|
||||
darkTheme.value = false
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
watch(
|
||||
() => darkTheme.value,
|
||||
(newData) => {
|
||||
document.documentElement.removeAttribute("data-theme");
|
||||
localStorage.removeItem("theme")
|
||||
if (newData) {
|
||||
// import('@/assets/sass/elementplus-dark.scss');
|
||||
document.documentElement.classList.add("dark")
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
localStorage.setItem("theme", "dark");
|
||||
} else {
|
||||
// import('@/assets/sass/elementplus-light.scss');
|
||||
|
||||
document.documentElement.classList.remove("dark")
|
||||
document.documentElement.setAttribute("data-theme", "default");
|
||||
localStorage.setItem("theme", "default");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return { darkTheme }
|
||||
}
|
||||
30
web-app/src/composables/useMobile.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
|
||||
// by convention, composable function names start with "use"
|
||||
export function useMobile() {
|
||||
const isMobile = ref<boolean>(false);
|
||||
|
||||
const handleWindowSizeChange = () => {
|
||||
isMobile.value = isMobileFunc();
|
||||
};
|
||||
|
||||
isMobile.value = isMobileFunc();
|
||||
function isMobileFunc() {
|
||||
if (screen.width <= 768) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener("resize", handleWindowSizeChange);
|
||||
handleWindowSizeChange();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("resize", handleWindowSizeChange);
|
||||
});
|
||||
|
||||
return isMobile;
|
||||
}
|
||||
311
web-app/src/composables/usePositions.ts
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
import { ref, onMounted, onUnmounted, reactive, computed, type ComputedRef } from "vue";
|
||||
import { config } from "@/wagmi";
|
||||
import { type WatchEventReturnType, toBytes, type Hex } from "viem";
|
||||
import axios from "axios";
|
||||
import { getAccount, watchContractEvent, watchChainId, watchAccount } from "@wagmi/core";
|
||||
import type { WatchChainIdReturnType, WatchAccountReturnType, GetAccountReturnType} from "@wagmi/core";
|
||||
|
||||
import { HarbContract } from "@/contracts/harb";
|
||||
import { bytesToUint256, uint256ToBytes } from "harb-lib";
|
||||
import { bigInt2Number } from "@/utils/helper";
|
||||
import { useChain } from "@/composables/useChain";
|
||||
import { taxRates } from "@/composables/useAdjustTaxRates";
|
||||
import { chainData } from "@/composables/useWallet";
|
||||
import logger from "@/utils/logger";
|
||||
const rawActivePositions = ref<Array<Position>>([]);
|
||||
const rawClosedPositoins = ref<Array<Position>>([]);
|
||||
const loading = ref(false);
|
||||
const chain = useChain();
|
||||
const activePositions = computed(() => {
|
||||
const account = getAccount(config as any);
|
||||
|
||||
return rawActivePositions.value
|
||||
.map((obj: any) => {
|
||||
return {
|
||||
...obj,
|
||||
positionId: formatId(obj.id),
|
||||
amount: bigInt2Number(obj.harbDeposit, 18),
|
||||
taxRatePercentage: Number(obj.taxRate) * 100,
|
||||
taxRate: Number(obj.taxRate),
|
||||
taxRateIndex: taxRates.find((taxRate) => taxRate.year === Number(obj.taxRate) * 100)?.index,
|
||||
iAmOwner: obj.owner?.toLowerCase() === account.address?.toLowerCase(),
|
||||
totalSupplyEnd: obj.totalSupplyEnd ? BigInt(obj.totalSupplyEnd) : undefined,
|
||||
totalSupplyInit: BigInt(obj.totalSupplyInit),
|
||||
taxPaid: BigInt(obj.taxPaid),
|
||||
share: Number(obj.share),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.taxRate > b.taxRate) {
|
||||
return 1;
|
||||
} else if (a.taxRate < b.taxRate) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export interface Position {
|
||||
creationTime: Date;
|
||||
id: string;
|
||||
positionId: bigint;
|
||||
owner: string;
|
||||
lastTaxTime: Date;
|
||||
taxPaid: bigint;
|
||||
taxRate: number;
|
||||
taxRateIndex: number;
|
||||
taxRatePercentage: number;
|
||||
share: number;
|
||||
status: string;
|
||||
totalSupplyEnd?: bigint;
|
||||
totalSupplyInit: bigint;
|
||||
amount: number;
|
||||
harbDeposit: bigint;
|
||||
iAmOwner: boolean;
|
||||
}
|
||||
|
||||
const myClosedPositions: ComputedRef<Position[]> = computed(() => {
|
||||
const account = getAccount(config as any);
|
||||
|
||||
return rawClosedPositoins.value.map((obj: any) => {
|
||||
const taxRatePosition = obj.taxRate * 100;
|
||||
console.log("taxRatePosition", taxRatePosition);
|
||||
|
||||
console.log("taxRates[taxRatePosition]", taxRates[taxRatePosition]);
|
||||
|
||||
return {
|
||||
...obj,
|
||||
positionId: formatId(obj.id),
|
||||
amount: obj.share * 1000000,
|
||||
// amount: bigInt2Number(obj.harbDeposit, 18),
|
||||
taxRatePercentage: Number(obj.taxRate) * 100,
|
||||
taxRate: Number(obj.taxRate),
|
||||
taxRateIndex: taxRates.find((taxRate) => taxRate.year === Number(obj.taxRate) * 100)?.index,
|
||||
iAmOwner: obj.owner?.toLowerCase() === account.address?.toLowerCase(),
|
||||
totalSupplyEnd: BigInt(obj.totalSupplyEnd),
|
||||
totalSupplyInit: BigInt(obj.totalSupplyInit),
|
||||
taxPaid: BigInt(obj.taxPaid),
|
||||
share: Number(obj.share),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const myActivePositions: ComputedRef<Position[]> = computed(() =>
|
||||
activePositions.value.filter((obj: Position) => {
|
||||
return obj.iAmOwner;
|
||||
})
|
||||
);
|
||||
|
||||
const tresholdValue = computed(() => {
|
||||
const arrayTaxRatePositions = activePositions.value.map((obj) => obj.taxRatePercentage);
|
||||
const sortedPositions = arrayTaxRatePositions.sort((a: any, b: any) => (a > b ? 1 : -1));
|
||||
const sumq = sortedPositions.reduce((partialSum, a) => partialSum + a, 0);
|
||||
const avg = sumq / sortedPositions.length;
|
||||
|
||||
return avg / 2;
|
||||
});
|
||||
|
||||
export async function loadActivePositions() {
|
||||
logger.info(`loadActivePositions for chain: ${chainData.value?.path}`);
|
||||
if (!chainData.value?.thegraph) {
|
||||
return [];
|
||||
}
|
||||
console.log("chainData.value?.thegraph", chainData.value?.thegraph);
|
||||
|
||||
const res = await axios.post(chainData.value?.thegraph, {
|
||||
query: `query MyQuery {
|
||||
positions(where: {status: Active}, orderBy: taxRate, orderDirection: asc) {
|
||||
id
|
||||
lastTaxTime
|
||||
owner
|
||||
payout
|
||||
share
|
||||
harbDeposit
|
||||
snatched
|
||||
status
|
||||
taxPaid
|
||||
taxRate
|
||||
totalSupplyEnd
|
||||
totalSupplyInit
|
||||
}
|
||||
}`,
|
||||
});
|
||||
console.log("res", res.data);
|
||||
|
||||
return res.data.data.positions as Position[];
|
||||
}
|
||||
|
||||
function formatId(id: Hex) {
|
||||
const bytes = toBytes(id);
|
||||
const bigIntId = bytesToUint256(bytes);
|
||||
return bigIntId;
|
||||
}
|
||||
|
||||
export async function loadMyClosedPositions(account: GetAccountReturnType) {
|
||||
logger.info(`loadMyClosedPositions for chain: ${chainData.value?.path}`);
|
||||
if (!chainData.value?.thegraph) {
|
||||
return [];
|
||||
}
|
||||
const res = await axios.post(chainData.value?.thegraph, {
|
||||
query: `query MyQuery {
|
||||
positions(where: {status: Closed, owner: "${account.address?.toLowerCase()}"}) {
|
||||
id
|
||||
lastTaxTime
|
||||
owner
|
||||
payout
|
||||
share
|
||||
harbDeposit
|
||||
snatched
|
||||
status
|
||||
taxPaid
|
||||
taxRate
|
||||
totalSupplyEnd
|
||||
totalSupplyInit
|
||||
}
|
||||
}`,
|
||||
});
|
||||
if (res.data.errors?.length > 0) {
|
||||
console.error("todo nur laden, wenn eingeloggt");
|
||||
return [];
|
||||
}
|
||||
const positions: Position[] = res.data.data.positions;
|
||||
return positions;
|
||||
}
|
||||
|
||||
export async function loadPositions() {
|
||||
loading.value = true;
|
||||
|
||||
rawActivePositions.value = await loadActivePositions();
|
||||
//optimal wäre es: laden, wenn new Position meine Position geclosed hat
|
||||
const account = getAccount(config as any);
|
||||
|
||||
if (account.address) {
|
||||
rawClosedPositoins.value = await loadMyClosedPositions(account);
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
let unwatch: WatchEventReturnType;
|
||||
let unwatchPositionRemovedEvent: WatchEventReturnType;
|
||||
let unwatchChainSwitch: WatchChainIdReturnType;
|
||||
let unwatchAccountChanged: WatchAccountReturnType;
|
||||
export function usePositions() {
|
||||
function watchEvent() {
|
||||
unwatch = watchContractEvent(config as any, {
|
||||
address: HarbContract.contractAddress,
|
||||
abi: HarbContract.abi,
|
||||
eventName: "PositionCreated",
|
||||
async onLogs(logs) {
|
||||
console.log("new Position", logs);
|
||||
await loadPositions();
|
||||
// await getMinStake();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function watchPositionRemoved() {
|
||||
unwatchPositionRemovedEvent = watchContractEvent(config as any, {
|
||||
address: HarbContract.contractAddress,
|
||||
abi: HarbContract.abi,
|
||||
eventName: "PositionRemoved",
|
||||
async onLogs(logs) {
|
||||
console.log("Position removed", logs);
|
||||
await loadPositions();
|
||||
// await getMinStake();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
//initial loading positions
|
||||
|
||||
if (activePositions.value.length < 1 && loading.value === false) {
|
||||
loadPositions();
|
||||
// await getMinStake();
|
||||
}
|
||||
|
||||
if (!unwatch) {
|
||||
watchEvent();
|
||||
}
|
||||
if (!unwatchPositionRemovedEvent) {
|
||||
watchPositionRemoved();
|
||||
}
|
||||
|
||||
if (!unwatchChainSwitch) {
|
||||
unwatchChainSwitch = watchChainId(config as any, {
|
||||
async onChange(chainId) {
|
||||
await loadPositions();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!unwatchAccountChanged) {
|
||||
unwatchAccountChanged = watchAccount(config as any, {
|
||||
async onChange() {
|
||||
await loadPositions();
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// if (unwatch) {
|
||||
// unwatch();
|
||||
// }
|
||||
// if (unwatchPositionRemovedEvent) {
|
||||
// unwatchPositionRemovedEvent();
|
||||
// }
|
||||
});
|
||||
|
||||
function createRandomPosition(amount: number = 1) {
|
||||
for (let index = 0; index < amount; index++) {
|
||||
const newPosition: Position = {
|
||||
creationTime: new Date(),
|
||||
id: "123",
|
||||
positionId: 123n,
|
||||
owner: "bla",
|
||||
lastTaxTime: new Date(),
|
||||
taxPaid: 100n,
|
||||
taxRate: randomInRange(0.01, 1),
|
||||
taxRateIndex: randomInRange(1, 30),
|
||||
taxRatePercentage: getRandomInt(1, 100),
|
||||
share: getRandomInt(0.001, 0.09),
|
||||
status: "active",
|
||||
totalSupplyEnd: undefined,
|
||||
totalSupplyInit: 1000000000000n,
|
||||
amount: 150,
|
||||
harbDeposit: getRandomBigInt(1000, 5000),
|
||||
iAmOwner: false,
|
||||
};
|
||||
rawActivePositions.value.push(newPosition);
|
||||
}
|
||||
}
|
||||
|
||||
function randomInRange(min: number, max: number) {
|
||||
return Math.random() < 0.5 ? (1 - Math.random()) * (max - min) + min : Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
function getRandomInt(min: number, max: number) {
|
||||
const minCeiled = Math.ceil(min);
|
||||
const maxFloored = Math.floor(max);
|
||||
const randomNumber = Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled); // The maximum is exclusive and the minimum is inclusive
|
||||
|
||||
return randomNumber;
|
||||
}
|
||||
|
||||
function getRandomBigInt(min: number, max: number) {
|
||||
const randomNumber = getRandomInt(min, max);
|
||||
return BigInt(randomNumber) * 10n ** 18n;
|
||||
}
|
||||
|
||||
return {
|
||||
activePositions,
|
||||
myActivePositions,
|
||||
myClosedPositions,
|
||||
tresholdValue,
|
||||
watchEvent,
|
||||
watchPositionRemoved,
|
||||
createRandomPosition,
|
||||
};
|
||||
}
|
||||
188
web-app/src/composables/useStake.ts
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
import { ref, onMounted, onUnmounted, reactive, computed } from "vue";
|
||||
import { type ComputedRef } from "vue";
|
||||
import { config } from "@/wagmi";
|
||||
import { AbiEncodingArrayLengthMismatchError, type WatchEventReturnType, decodeEventLog, type Hex } from "viem";
|
||||
import axios from "axios";
|
||||
import {
|
||||
getAccount,
|
||||
watchContractEvent,
|
||||
readContract,
|
||||
signTypedData,
|
||||
waitForTransactionReceipt,
|
||||
watchAccount,
|
||||
verifyTypedData,
|
||||
} from "@wagmi/core";
|
||||
import { HarbContract } from "@/contracts/harb";
|
||||
import { type Abi, type Address } from "viem";
|
||||
import { StakeContract, minStake, snatchService, permitAndSnatch, assetsToShares } from "@/contracts/stake";
|
||||
import { getNonce, nonce, getName } from "@/contracts/harb";
|
||||
import { useWallet } from "@/composables/useWallet";
|
||||
import { createPermitObject, getSignatureRSV } from "@/utils/blockchain";
|
||||
import { formatBigIntDivision, compactNumber } from "@/utils/helper";
|
||||
import { useToast } from "vue-toastification";
|
||||
import { taxRates } from "@/composables/useAdjustTaxRates";
|
||||
import { useContractToast } from "./useContractToast";
|
||||
const wallet = useWallet();
|
||||
const toast = useToast();
|
||||
const contractToast = useContractToast();
|
||||
|
||||
enum StakeState {
|
||||
NoBalance = "NoBalance",
|
||||
StakeAble = "StakeAble",
|
||||
SignTransaction = "SignTransaction",
|
||||
Waiting = "Waiting",
|
||||
NotEnoughApproval = "NotEnoughApproval",
|
||||
}
|
||||
|
||||
interface PositionCreatedEvent {
|
||||
eventName: undefined;
|
||||
args: PositionCreatedArgs;
|
||||
}
|
||||
|
||||
interface PositionCreatedArgs {
|
||||
creationTime: number;
|
||||
owner: Hex;
|
||||
harbergDeposit: bigint;
|
||||
positionId: bigint;
|
||||
share: bigint;
|
||||
taxRate: number;
|
||||
}
|
||||
|
||||
// const state = ref<StakeState>(StakeState.NoBalance);
|
||||
|
||||
export function useStake() {
|
||||
const stakingAmountRaw = ref();
|
||||
const stakingAmountShares = ref();
|
||||
const loading = ref(false);
|
||||
const waiting = ref(false);
|
||||
|
||||
onMounted(async () => {});
|
||||
|
||||
const state: ComputedRef<StakeState> = computed(() => {
|
||||
const balance = wallet.balance.value;
|
||||
console.log("balance123", balance);
|
||||
console.log("wallet", wallet);
|
||||
|
||||
|
||||
if (loading.value) {
|
||||
return StakeState.SignTransaction;
|
||||
} else if (minStake.value > balance || stakingAmount.value > balance) {
|
||||
return StakeState.NoBalance;
|
||||
} else if (waiting.value) {
|
||||
return StakeState.Waiting;
|
||||
} else {
|
||||
return StakeState.StakeAble;
|
||||
}
|
||||
});
|
||||
|
||||
const stakingAmount = computed({
|
||||
// getter
|
||||
get() {
|
||||
return stakingAmountRaw.value || minStake.value;
|
||||
},
|
||||
// setter
|
||||
set(newValue) {
|
||||
stakingAmountRaw.value = newValue;
|
||||
},
|
||||
});
|
||||
|
||||
const stakingAmountNumber = computed({
|
||||
// getter
|
||||
get() {
|
||||
return formatBigIntDivision(stakingAmount.value, 10n ** 18n);
|
||||
},
|
||||
// setter
|
||||
set(newValue) {
|
||||
stakingAmount.value = BigInt(newValue * 10 ** 18);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// const stakingAmountNumber = computed(() => return staking)
|
||||
|
||||
async function snatch(stakingAmount: BigInt, taxRate: number, positions:Array<any> = []) {
|
||||
console.log("snatch", { stakingAmount, taxRate, positions });
|
||||
const account = getAccount(config as any);
|
||||
const taxRateObj = taxRates.find((obj) => obj.year === taxRate);
|
||||
|
||||
try {
|
||||
const assets: BigInt = stakingAmount;
|
||||
const receiver = wallet.account.address!;
|
||||
console.log("receiver", receiver);
|
||||
|
||||
// await snatchService(assets, receiver, taxRate, []);
|
||||
// assets: BigInt, receiver: Address, taxRate: Number, positionsToSnatch: Array<BigInt>
|
||||
const deadline = BigInt(Date.now()) / 1000n + 1200n;
|
||||
|
||||
const name = await getName();
|
||||
|
||||
const { types, message, domain, primaryType } = createPermitObject(
|
||||
HarbContract.contractAddress,
|
||||
account.address!,
|
||||
StakeContract.contractAddress,
|
||||
nonce.value,
|
||||
deadline,
|
||||
assets,
|
||||
account.chainId!,
|
||||
name
|
||||
);
|
||||
console.log("resultPermitObject", { types, message, domain, primaryType });
|
||||
|
||||
const signature = await signTypedData(config as any, {
|
||||
domain: domain as any,
|
||||
message: message,
|
||||
primaryType: primaryType,
|
||||
types: types,
|
||||
});
|
||||
|
||||
console.log("signature", {
|
||||
domain: domain as any,
|
||||
message: message,
|
||||
primaryType: primaryType,
|
||||
types: types,
|
||||
});
|
||||
|
||||
const { r, s, v } = getSignatureRSV(signature);
|
||||
loading.value = true;
|
||||
console.log("permitAndSnatch", assets, account.address!, taxRateObj?.index!, positions, deadline, v, r, s);
|
||||
|
||||
const hash = await permitAndSnatch(assets, account.address!, taxRateObj?.index!, positions, deadline, v, r, s);
|
||||
console.log("hash", hash);
|
||||
loading.value = false;
|
||||
waiting.value = true;
|
||||
const data = await waitForTransactionReceipt(config as any, {
|
||||
hash: hash,
|
||||
});
|
||||
|
||||
const topics: any = decodeEventLog({
|
||||
abi: StakeContract.abi,
|
||||
data: data.logs[3].data,
|
||||
topics: data.logs[3].topics,
|
||||
});
|
||||
const eventArgs: PositionCreatedArgs = topics.args;
|
||||
|
||||
const amount = compactNumber(
|
||||
formatBigIntDivision(eventArgs.harbergDeposit, 10n ** BigInt(wallet.balance.decimals))
|
||||
);
|
||||
contractToast.showSuccessToast(
|
||||
amount,
|
||||
"Success!",
|
||||
"You Staked",
|
||||
"Check your positions on the<br /> Staker Dashboard",
|
||||
"$KRK"
|
||||
);
|
||||
|
||||
waiting.value = false;
|
||||
await getNonce();
|
||||
} catch (error: any) {
|
||||
console.error("error", error);
|
||||
console.log(JSON.parse(JSON.stringify(error)));
|
||||
contractToast.showFailToast(error.name);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
waiting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return reactive({ stakingAmount, stakingAmountShares, stakingAmountNumber, state, snatch });
|
||||
}
|
||||
217
web-app/src/composables/useStatCollection.ts
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
import { ref, onMounted, onUnmounted, reactive, computed } from "vue";
|
||||
import axios from "axios";
|
||||
import { chainData } from "./useWallet";
|
||||
import { watchBlocks, watchChainId } from "@wagmi/core";
|
||||
import { config } from "@/wagmi";
|
||||
import logger from "@/utils/logger";
|
||||
import type { WatchBlocksReturnType } from "viem";
|
||||
import { bigInt2Number } from "@/utils/helper";
|
||||
const demo = sessionStorage.getItem("demo") === "true";
|
||||
|
||||
interface statsCollection {
|
||||
burnNextHourProjected: bigint;
|
||||
burnedLastDay: bigint;
|
||||
burnedLastWeek: bigint;
|
||||
id: string;
|
||||
lastUpdatedHour: number;
|
||||
mintNextHourProjected: bigint;
|
||||
mintedLastDay: bigint;
|
||||
mintedLastWeek: bigint;
|
||||
outstandingStake: bigint;
|
||||
harbTotalSupply: bigint;
|
||||
stakeTotalSupply: bigint;
|
||||
ringBufferPointer: number;
|
||||
totalBurned: bigint;
|
||||
totalMinted: bigint;
|
||||
}
|
||||
|
||||
const rawStatsCollections = ref<Array<statsCollection>>([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
|
||||
export async function loadStatsCollection() {
|
||||
logger.info(`loadStatsCollection for chain: ${chainData.value?.path}`);
|
||||
if (!chainData.value?.thegraph) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const res = await axios.post(chainData.value?.thegraph, {
|
||||
query: `query MyQuery {
|
||||
stats_collection {
|
||||
burnNextHourProjected
|
||||
burnedLastDay
|
||||
burnedLastWeek
|
||||
id
|
||||
lastUpdatedHour
|
||||
mintNextHourProjected
|
||||
mintedLastDay
|
||||
mintedLastWeek
|
||||
outstandingStake
|
||||
harbTotalSupply
|
||||
stakeTotalSupply
|
||||
ringBufferPointer
|
||||
totalBurned
|
||||
totalMinted
|
||||
}
|
||||
}`,
|
||||
});
|
||||
console.groupEnd();
|
||||
return res.data?.data?.stats_collection;
|
||||
}
|
||||
|
||||
const profit7d = computed(() => {
|
||||
if (!statsCollection.value) {
|
||||
return 0n;
|
||||
}
|
||||
return (
|
||||
(BigInt(statsCollection.value?.mintedLastWeek) - BigInt(statsCollection.value?.burnedLastWeek)) /
|
||||
(BigInt(statsCollection.value?.totalMinted) - BigInt(statsCollection.value?.totalBurned))
|
||||
);
|
||||
});
|
||||
const nettoToken7d = computed(() => {
|
||||
if (!statsCollection.value) {
|
||||
return 0n;
|
||||
}
|
||||
|
||||
return BigInt(statsCollection.value?.mintedLastWeek) - BigInt(statsCollection.value.burnedLastWeek);
|
||||
});
|
||||
|
||||
const statsCollection = computed(() => {
|
||||
if (rawStatsCollections.value?.length > 0) {
|
||||
return rawStatsCollections.value[0];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const outstandingStake = computed(() => {
|
||||
if (demo) {
|
||||
// outStandingStake = 1990000000000000000000000n;
|
||||
return 2000000000000000000000000n;
|
||||
}
|
||||
if (rawStatsCollections.value?.length > 0) {
|
||||
return BigInt(rawStatsCollections.value[0].outstandingStake);
|
||||
} else {
|
||||
return 0n;
|
||||
}
|
||||
});
|
||||
|
||||
const harbTotalSupply = computed(() => {
|
||||
if (rawStatsCollections.value?.length > 0) {
|
||||
return BigInt(rawStatsCollections.value[0].harbTotalSupply);
|
||||
} else {
|
||||
return 0n;
|
||||
}
|
||||
});
|
||||
|
||||
const stakeTotalSupply = computed(() => {
|
||||
if (rawStatsCollections.value?.length > 0) {
|
||||
return BigInt(rawStatsCollections.value[0].stakeTotalSupply);
|
||||
} else {
|
||||
return 0n;
|
||||
}
|
||||
});
|
||||
|
||||
//Total Supply Change / 7d=mintedLastWeek−burnedLastWeek
|
||||
const totalSupplyChange7d = computed(() => {
|
||||
if (rawStatsCollections.value?.length > 0) {
|
||||
return BigInt(rawStatsCollections.value[0].mintedLastWeek - rawStatsCollections.value[0].burnedLastWeek);
|
||||
} else {
|
||||
return 0n;
|
||||
}
|
||||
});
|
||||
|
||||
//totalsupply Change7d / harbtotalsupply
|
||||
const inflation7d = computed(() => {
|
||||
if (rawStatsCollections.value?.length > 0 && rawStatsCollections.value[0].harbTotalSupply > 0) {
|
||||
return BigInt(rawStatsCollections.value[0].mintedLastWeek - rawStatsCollections.value[0].burnedLastWeek);
|
||||
} else {
|
||||
return 0n;
|
||||
}
|
||||
});
|
||||
|
||||
const stakeableSupply = computed(() => {
|
||||
if (rawStatsCollections.value?.length > 0 && rawStatsCollections.value[0].harbTotalSupply > 0) {
|
||||
console.log("rawStatsCollections.value[0]", rawStatsCollections.value[0]);
|
||||
|
||||
return stakeTotalSupply.value / 5n;
|
||||
} else {
|
||||
return 0n;
|
||||
}
|
||||
});
|
||||
|
||||
//maxSlots
|
||||
const maxSlots = computed(() => {
|
||||
if (rawStatsCollections.value?.length > 0 && rawStatsCollections.value[0].harbTotalSupply > 0) {
|
||||
console.log("rawStatsCollections.value[0]", rawStatsCollections.value[0]);
|
||||
|
||||
return (bigInt2Number(stakeTotalSupply.value, 18) * 0.2) / 100;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
const claimedSlots = computed(() => {
|
||||
if (stakeTotalSupply.value > 0n) {
|
||||
const stakeableSupplyNumber = bigInt2Number(stakeableSupply.value, 18);
|
||||
const outstandingStakeNumber = bigInt2Number(outstandingStake.value, 18);
|
||||
return (outstandingStakeNumber / stakeableSupplyNumber) * maxSlots.value;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
export async function loadStats() {
|
||||
loading.value = true;
|
||||
|
||||
rawStatsCollections.value = await loadStatsCollection();
|
||||
|
||||
loading.value = false;
|
||||
initialized.value = true;
|
||||
}
|
||||
|
||||
let unwatch: any = null;
|
||||
let unwatchBlock: WatchBlocksReturnType;
|
||||
const loadingWatchBlock = ref(false);
|
||||
export function useStatCollection() {
|
||||
onMounted(async () => {
|
||||
//initial loading stats
|
||||
if (rawStatsCollections.value?.length === 0 && !loading.value) {
|
||||
await loadStats();
|
||||
}
|
||||
});
|
||||
if (!unwatch) {
|
||||
console.log("watchChain");
|
||||
|
||||
//chain Switch reload stats for other chain
|
||||
unwatch = watchChainId(config as any, {
|
||||
async onChange(chainId) {
|
||||
await loadStats();
|
||||
},
|
||||
});
|
||||
|
||||
// const unwatchBlock = watchBlocks(config as any, {
|
||||
// async onBlock(block) {
|
||||
// console.log('Block changed!', block)
|
||||
// await loadStats();
|
||||
// },
|
||||
// })
|
||||
}
|
||||
onUnmounted(() => {
|
||||
unwatch();
|
||||
});
|
||||
|
||||
return reactive({
|
||||
profit7d,
|
||||
nettoToken7d,
|
||||
inflation7d,
|
||||
outstandingStake,
|
||||
harbTotalSupply,
|
||||
stakeTotalSupply,
|
||||
totalSupplyChange7d,
|
||||
initialized,
|
||||
maxSlots,
|
||||
stakeableSupply,
|
||||
claimedSlots,
|
||||
});
|
||||
}
|
||||
77
web-app/src/composables/useUnstake.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { ref, onMounted, onUnmounted, reactive, computed, type ComputedRef } from "vue";
|
||||
import * as StakeContract from "@/contracts/stake";
|
||||
import { waitForTransactionReceipt } from "@wagmi/core";
|
||||
import { config } from "@/wagmi";
|
||||
import { useContractToast } from "./useContractToast";
|
||||
import { compactNumber, formatBigIntDivision } from "@/utils/helper";
|
||||
import { decodeEventLog } from "viem";
|
||||
import { useWallet } from "./useWallet";
|
||||
|
||||
const contractToast = useContractToast();
|
||||
const wallet = useWallet();
|
||||
|
||||
enum StakeState {
|
||||
SignTransaction = "SignTransaction",
|
||||
Waiting = "Waiting",
|
||||
Unstakeable = "Unstakeable",
|
||||
}
|
||||
|
||||
interface PositionRemovedArgs {
|
||||
lastTaxTime: number;
|
||||
positionId: bigint;
|
||||
harbergPayout: bigint;
|
||||
}
|
||||
|
||||
export function useUnstake() {
|
||||
const loading = ref();
|
||||
const waiting = ref();
|
||||
|
||||
const state: ComputedRef<StakeState> = computed(() => {
|
||||
if (loading.value) {
|
||||
return StakeState.SignTransaction;
|
||||
} else if (waiting.value) {
|
||||
return StakeState.Waiting;
|
||||
} else {
|
||||
return StakeState.Unstakeable;
|
||||
}
|
||||
});
|
||||
|
||||
async function exitPosition(positionId: bigint) {
|
||||
try {
|
||||
console.log("positionId", positionId);
|
||||
|
||||
loading.value = true;
|
||||
const hash = await StakeContract.exitPosition(positionId);
|
||||
console.log("hash", hash);
|
||||
loading.value = false;
|
||||
waiting.value = true;
|
||||
const data = await waitForTransactionReceipt(config as any, {
|
||||
hash: hash,
|
||||
});
|
||||
|
||||
const topics: any = decodeEventLog({
|
||||
abi: StakeContract.StakeContract.abi,
|
||||
data: data.logs[2].data,
|
||||
topics: data.logs[2].topics,
|
||||
});
|
||||
|
||||
const eventArgs: PositionRemovedArgs = topics.args;
|
||||
|
||||
const amount = compactNumber(
|
||||
formatBigIntDivision(eventArgs.harbergPayout, 10n ** BigInt(wallet.balance.decimals))
|
||||
);
|
||||
contractToast.showSuccessToast(amount, "Success!", "You unstaked", "", "$KRK");
|
||||
|
||||
waiting.value = false;
|
||||
wallet.loadBalance();
|
||||
} catch (error: any) {
|
||||
console.error("error", error);
|
||||
contractToast.showFailToast(error.name);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
waiting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return reactive({ state, exitPosition });
|
||||
}
|
||||
102
web-app/src/composables/useWallet.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import { ref, onMounted, onUnmounted, reactive, computed } from "vue";
|
||||
import { type Ref } from "vue";
|
||||
import { getBalance, watchAccount, watchChainId } from "@wagmi/core";
|
||||
import { type WatchAccountReturnType, type GetAccountReturnType, type GetBalanceReturnType } from "@wagmi/core";
|
||||
import { config } from "@/wagmi";
|
||||
import { getAllowance, HarbContract, getNonce } from "@/contracts/harb";
|
||||
import logger from "@/utils/logger";
|
||||
import { setHarbContract } from "@/contracts/harb";
|
||||
import { setStakeContract } from "@/contracts/stake";
|
||||
import {chainsData} from "@/config"
|
||||
|
||||
const balance = ref<GetBalanceReturnType>({
|
||||
value: 0n,
|
||||
decimals: 0,
|
||||
symbol: "",
|
||||
formatted: ""
|
||||
});
|
||||
const account = ref<GetAccountReturnType>({
|
||||
address: undefined,
|
||||
addresses: undefined,
|
||||
chain: undefined,
|
||||
chainId: undefined,
|
||||
connector: undefined,
|
||||
isConnected: false,
|
||||
isConnecting: false,
|
||||
isDisconnected: true,
|
||||
isReconnecting: false,
|
||||
status: "disconnected",
|
||||
});
|
||||
|
||||
export const chainData = computed(() => {
|
||||
return chainsData.find((obj) => obj.id === account.value.chainId)
|
||||
})
|
||||
|
||||
let unwatch: any = null;
|
||||
let unwatchChain: any = null;
|
||||
export function useWallet() {
|
||||
|
||||
async function loadBalance() {
|
||||
logger.contract("loadBalance")
|
||||
if (account.value.address) {
|
||||
console.log("HarbContract",HarbContract );
|
||||
|
||||
balance.value = await getBalance(config as any, {
|
||||
address: account.value.address,
|
||||
token: HarbContract.contractAddress,
|
||||
});
|
||||
console.log("balance.value", balance.value);
|
||||
|
||||
return balance.value;
|
||||
} else {
|
||||
return 0n;
|
||||
}
|
||||
}
|
||||
|
||||
if (!unwatch) {
|
||||
console.log("useWallet function");
|
||||
|
||||
unwatch = watchAccount(config as any, {
|
||||
async onChange(data) {
|
||||
console.log("watchaccount-useWallet", data);
|
||||
|
||||
if(!data.address) {
|
||||
logger.info(`disconnected`);
|
||||
balance.value = {
|
||||
value: 0n,
|
||||
decimals: 0,
|
||||
symbol: "",
|
||||
formatted: ""
|
||||
}
|
||||
} else if (account.value.address !== data.address || account.value.chainId !== data.chainId) {
|
||||
logger.info(`Account changed!:`, data.address);
|
||||
account.value = data;
|
||||
await loadBalance();
|
||||
await getAllowance();
|
||||
// await loadPositions();
|
||||
await getNonce();
|
||||
setHarbContract()
|
||||
setStakeContract()
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//funzt nicht mehr-> library Änderung?
|
||||
if(!unwatchChain){
|
||||
console.log("unwatchChain");
|
||||
|
||||
unwatchChain = watchChainId(config as any, {
|
||||
async onChange(chainId) {
|
||||
console.log("chainId123", chainId);
|
||||
|
||||
await loadBalance();
|
||||
await getAllowance();
|
||||
await getNonce();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return reactive({ balance, account, loadBalance });
|
||||
}
|
||||
32
web-app/src/config.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
export const chainsData = [
|
||||
{
|
||||
//sepolia
|
||||
id: 11155111,
|
||||
thegraph: "https://api.studio.thegraph.com/query/71271/harb/v0.0.50",
|
||||
path: "sepolia",
|
||||
stake: "0xCd21a41a137BCAf8743E47D048F57D92398f7Da9",
|
||||
harb: "0x087F256D11fe533b0c7d372e44Ee0F9e47C89dF9",
|
||||
uniswap: "https://app.uniswap.org/swap?chain=mainnet&inputCurrency=NATIVE"
|
||||
}, {
|
||||
//base-sepolia
|
||||
id: 84532,
|
||||
thegraph: "https://api.studio.thegraph.com/query/71271/harb-base-sepolia/v0.0.12",
|
||||
path: "sepoliabase",
|
||||
stake: "0xe28020BCdEeAf2779dd47c670A8eFC2973316EE2",
|
||||
harb: "0x22c264Ecf8D4E49D1E3CabD8DD39b7C4Ab51C1B8",
|
||||
uniswap: "https://app.uniswap.org/swap?chain=mainnet&inputCurrency=NATIVE"
|
||||
},
|
||||
{
|
||||
//base
|
||||
id: 8453,
|
||||
thegraph: import.meta.env.VITE_BASE_URL,
|
||||
path: "base",
|
||||
stake: "0xed70707fab05d973ad41eae8d17e2bcd36192cfc",
|
||||
harb: "0x45caa5929f6ee038039984205bdecf968b954820",
|
||||
uniswap: "https://app.uniswap.org/swap?chain=mainnet&inputCurrency=NATIVE"
|
||||
},
|
||||
]
|
||||
|
||||
export function getChain(id:number){
|
||||
return chainsData.find((obj) => obj.id === id)
|
||||
}
|
||||
171
web-app/src/contracts/harb.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import { ref, onMounted, onUnmounted, reactive, computed, watch } from "vue";
|
||||
import { config } from "@/wagmi";
|
||||
import { type WatchEventReturnType } from "viem";
|
||||
import {
|
||||
getAccount,
|
||||
watchContractEvent,
|
||||
readContract,
|
||||
writeContract,
|
||||
waitForTransactionReceipt,
|
||||
type WaitForTransactionReceiptParameters,
|
||||
getChainId
|
||||
} from "@wagmi/core";
|
||||
// import { HarbContract } from "@/contracts/harb";
|
||||
import HarbJson from "@/assets/contracts/harb.json";
|
||||
import { type Abi, type Address } from "viem";
|
||||
import { StakeContract } from "@/contracts/stake";
|
||||
import {getChain} from "@/config"
|
||||
import logger from "@/utils/logger";
|
||||
// const chain1 = useChain();
|
||||
// console.log("chain1", chain1);
|
||||
|
||||
|
||||
interface Contract {
|
||||
abi: Abi;
|
||||
contractAddress: Address;
|
||||
}
|
||||
|
||||
export const allowance = ref();
|
||||
export const nonce = ref();
|
||||
export const name = ref();
|
||||
export const ubiDue = ref();
|
||||
export const totalSupply = ref(0n);
|
||||
|
||||
// export const HarbContract = await getHarbJson()
|
||||
export let HarbContract = getHarbJson();
|
||||
|
||||
function getHarbJson(){
|
||||
console.log("getHarbJson");
|
||||
|
||||
const chainId = getChainId(config as any);
|
||||
console.log("chainId", chainId);
|
||||
|
||||
const chain = getChain(chainId)
|
||||
|
||||
return {abi: HarbJson, contractAddress: chain?.harb} as Contract;
|
||||
}
|
||||
|
||||
export function setHarbContract(){
|
||||
console.log("setHarbContract");
|
||||
|
||||
HarbContract = getHarbJson();
|
||||
}
|
||||
|
||||
// watch(chainData, async (newQuestion, oldQuestion) => {
|
||||
// console.log("log harb update");
|
||||
|
||||
// });
|
||||
|
||||
|
||||
export async function getAllowance() {
|
||||
logger.contract("getAllowance");
|
||||
|
||||
const account = getAccount(config as any);
|
||||
if (!account.address) {
|
||||
return 0n;
|
||||
}
|
||||
const result = await readContract(config as any, {
|
||||
abi: HarbContract.abi,
|
||||
address: HarbContract.contractAddress,
|
||||
functionName: "allowance",
|
||||
args: [account.address, StakeContract.contractAddress],
|
||||
});
|
||||
allowance.value = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getMinStake() {
|
||||
logger.contract("getMinStake");
|
||||
|
||||
const result: bigint = await readContract(config as any, {
|
||||
abi: HarbContract.abi,
|
||||
address: HarbContract.contractAddress,
|
||||
functionName: "minStake",
|
||||
args: [],
|
||||
}) as bigint;
|
||||
allowance.value = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getNonce() {
|
||||
logger.contract("getNonce");
|
||||
|
||||
const account = getAccount(config as any);
|
||||
if (!account.address) {
|
||||
return 0n;
|
||||
}
|
||||
console.log("HarbContract.contractAddress", HarbContract.contractAddress);
|
||||
|
||||
const result = await readContract(config as any, {
|
||||
abi: HarbContract.abi,
|
||||
address: HarbContract.contractAddress,
|
||||
functionName: "nonces",
|
||||
args: [account.address],
|
||||
});
|
||||
nonce.value = result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getName() {
|
||||
logger.contract("getName");
|
||||
|
||||
|
||||
const result = await readContract(config as any, {
|
||||
abi: HarbContract.abi,
|
||||
address: HarbContract.contractAddress,
|
||||
functionName: "name",
|
||||
args: [],
|
||||
});
|
||||
name.value = result;
|
||||
|
||||
return result as string;
|
||||
}
|
||||
|
||||
|
||||
export async function approve(amount: bigint): Promise<any> {
|
||||
const account = getAccount(config as any);
|
||||
if (!account.address) {
|
||||
throw new Error("no address found");
|
||||
}
|
||||
const result = await writeContract(config as any, {
|
||||
abi: HarbContract.abi,
|
||||
address: HarbContract.contractAddress,
|
||||
functionName: "approve",
|
||||
args: [StakeContract.contractAddress, amount],
|
||||
});
|
||||
console.log("result", result);
|
||||
const transactionReceipt = waitForTransactionReceipt(config as any, {
|
||||
hash: result,
|
||||
});
|
||||
console.log("transactionReceipt", transactionReceipt);
|
||||
|
||||
return transactionReceipt;
|
||||
}
|
||||
|
||||
//claim
|
||||
export async function claimUbi(address: Address): Promise<any> {
|
||||
const result = await writeContract(config as any, {
|
||||
abi: HarbContract.abi,
|
||||
address: HarbContract.contractAddress,
|
||||
functionName: "claimUbi",
|
||||
args: [address],
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
export async function getTotalSupply() {
|
||||
logger.contract("getTotalSupply");
|
||||
|
||||
|
||||
const result = await readContract(config as any, {
|
||||
abi: HarbContract.abi,
|
||||
address: HarbContract.contractAddress,
|
||||
functionName: "totalSupply",
|
||||
args: [],
|
||||
});
|
||||
totalSupply.value = result as bigint;
|
||||
return result;
|
||||
}
|
||||
164
web-app/src/contracts/stake.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import { ref } from "vue";
|
||||
import { config } from "@/wagmi";
|
||||
import { readContract, writeContract, getChainId } from "@wagmi/core";
|
||||
import StakeJson from "@/assets/contracts/stake.json";
|
||||
import { type Abi, type Address } from "viem";
|
||||
import {getChain} from "@/config"
|
||||
import logger from "@/utils/logger";
|
||||
|
||||
const TAX_FLOOR_DURATION = 60 * 60 * 24 * 3;
|
||||
interface Contract {
|
||||
abi: Abi;
|
||||
contractAddress: Address;
|
||||
}
|
||||
|
||||
|
||||
export const minStake = ref();
|
||||
export const totalSupply = ref(0n);
|
||||
export const outstandingSupply = ref(0n)
|
||||
|
||||
|
||||
export let StakeContract = getStakeJson()
|
||||
|
||||
function getStakeJson(){
|
||||
const chainId = getChainId(config as any);
|
||||
console.log("chainId", chainId);
|
||||
|
||||
const chain = getChain(chainId)
|
||||
|
||||
return {abi: StakeJson, contractAddress: chain?.stake} as Contract;
|
||||
}
|
||||
|
||||
export function setStakeContract(){
|
||||
logger.contract("setStakeContract")
|
||||
StakeContract = getStakeJson();
|
||||
}
|
||||
|
||||
|
||||
export async function snatchService(
|
||||
assets: BigInt,
|
||||
receiver: Address,
|
||||
taxRate: Number,
|
||||
positionsToSnatch: Array<BigInt>
|
||||
) {
|
||||
console.log("StakeContract", StakeContract);
|
||||
|
||||
const result = await writeContract(config as any, {
|
||||
abi: StakeContract.abi,
|
||||
address: StakeContract.contractAddress,
|
||||
functionName: "snatch",
|
||||
args: [assets, receiver, taxRate, positionsToSnatch],
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function exitPosition(positionId: bigint): Promise<any> {
|
||||
const result = await writeContract(config as any, {
|
||||
abi: StakeContract.abi,
|
||||
address: StakeContract.contractAddress,
|
||||
functionName: "exitPosition",
|
||||
args: [positionId],
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//changeTax
|
||||
export async function changeTax(positionId: bigint, taxRate: number): Promise<any> {
|
||||
const result = await writeContract(config as any, {
|
||||
abi: StakeContract.abi,
|
||||
address: StakeContract.contractAddress,
|
||||
functionName: "changeTax",
|
||||
args: [positionId, taxRate],
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* snatch/stake with permit
|
||||
*/
|
||||
export async function permitAndSnatch(
|
||||
assets: BigInt,
|
||||
receiver: Address,
|
||||
taxRate: number,
|
||||
positionsToSnatch: Array<BigInt>,
|
||||
deadline: BigInt,
|
||||
v: any,
|
||||
r: any,
|
||||
s: any
|
||||
) {
|
||||
console.log("permitAndSnatch", assets, receiver, taxRate, positionsToSnatch, deadline, v, r, s);
|
||||
|
||||
const result = await writeContract(config as any, {
|
||||
abi: StakeContract.abi,
|
||||
address: StakeContract.contractAddress,
|
||||
functionName: "permitAndSnatch",
|
||||
args: [assets, receiver, taxRate, positionsToSnatch, deadline, v, r, s],
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getTotalSupply() {
|
||||
logger.contract("getTotalSupply")
|
||||
await setStakeContract();
|
||||
const result = await readContract(config as any, {
|
||||
abi: StakeContract.abi,
|
||||
address: StakeContract.contractAddress,
|
||||
functionName: "totalSupply",
|
||||
args: [],
|
||||
});
|
||||
console.log("result", result);
|
||||
|
||||
totalSupply.value = result as bigint;
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getOutstandingSupply() {
|
||||
logger.contract("getOutstandingSupply")
|
||||
|
||||
const result = await readContract(config as any, {
|
||||
abi: StakeContract.abi,
|
||||
address: StakeContract.contractAddress,
|
||||
functionName: "outstandingStake",
|
||||
args: [],
|
||||
});
|
||||
|
||||
outstandingSupply.value = result as bigint;
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getTaxDue(positionID: bigint) {
|
||||
logger.contract("getTaxDue")
|
||||
const result = await readContract(config as any, {
|
||||
abi: StakeContract.abi,
|
||||
address: StakeContract.contractAddress,
|
||||
functionName: "taxDue",
|
||||
args: [positionID, TAX_FLOOR_DURATION],
|
||||
});
|
||||
return result as bigint;
|
||||
}
|
||||
|
||||
export async function payTax(positionID: bigint) {
|
||||
console.log("payTax", positionID);
|
||||
|
||||
const result = await writeContract(config as any, {
|
||||
abi: StakeContract.abi,
|
||||
address: StakeContract.contractAddress,
|
||||
functionName: "payTax",
|
||||
args: [positionID],
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function assetsToShares(asset: bigint) {
|
||||
console.log("assetsToShares", asset);
|
||||
|
||||
const result = await readContract(config as any, {
|
||||
abi: StakeContract.abi,
|
||||
address: StakeContract.contractAddress,
|
||||
functionName: "assetsToShares",
|
||||
args: [asset],
|
||||
});
|
||||
return result as bigint;
|
||||
}
|
||||
17
web-app/src/directives/ClickOutsideDirective.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
export default {
|
||||
beforeMount(el: any, binding: any) {
|
||||
el.clickOutsideEvent = function (event: any) {
|
||||
// Check if the clicked element is neither the element
|
||||
// to which the directive is applied nor its child
|
||||
if (!(el === event.target || el.contains(event.target))) {
|
||||
// Invoke the provided method
|
||||
binding.value(event);
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", el.clickOutsideEvent);
|
||||
},
|
||||
unmounted(el: any) {
|
||||
// Remove the event listener when the bound element is unmounted
|
||||
document.removeEventListener("click", el.clickOutsideEvent);
|
||||
},
|
||||
};
|
||||
5
web-app/src/layouts/DefaultLayout.vue
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<main>
|
||||
<slot></slot>
|
||||
</main>
|
||||
</template>
|
||||
88
web-app/src/layouts/NavbarLayout.vue
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<!-- src/layouts/NavbarLayout.vue -->
|
||||
<template>
|
||||
<navbar></navbar>
|
||||
<slideout-panel v-model="showPanel">
|
||||
<connect-wallet></connect-wallet>
|
||||
<!-- <f-footer :dark="true"></f-footer> -->
|
||||
</slideout-panel>
|
||||
<div class="navbar-layout">
|
||||
<main>
|
||||
<slot></slot>
|
||||
</main>
|
||||
</div>
|
||||
<!-- <footer>
|
||||
<f-footer :dark="darkTheme"></f-footer>
|
||||
</footer> -->
|
||||
<div class="mobile-navigation-bar" v-if="isMobile">
|
||||
<div class="mobile-navigation-tab" @click="router.push('/')">
|
||||
<div class="mobile-navigation-tab__icon">
|
||||
<icon-home />
|
||||
</div>
|
||||
<div class="mobile-navigation-tab__title">Stake</div>
|
||||
</div>
|
||||
<div class="mobile-navigation-tab" @click="openDocs">
|
||||
<div class="mobile-navigation-tab__icon">
|
||||
<icon-docs />
|
||||
</div>
|
||||
<div class="mobile-navigation-tab__title">Docs</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Navbar from "@/components/layouts/Navbar.vue";
|
||||
import SlideoutPanel from "@/components/layouts/SlideoutPanel.vue";
|
||||
import ConnectWallet from "@/components/layouts/ConnectWallet.vue";
|
||||
import ThemeToggle from "@/components/layouts/ThemeToggle.vue";
|
||||
import FFooter from "@/components/layouts/FFooter.vue";
|
||||
import { useMobile } from "@/composables/useMobile";
|
||||
import { useDark } from "@/composables/useDark";
|
||||
import IconHome from "@/components/icons/IconHome.vue";
|
||||
import IconDocs from "@/components/icons/IconDocs.vue";
|
||||
import { ref, provide } from "vue";
|
||||
import {useRouter } from "vue-router"
|
||||
const showPanel = ref(false);
|
||||
const { darkTheme } = useDark();
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const isMobile = useMobile();
|
||||
provide("isMobile", isMobile);
|
||||
provide("showPanel", showPanel);
|
||||
|
||||
function openDocs(){
|
||||
window.open("https://emberspirit007.github.io/KraikenLanding/#/docs/Introduction")
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.navbar-layout
|
||||
padding-top: 100px
|
||||
|
||||
.mobile-navigation-bar
|
||||
height: 72px
|
||||
z-index: 1
|
||||
width: 100%
|
||||
padding: 16px 48px
|
||||
box-shadow: 0px -4px 6px 0px rgba(0, 0, 0, 0.25)
|
||||
background: var(--midnight-black, #0F0F0F)
|
||||
bottom: -1px
|
||||
position: fixed
|
||||
display: flex
|
||||
align-items: center
|
||||
.mobile-navigation-tab
|
||||
font-size: 40px
|
||||
cursor: pointer
|
||||
flex: 1 1 50%
|
||||
text-align: center
|
||||
color: white
|
||||
-webkit-tap-highlight-color: transparent
|
||||
.mobile-navigation-tab__icon
|
||||
svg
|
||||
width: 20px
|
||||
height: 20px
|
||||
path
|
||||
fill: white
|
||||
.mobile-navigation-tab__title
|
||||
font-size: 12px
|
||||
</style>
|
||||
25
web-app/src/main.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { WagmiPlugin } from "@wagmi/vue";
|
||||
import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query";
|
||||
import { createApp } from "vue";
|
||||
import { config } from "./wagmi";
|
||||
import ClickOutSide from "@/directives/ClickOutsideDirective";
|
||||
import router from "./router";
|
||||
|
||||
import App from "./App.vue";
|
||||
import "./assets/styles/main.sass";
|
||||
import Toast from "vue-toastification";
|
||||
import "vue-toastification/dist/index.css";
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.directive("click-outside", ClickOutSide);
|
||||
|
||||
app.use(WagmiPlugin, { config });
|
||||
app.use(VueQueryPlugin, { queryClient });
|
||||
app.use(router);
|
||||
app.use(Toast, {
|
||||
transition: "Vue-Toastification__fade",
|
||||
containerClassName: "harb-toast-container",
|
||||
});
|
||||
app.mount("#app");
|
||||
12
web-app/src/router/authGuard.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// src/router/authGuard.ts
|
||||
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router';
|
||||
|
||||
export function authGuard(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext): void {
|
||||
const isAuthenticated: boolean = !!localStorage.getItem('authentificated'); // Prüft, ob Token existiert
|
||||
|
||||
if (isAuthenticated) {
|
||||
next(); // Zugriff erlauben
|
||||
} else {
|
||||
next('/login'); // Weiterleitung zur Login-Seite
|
||||
}
|
||||
}
|
||||
35
web-app/src/router/index.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import HomeView from "../views/HomeView.vue";
|
||||
import { authGuard } from './authGuard';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
redirect: "/stake",
|
||||
},
|
||||
{
|
||||
path: "/stake",
|
||||
name: "stake",
|
||||
meta: {
|
||||
title: "Stake",
|
||||
group: "navbar",
|
||||
layout: 'NavbarLayout'
|
||||
},
|
||||
beforeEnter: authGuard,
|
||||
component: () => import("../views/StakeView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
name: "login",
|
||||
meta: {
|
||||
layout: 'DefaultLayout'
|
||||
, },
|
||||
component: () => import("../views/LoginView.vue"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default router;
|
||||
12
web-app/src/types/router.d.ts
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// src/types/router.d.ts
|
||||
import 'vue-router';
|
||||
|
||||
// Verfügbare Layout-Namen definieren
|
||||
export type LayoutName = 'DefaultLayout' | 'NavbarLayout';
|
||||
|
||||
// Erweiterung des Vue-Router Meta-Typs
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
layout?: LayoutName; // Layout ist optional
|
||||
}
|
||||
}
|
||||
85
web-app/src/utils/blockchain.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { type Address, type TypedDataDomain, type Hex, slice, hexToNumber, hexToBigInt, recoverAddress } from "viem";
|
||||
|
||||
|
||||
export function createPermitObject(
|
||||
verifyingContract: Address,
|
||||
fromAddress: Address,
|
||||
spender: Address,
|
||||
nonce: BigInt,
|
||||
deadline: BigInt,
|
||||
value: BigInt,
|
||||
chain: number,
|
||||
domainName: string
|
||||
) {
|
||||
const message = {
|
||||
owner: fromAddress,
|
||||
spender: spender,
|
||||
nonce: nonce,
|
||||
deadline: deadline,
|
||||
value: value,
|
||||
};
|
||||
|
||||
|
||||
const domainType = [
|
||||
{ name: "name", type: "string" },
|
||||
{ name: "version", type: "string" },
|
||||
{ name: "chainId", type: "uint256" },
|
||||
{ name: "verifyingContract", type: "address" },
|
||||
];
|
||||
|
||||
const primaryType: "EIP712Domain" | "Permit" = "Permit";
|
||||
|
||||
const types = {
|
||||
EIP712Domain: domainType,
|
||||
Permit: [
|
||||
{
|
||||
name: "owner",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
name: "spender",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
name: "value",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
name: "nonce",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
name: "deadline",
|
||||
type: "uint256",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const domain: TypedDataDomain | undefined = {
|
||||
name: domainName,
|
||||
version: "1",
|
||||
chainId: chain,
|
||||
verifyingContract: verifyingContract,
|
||||
};
|
||||
|
||||
return {
|
||||
types,
|
||||
message,
|
||||
domain,
|
||||
primaryType,
|
||||
};
|
||||
}
|
||||
|
||||
export function getSignatureRSV2(sig: `0x${string}`) {
|
||||
// splits the signature to r, s, and v values.
|
||||
// const pureSig = sig.replace("0x", "");
|
||||
const [r, s, v] = [slice(sig, 0, 32), slice(sig, 32, 64), slice(sig, 64, 65)];
|
||||
return { r, s, v: Number(v) };
|
||||
}
|
||||
|
||||
export function getSignatureRSV(signature: `0x${string}`) {
|
||||
const r = signature.slice(0, 66) as `0x${string}`;
|
||||
const s = `0x${signature.slice(66, 130)}` as `0x${string}`;
|
||||
const v = hexToBigInt(`0x${signature.slice(130, 132)}`);
|
||||
return { r, s, v };
|
||||
}
|
||||
135
web-app/src/utils/blockies.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
var randseed = new Array(4); // Xorshift: [x, y, z, w] 32 bit values
|
||||
|
||||
interface BlockiesOpt {
|
||||
seed: string;
|
||||
size: number;
|
||||
scale: number;
|
||||
color?: string;
|
||||
bgcolor?: string;
|
||||
spotcolor?: string;
|
||||
}
|
||||
|
||||
export function getBlocky(address: string) {
|
||||
if (!address || typeof address !== "string" ) {
|
||||
return;
|
||||
}
|
||||
console.log("address", address);
|
||||
|
||||
var blockiesData = createIcon({
|
||||
seed: address?.toLowerCase(),
|
||||
size: 8,
|
||||
scale: 4,
|
||||
}).toDataURL();
|
||||
|
||||
return blockiesData;
|
||||
}
|
||||
|
||||
function seedrand(seed: string) {
|
||||
for (var i = 0; i < randseed.length; i++) {
|
||||
randseed[i] = 0;
|
||||
}
|
||||
for (var i = 0; i < seed.length; i++) {
|
||||
randseed[i % 4] = (randseed[i % 4] << 5) - randseed[i % 4] + seed.charCodeAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
function rand() {
|
||||
// based on Java's String.hashCode(), expanded to 4 32bit values
|
||||
var t = randseed[0] ^ (randseed[0] << 11);
|
||||
|
||||
randseed[0] = randseed[1];
|
||||
randseed[1] = randseed[2];
|
||||
randseed[2] = randseed[3];
|
||||
randseed[3] = randseed[3] ^ (randseed[3] >> 19) ^ t ^ (t >> 8);
|
||||
|
||||
return (randseed[3] >>> 0) / ((1 << 31) >>> 0);
|
||||
}
|
||||
|
||||
function createColor() {
|
||||
//saturation is the whole color spectrum
|
||||
var h = Math.floor(rand() * 360);
|
||||
//saturation goes from 40 to 100, it avoids greyish colors
|
||||
var s = rand() * 60 + 40 + "%";
|
||||
//lightness can be anything from 0 to 100, but probabilities are a bell curve around 50%
|
||||
var l = (rand() + rand() + rand() + rand()) * 25 + "%";
|
||||
|
||||
var color = "hsl(" + h + "," + s + "," + l + ")";
|
||||
return color;
|
||||
}
|
||||
|
||||
function createImageData(size: number) {
|
||||
var width = size; // Only support square icons for now
|
||||
var height = size;
|
||||
|
||||
var dataWidth = Math.ceil(width / 2);
|
||||
var mirrorWidth = width - dataWidth;
|
||||
|
||||
var data = [];
|
||||
for (var y = 0; y < height; y++) {
|
||||
var row = [];
|
||||
for (var x = 0; x < dataWidth; x++) {
|
||||
// this makes foreground and background color to have a 43% (1/2.3) probability
|
||||
// spot color has 13% chance
|
||||
row[x] = Math.floor(rand() * 2.3);
|
||||
}
|
||||
var r = row.slice(0, mirrorWidth);
|
||||
r.reverse();
|
||||
row = row.concat(r);
|
||||
|
||||
for (var i = 0; i < row.length; i++) {
|
||||
data.push(row[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function buildOpts(opts: any) {
|
||||
var newOpts: any = {};
|
||||
newOpts.seed = opts.seed || Math.floor(Math.random() * Math.pow(10, 16)).toString(16);
|
||||
|
||||
seedrand(newOpts.seed);
|
||||
|
||||
newOpts.size = opts.size || 8;
|
||||
newOpts.scale = opts.scale || 4;
|
||||
newOpts.color = opts.color || createColor();
|
||||
newOpts.bgcolor = opts.bgcolor || createColor();
|
||||
newOpts.spotcolor = opts.spotcolor || createColor();
|
||||
|
||||
return newOpts;
|
||||
}
|
||||
|
||||
function renderIcon(opts: BlockiesOpt, canvas: HTMLCanvasElement) {
|
||||
opts = buildOpts(opts || {});
|
||||
var imageData = createImageData(opts.size);
|
||||
var width = Math.sqrt(imageData.length);
|
||||
|
||||
canvas.width = canvas.height = opts.size * opts.scale;
|
||||
|
||||
var cc = canvas.getContext("2d")!;
|
||||
cc.fillStyle = opts.bgcolor!;
|
||||
cc.fillRect(0, 0, canvas.width, canvas.height);
|
||||
cc.fillStyle = opts.color!;
|
||||
|
||||
for (var i = 0; i < imageData.length; i++) {
|
||||
// if data is 0, leave the background
|
||||
if (imageData[i]) {
|
||||
var row = Math.floor(i / width);
|
||||
var col = i % width;
|
||||
|
||||
// if data is 2, choose spot color, if 1 choose foreground
|
||||
cc.fillStyle = imageData[i] == 1 ? opts.color! : opts.spotcolor!;
|
||||
|
||||
cc.fillRect(col * opts.scale, row * opts.scale, opts.scale, opts.scale);
|
||||
}
|
||||
}
|
||||
return canvas;
|
||||
}
|
||||
|
||||
export function createIcon(opts: BlockiesOpt) {
|
||||
var canvas = document.createElement("canvas");
|
||||
|
||||
renderIcon(opts, canvas);
|
||||
|
||||
return canvas;
|
||||
}
|
||||
21
web-app/src/utils/converter.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* @notice Converts Harberg token assets to shares of the total staking pool.
|
||||
* @param assets Number of Harberg tokens to convert.
|
||||
* @param totalSupply Total supply of shares.
|
||||
* @param harbergTotalSupply Total supply of Harberg tokens.
|
||||
* @returns Number of shares corresponding to the input assets.
|
||||
*/
|
||||
export function assetsToShares(assets: bigint, totalSupply: bigint, harbergTotalSupply: bigint): bigint {
|
||||
return (assets * totalSupply) / harbergTotalSupply;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Converts shares of the total staking pool back to Harberg token assets.
|
||||
* @param shares Number of shares to convert.
|
||||
* @param totalSupply Total supply of shares.
|
||||
* @param harbergTotalSupply Total supply of Harberg tokens.
|
||||
* @returns The equivalent number of Harberg tokens for the given shares.
|
||||
*/
|
||||
export function sharesToAssets(shares: bigint, totalSupply: bigint, harbergTotalSupply: bigint): bigint {
|
||||
return (shares * harbergTotalSupply) / totalSupply;
|
||||
}
|
||||
64
web-app/src/utils/helper.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { formatUnits } from 'viem'
|
||||
|
||||
|
||||
export function getAddressShortName(address: string) {
|
||||
if (!address) {
|
||||
return "";
|
||||
}
|
||||
const addressBegin = address.substring(0, 6);
|
||||
const addressEnd = address.substring(address.length - 4, address.length);
|
||||
return addressBegin + "..." + addressEnd;
|
||||
}
|
||||
|
||||
export function compactNumber(number: number) {
|
||||
return Intl.NumberFormat("en-US", {
|
||||
notation: "compact",
|
||||
// minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(number);
|
||||
}
|
||||
|
||||
export function formatBigNumber(number: bigint, decimals: number, digits: number = 5) {
|
||||
let bigIntNumber = number;
|
||||
if(!bigIntNumber){
|
||||
bigIntNumber = BigInt(0)
|
||||
}
|
||||
const formattedNumber = Number(formatUnits(bigIntNumber, decimals))
|
||||
if(formattedNumber === 0){
|
||||
return "0"
|
||||
}
|
||||
return formattedNumber.toFixed(digits)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function bigInt2Number(number: bigint, decimals: number) {
|
||||
let bigIntNumber = number;
|
||||
if(!bigIntNumber){
|
||||
bigIntNumber = BigInt(0)
|
||||
}
|
||||
const formattedNumber = Number(formatUnits(bigIntNumber, decimals))
|
||||
return formattedNumber
|
||||
}
|
||||
|
||||
export function InsertCommaNumber(number: any) {
|
||||
if (!number) {
|
||||
return 0;
|
||||
}
|
||||
const formattedWithOptions = number.toLocaleString("en-US");
|
||||
return formattedWithOptions;
|
||||
}
|
||||
|
||||
export function formatBigIntDivision(nominator: bigint, denominator: bigint, digits: number = 2) {
|
||||
if (!nominator) {
|
||||
return 0;
|
||||
}
|
||||
let display = nominator.toString();
|
||||
const decimal = (Number(denominator) / 10).toString().length;
|
||||
|
||||
let [integer, fraction] = [display.slice(0, display.length - decimal), display.slice(display.length - decimal)];
|
||||
|
||||
// output type number
|
||||
return Number(integer + "." + fraction);
|
||||
}
|
||||
|
||||
23
web-app/src/utils/logger.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
export function info(text: string, data: any = null ){
|
||||
if(data){
|
||||
console.log(`%c ${text}`, 'color: #17a2b8', data);
|
||||
|
||||
} else{
|
||||
console.log(`%c ${text}`, 'color: #17a2b8');
|
||||
}
|
||||
}
|
||||
|
||||
export function contract(text: string, data: any = null ){
|
||||
if(data){
|
||||
console.log(`%c ${text}`, 'color: #8732a8', data);
|
||||
|
||||
} else{
|
||||
console.log(`%c ${text}`, 'color: #8732a8');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
info,
|
||||
contract
|
||||
}
|
||||
15
web-app/src/views/AboutView.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@media (min-width: 1024px) {
|
||||
.about {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||