Explorer V2 (#5548)

* remove pnpm lock file (should only be using yarn)

* Add lefthook configuration for pre-commit checks

* Add explorer-v2 to package.json dependencies

* add explorer v2

* update explorer v2 package name

* + basepath
+ redirect to basepath
+ blog icons refactor
+ icons refactor

* Add Getting Started instructions to README

* fix noise graph bug and line graph UI

* Delete unused translations, clean up console logs

* / test image url

* update yarn.lock

---------

Co-authored-by: RadekSabacky <radek@nymtech.net>
Co-authored-by: windy-ux <75579979+windy-ux@users.noreply.github.com>
Co-authored-by: Yana <iana.matrosova@gmail.com>
Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
This commit is contained in:
Fouad
2025-03-13 11:31:59 +00:00
committed by GitHub
parent 79ce611d21
commit dc88650d6d
219 changed files with 62763 additions and 5362 deletions
+10
View File
@@ -67,3 +67,13 @@ As a general approach, licensing is as follows this pattern:
- documentation is Apache 2.0 or CC0-1.0
Nym Node Operators and Validators Terms and Conditions can be found [here](https://nym.com/operators-validators-terms).
## Getting Started
```bash
yarn install
```
```bash
yarn build
```
+3
View File
@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}
+40
View File
@@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
+36
View File
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+33
View File
@@ -0,0 +1,33 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": ["node_modules", ".next", "public"]
},
"formatter": {
"enabled": true,
"indentStyle": "space"
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "warn"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
}
}
+23
View File
@@ -0,0 +1,23 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
basePath: "/explorer",
assetPrefix: "/explorer",
trailingSlash: false,
async redirects() {
return [
// Change the basePath to /explorer
{
source: "/",
destination: "/explorer",
basePath: false,
permanent: true,
},
]
}
};
export default nextConfig;
+64
View File
@@ -0,0 +1,64 @@
{
"name": "@nymproject/explorer-v2",
"version": "0.1.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"build:prod": "yarn --cwd .. build && next build",
"start": "next start",
"lint": "biome check --fix"
},
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@chain-registry/types": "^0.50.36",
"@cosmos-kit/keplr-extension": "^2.14.0",
"@cosmos-kit/react": "^2.20.1",
"@emotion/cache": "^11.13.5",
"@emotion/react": "^11.13.5",
"@emotion/styled": "^11.13.5",
"@interchain-ui/react": "^1.26.1",
"@mui/icons-material": "^5.16.11",
"@mui/material": "^6.1.10",
"@mui/material-nextjs": "^6.1.9",
"@mui/x-date-pickers": "^7.23.2",
"@nivo/line": "^0.88.0",
"@nymproject/contract-clients": "^1.4.1",
"@nymproject/react": "1.0.0",
"@tanstack/react-query": "^5.64.2",
"@tanstack/react-query-devtools": "^5.64.2",
"@tanstack/react-query-next-experimental": "^5.66.0",
"@tanstack/react-table": "^8.20.6",
"@types/qs": "^6.9.18",
"@uidotdev/usehooks": "^2.4.1",
"chain-registry": "^1.69.64",
"cldr-compact-number": "^0.4.0",
"date-fns": "^4.1.0",
"i18next": "^24.2.2",
"i18next-resources-to-backend": "^1.2.1",
"isomorphic-dompurify": "^2.21.0",
"material-react-table": "^3.0.3",
"next": "^15.2.0",
"openapi-fetch": "^0.13.4",
"qrcode.react": "^4.1.0",
"qs": "^6.14.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-i18next": "^15.4.0",
"react-markdown": "^9.0.3",
"react-random-avatars": "^1.3.1",
"react-world-flags": "^1.6.0",
"zod": "^3.24.1"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "15.0.3",
"lefthook": "^1.8.5",
"typescript": "^5"
}
}
+1
View File
@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,61 @@
<svg
width="33"
height="32"
viewBox="0 0 33 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
width="6.4"
height="6.38019"
transform="matrix(1 1.74846e-07 1.74846e-07 -1 7.06641 25.5204)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
width="6.4"
height="6.38019"
transform="matrix(1 1.74846e-07 1.74846e-07 -1 0.666504 31.9006)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
width="6.4"
height="6.38019"
transform="matrix(1 1.74846e-07 1.74846e-07 -1 13.4663 19.1406)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
width="6.4"
height="6.38019"
transform="matrix(1 1.74846e-07 1.74846e-07 -1 19.8667 12.761)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<path
d="M32.6663 32L26.2663 32L26.2663 6.38018L0.708989 6.38017L0.70899 -1.52588e-05L24.9863 -1.1014e-05C29.2278 -1.02724e-05 32.6663 3.43845 32.6663 7.68L32.6663 32Z"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="15" width="3" height="3" transform="rotate(-90 0 15)" fill="currentColor" />
<rect x="3" y="12" width="3" height="3" transform="rotate(-90 3 12)" fill="currentColor" />
<rect x="6" y="9" width="3" height="3" transform="rotate(-90 6 9)" fill="currentColor" />
<rect x="9" y="6" width="3" height="3" transform="rotate(-90 9 6)" fill="currentColor"/>
<path d="M0 2.19345e-05V3.00002H11.9997V15H14.9997V3.00002C14.9997 1.34317 13.6565 2.19345e-05 11.9997 2.19345e-05H0Z" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 579 B

+31
View File
@@ -0,0 +1,31 @@
<svg width="361" height="361" viewBox="0 0 361 361" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_236_24563)">
<path d="M180.789 120.41V150.41H210.789V180.41H240.789V180.41C240.789 147.273 213.926 120.41 180.789 120.41V120.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M180.789 240.487L180.789 210.487L150.789 210.487L150.789 180.487L120.789 180.487V180.487C120.789 213.624 147.652 240.487 180.789 240.487V240.487Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M120.789 180.41L150.789 180.41L150.789 150.41L180.789 150.41L180.789 120.41V120.41C147.652 120.41 120.789 147.273 120.789 180.41V180.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M240.789 180.487L210.789 180.487L210.789 210.487L180.789 210.487L180.789 240.487V240.487C213.926 240.487 240.789 213.624 240.789 180.487V180.487Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M330.773 120.41L330.773 150.41V150.41C347.342 150.41 360.773 136.979 360.773 120.41V120.41L330.773 120.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M270.789 60.4102L270.789 90.4102L300.789 90.4102L300.789 60.4102L270.789 60.4102Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M240.789 30.4102L240.789 60.4102L270.789 60.4102L270.789 30.4102L240.789 30.4102Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M60.7891 30.4102L60.7891 60.4102L90.7891 60.4102L90.7891 30.4102L60.7891 30.4102Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M30.7734 60.4102L30.7734 90.4102L60.7734 90.4102L60.7734 60.4102L30.7734 60.4102Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M90.7891 0.410156L90.7891 30.4102L240.789 30.4102L240.789 0.410181L90.7891 0.410156Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<rect width="30" height="150.466" transform="matrix(-1.62921e-07 1 1 1.62921e-07 180.789 120.41)" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<rect width="120" height="30" transform="matrix(-1.62921e-07 1 1 1.62921e-07 330.773 0.410156)" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M301.023 90.4102L301.023 120.41L331.023 120.41L331.023 90.4102L301.023 90.4102Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M30.7734 240.41L30.7734 210.41V210.41C14.2049 210.41 0.77344 223.842 0.773438 240.41V240.41L30.7734 240.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M90.7734 300.41L90.7734 270.41L60.7734 270.41L60.7734 300.41L90.7734 300.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M120.773 330.41L120.773 300.41L90.7734 300.41L90.7734 330.41L120.773 330.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M300.773 330.41L300.773 300.41L270.773 300.41L270.773 330.41L300.773 330.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M330.773 300.41L330.773 270.41L300.773 270.41L300.773 300.41L330.773 300.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M270.773 360.41L270.773 330.41L120.773 330.41L120.773 360.41L270.773 360.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<rect width="30" height="150.481" transform="matrix(1.62921e-07 -1 -1 -1.62921e-07 180.789 240.41)" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<rect width="120" height="30" transform="matrix(1.62921e-07 -1 -1 -1.62921e-07 30.7734 360.41)" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
<path d="M60.5391 270.41L60.5391 240.41L30.5391 240.41L30.5391 270.41L60.5391 270.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
</g>
<defs>
<clipPath id="clip0_236_24563">
<rect width="360" height="360" fill="white" style="fill:white;fill-opacity:1;" transform="translate(0.773438 0.410156)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M3 3L-4.47035e-08 3L-8.31509e-07 6L3 6L3 3Z" fill="currentColor" />
<path d="M6 6L3 6L3 9L6 9L6 6Z" fill="currentColor" />
<path d="M9 9L6 9L6 12L9 12L9 9Z" fill="currentColor" />
<path d="M12 6L9 6L9 9L12 9L12 6Z" fill="currentColor" />
<path d="M15 3L12 3L12 6L15 6L15 3Z" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 431 B

+14
View File
@@ -0,0 +1,14 @@
<svg
width="9"
height="5"
viewBox="0 0 9 5"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0.828125 0.828125L4.5 4.5L8.17188 0.828125"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

After

Width:  |  Height:  |  Size: 256 B

+35
View File
@@ -0,0 +1,35 @@
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.6668 13.3333V14.6667H10.0002V15.3333H2.00016V14.6667H1.3335V3.99999H2.00016V3.33333H4.00016V13.3333H10.6668Z"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<path
d="M10.6665 4.66667V0.666672H5.33317V1.33334H4.6665V12H5.33317V12.6667H13.9998V12H14.6665V4.66667H10.6665ZM13.3332 11.3333H5.99984V2.00001H9.33317V6.00001H13.3332V11.3333Z"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<path
d="M14.6668 3.33334V4H11.3335V0.666672H12.0002V1.33334H12.6668V2.00001H13.3335V2.66667H14.0002V3.33334H14.6668Z"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
</svg>

After

Width:  |  Height:  |  Size: 967 B

+135
View File
@@ -0,0 +1,135 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
>
<g clip-path="url(#clip0_8428_65000)">
<rect
y="3"
width="3"
height="3"
transform="rotate(-90 0 3)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
y="15"
width="3"
height="3"
transform="rotate(-90 0 15)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
x="12"
y="3"
width="3"
height="3"
transform="rotate(-90 12 3)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
x="9"
y="12"
width="3"
height="3"
transform="rotate(-90 9 12)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
x="12"
y="15"
width="3"
height="3"
transform="rotate(-90 12 15)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
x="3"
y="6"
width="3"
height="3"
transform="rotate(-90 3 6)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
x="3"
y="12"
width="3"
height="3"
transform="rotate(-90 3 12)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
x="9"
y="6"
width="3"
height="3"
transform="rotate(-90 9 6)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
x="6"
y="9"
width="3"
height="3"
transform="rotate(-90 6 9)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
</g>
<defs>
<clipPath id="clip0_8428_65000">
<rect
width="15"
height="15"
fill="white"
style="fill: white; fill-opacity: 1"
/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.8343 11.2552C23.7199 14.0472 24.6511 17.1964 24.303 20.8219C24.3015 20.8372 24.2936 20.8513 24.2811 20.8606C22.8532 21.9165 21.4697 22.5573 20.1057 22.9823C20.0951 22.9855 20.0837 22.9853 20.0732 22.9817C20.0627 22.9782 20.0535 22.9714 20.047 22.9623C19.7319 22.5207 19.4456 22.0552 19.1947 21.5663C19.1803 21.5375 19.1935 21.5028 19.2231 21.4915C19.6779 21.3189 20.1103 21.1121 20.5262 20.8673C20.559 20.8479 20.5611 20.8007 20.5308 20.778C20.4425 20.712 20.3551 20.6426 20.2714 20.5733C20.2557 20.5604 20.2347 20.5579 20.2169 20.5665C17.5166 21.8223 14.5586 21.8223 11.8263 20.5665C11.8086 20.5585 11.7875 20.5613 11.7723 20.5739C11.6888 20.6432 11.6011 20.712 11.5137 20.778C11.4834 20.8007 11.4859 20.8479 11.5189 20.8673C11.9349 21.1075 12.3673 21.3189 12.8214 21.4923C12.8508 21.5037 12.8648 21.5375 12.8502 21.5663C12.6048 22.0558 12.3184 22.5213 11.9975 22.9629C11.9835 22.9808 11.9605 22.989 11.9388 22.9823C10.5813 22.5573 9.19781 21.9165 7.76992 20.8606C7.75802 20.8513 7.74947 20.8366 7.74822 20.8213C7.45729 17.6853 8.0502 14.51 10.2146 11.2546C10.2198 11.246 10.2277 11.2393 10.2369 11.2353C11.3019 10.743 12.4428 10.3809 13.6353 10.1741C13.657 10.1707 13.6787 10.1808 13.69 10.2002C13.8373 10.4629 14.0057 10.7998 14.1197 11.0751C15.3767 10.8818 16.6532 10.8818 17.9365 11.0751C18.0505 10.8057 18.213 10.4629 18.3597 10.2002C18.365 10.1906 18.3731 10.1829 18.3829 10.1782C18.3927 10.1735 18.4037 10.1721 18.4144 10.1741C19.6075 10.3815 20.7485 10.7437 21.8126 11.2353C21.822 11.2393 21.8297 11.246 21.8343 11.2552V11.2552ZM14.7587 17.2178C14.7719 16.2908 14.1007 15.5236 13.2582 15.5236C12.4226 15.5236 11.7579 16.284 11.7579 17.2178C11.7579 18.1514 12.4357 18.9118 13.2582 18.9118C14.094 18.9118 14.7587 18.1514 14.7587 17.2178V17.2178ZM20.3062 17.2178C20.3194 16.2908 19.6482 15.5236 18.8059 15.5236C17.9701 15.5236 17.3054 16.284 17.3054 17.2178C17.3054 18.1514 17.9833 18.9118 18.8059 18.9118C19.6482 18.9118 20.3062 18.1514 20.3062 17.2178V17.2178Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none">
<g clip-path="url(#clip0_6568_5882)">
<path d="M4 8H8V9H4V8ZM4 6H8V7H4V6ZM7 1H3C2.45 1 2 1.45 2 2V10C2 10.55 2.445 11 2.995 11H9C9.55 11 10 10.55 10 10V4L7 1ZM9 10H3V2H6.5V4.5H9V10Z" fill="#242B2D" style="fill:#242B2D;fill:color(display-p3 0.1412 0.1686 0.1765);fill-opacity:1;"/>
</g>
<defs>
<clipPath id="clip0_6568_5882">
<rect width="12" height="12" fill="white" style="fill:white;fill-opacity:1;"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 526 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M2.5 5H2V4H3V4.5H3.5V5H4V5.5H4.5V6H5V6.5H5.5V0.5H6.5V6.5H7V6H7.5V5.5H8V5H8.5V4.5H9V4H10V5H9.5V5.5H9V6H8.5V6.5H8V7H7.5V7.5H7V8H6.5V8.5H5.5V8H5V7.5H4.5V7H4V6.5H3.5V6H3V5.5H2.5V5Z" fill="#242B2D" style="fill:#242B2D;fill:color(display-p3 0.1412 0.1686 0.1765);fill-opacity:1;"/>
<path d="M11 10.5H1V11.5H11V10.5Z" fill="#242B2D" style="fill:#242B2D;fill:color(display-p3 0.1412 0.1686 0.1765);fill-opacity:1;"/>
</svg>

After

Width:  |  Height:  |  Size: 520 B

+22
View File
@@ -0,0 +1,22 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="25"
viewBox="0 0 24 25"
fill="none"
>
<circle cx="12" cy="12.5" r="10" fill="url(#paint0_angular_2549_7570)" />
<defs>
<radialGradient
id="paint0_angular_2549_7570"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(12 12.5) rotate(90) scale(12)"
>
<stop stopColor="#22D27E" />
<stop offset="1" stopColor="#9002FF" />
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 515 B

@@ -0,0 +1,3 @@
<svg width="84" height="84" viewBox="0 0 84 84" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M63 0L56 -2.44784e-06L56 7L63 7L70 7H77V14V21L77 28L84 28V21V14C84 6.26801 77.732 0 70 0H63ZM7 14L7 7H14H21L28 7L28 2.14186e-06L21 0H14C6.26801 0 0 6.26801 0 14V21L-3.87835e-06 28L7 28L7 21V14ZM84 70C84 77.732 77.732 84 70 84H63H56L56 77H63H70H77V70L77 63L77 56L84 56V63V70ZM21 77H14L14 70L21 70L21 63H28L28 56H35L35 63H42H49L49 56H56L56 49L63 49L63 42V35L56 35V28L49 28L49 21L42 21H35L35 28L28 28L28 35H21L21 42V49L28 49L28 56H21L21 63L14 63L14 70H7L7 77L7 70L7 63L7 56L0 56L-3.87835e-06 63L0 70C0 77.732 6.26801 84 14 84H21H28L28 77H21ZM28 49L28 42L28 35H35L35 28H42L49 28L49 35L56 35V42L56 49H49V56L42 56H35V49H28Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
</svg>

After

Width:  |  Height:  |  Size: 869 B

+79
View File
@@ -0,0 +1,79 @@
<svg
width="84"
height="84"
viewBox="0 0 84 84"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
width="7"
height="56"
transform="matrix(-2.36041e-07 1 1 7.28523e-08 28 77)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
width="7"
height="56"
transform="matrix(1 0 -1.63189e-07 -1 0 56)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<path
d="M63 0L63 21L84 21L84 28L63 28C59.134 28 56 24.866 56 21L56 0H63Z"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
width="7"
height="42"
transform="matrix(1 0 -1.63189e-07 -1 28 42)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<rect
width="7"
height="35"
transform="matrix(-2.36041e-07 1 1 7.28523e-08 49 49)"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<path
d="M28 84L28 77L7 77L7 56L-1.89145e-06 56L-2.80939e-06 77C-2.97838e-06 80.866 3.134 84 7 84L28 84Z"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
<path
d="M56 56L56 49L35 49L35 28L28 28L28 49C28 52.866 31.134 56 35 56L56 56Z"
fill="#242B2D"
style="
fill: #242b2d;
fill: color(display-p3 0.1412 0.1686 0.1765);
fill-opacity: 1;
"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9408 7.86426C10.9716 7.86426 6.94678 11.8731 6.94678 16.8225C6.94678 20.7866 9.52132 24.1347 13.0965 25.3217C13.5462 25.4001 13.7148 25.1313 13.7148 24.8962C13.7148 24.6834 13.7035 23.9779 13.7035 23.2277C11.4438 23.642 10.8592 22.679 10.6793 22.1751C10.5781 21.9175 10.1397 21.1225 9.75741 20.9097C9.44262 20.7418 8.99292 20.3274 9.74617 20.3162C10.4545 20.3051 10.9604 20.9657 11.129 21.2345C11.9385 22.5894 13.2314 22.2087 13.7485 21.9735C13.8272 21.3912 14.0633 20.9993 14.3219 20.7754C12.3207 20.5514 10.2296 19.7788 10.2296 16.3522C10.2296 15.378 10.5781 14.5718 11.1515 13.9447C11.0616 13.7207 10.7468 12.8025 11.2414 11.5707C11.2414 11.5707 11.9947 11.3356 13.7148 12.489C14.4343 12.2874 15.1988 12.1866 15.9633 12.1866C16.7278 12.1866 17.4923 12.2874 18.2118 12.489C19.9319 11.3244 20.6852 11.5707 20.6852 11.5707C21.1798 12.8025 20.8651 13.7207 20.7751 13.9447C21.3485 14.5718 21.697 15.3668 21.697 16.3522C21.697 19.79 19.5946 20.5514 17.5935 20.7754C17.9195 21.0553 18.2006 21.5928 18.2006 22.4326C18.2006 23.6308 18.1893 24.5938 18.1893 24.8962C18.1893 25.1313 18.358 25.4113 18.8077 25.3217C22.3603 24.1347 24.9349 20.7754 24.9349 16.8225C24.9349 11.8731 20.91 7.86426 15.9408 7.86426Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg viewBox="0 0 89 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M0 2.73999C0 1.50288 1.00579 0.5 2.2465 0.5H10.4256C11.8792 0.5 13.1513 1.47412 13.5263 2.87439L18.7506 22.3815C18.8474 22.7432 19.3816 22.6733 19.3816 22.299V0.5H25.8001V22.2599C25.8001 23.497 24.7943 24.4999 23.5536 24.4999H15.314C13.8643 24.4999 12.5946 23.5308 12.216 22.1355L7.04928 3.0892C6.95141 2.72844 6.41855 2.79902 6.41855 3.17275V24.4999H0V2.73999Z"
fill="currentColor"
/>
<path
d="M55.5159 0.500025C54.2752 0.500025 53.2694 1.5029 53.2694 2.74002V24.4999H59.6879V3.18233C59.6879 2.79548 60.2483 2.74066 60.3237 3.12013L64.0601 21.9219C64.3579 23.4203 65.6762 24.4999 67.2082 24.4999L74.6942 24.4999C76.2294 24.4999 77.5496 23.4158 77.8439 21.9135L81.5171 3.16662C81.5916 2.78643 82.153 2.84061 82.153 3.22798V24.4999H88.5714V2.74002C88.5714 1.5029 87.5656 0.500025 86.3249 0.500025L78.3813 0.50002C76.859 0.500019 75.5461 1.56646 75.2383 3.05306L71.2361 22.3844C71.1655 22.7251 70.6773 22.7246 70.6074 22.3838L66.6405 3.05837C66.3349 1.56926 65.0208 0.500019 63.4964 0.50002L55.5159 0.500025Z"
fill="currentColor"
/>
<path
d="M26.5569 0.50009H33.3531L39.2253 13.0169C39.4563 13.5093 40.1589 13.5085 40.3888 13.0155L46.2237 0.50009H53.1458L41.8949 24.5H34.9858L39.4713 14.9H35.3564C34.1107 14.9 32.9775 14.1813 32.4496 13.0563L26.5569 0.50009Z"
fill="currentColor"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,3 @@
<svg width="84" height="84" viewBox="0 0 84 84" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M63 0H56V7H63H70H77V14V21V28H84V21V14C84 6.26801 77.732 0 70 0H63ZM7 77H14H21H28V84H21H14C6.26801 84 0 77.732 0 70V63V56H7L7 63L7 70L7 77ZM70 84C77.732 84 84 77.732 84 70V63V56H77V63V70V77H70H63H56V84H63H70ZM7 7L7 14L7 21L7 28H0V21V14C1.93187e-06 6.26801 6.26802 0 14 0H21H28V7L21 7L14 7L7 7ZM63 21L63 28L56 28L56 21L63 21ZM63 21V14L70 14L70 21L63 21ZM49 35V28L56 28V35H49ZM49 49V42L49 35L42 35L35 35L35 28L28 28L28 21L21 21L21 14L14 14L14 21L21 21L21 28L28 28V35L35 35V42V49L28 49L28 56L21 56L21 63H14L14 70H21L21 63L28 63L28 56H35V49H42L49 49ZM56 56H49L49 49L56 49V56ZM63 63L56 63V56L63 56L63 63ZM63 63H70V70H63L63 63Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
</svg>

After

Width:  |  Height:  |  Size: 872 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="84" height="84" viewBox="0 0 84 84" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M84 14C84 6.26801 77.732 0 70 0H63H56V7H63H70H77V14V21V28H84V21V14ZM28 14C20.268 14 14 20.268 14 28C14 35.732 20.268 42 28 42C20.268 42 14 48.268 14 56C14 63.732 20.268 70 28 70C35.7299 70 41.9966 63.7354 42 56.0062C42.0034 63.7353 48.2701 70 56 70C63.732 70 70 63.732 70 56C70 48.2701 63.7353 42.0034 56.0062 42C63.7353 41.9966 70 35.7299 70 28C70 20.268 63.732 14 56 14C48.268 14 42 20.268 42 28C42 20.268 35.732 14 28 14ZM49 28V35L56 35H63V28V21L56 21H49V28ZM42 28.0062C41.9966 35.7329 35.7339 41.996 28.0074 42C35.736 42.004 42 48.2705 42 56C42 48.268 48.268 42 56 42C48.2701 42 42.0034 35.7353 42 28.0062ZM56 63H63V56V49L56 49H49V56V63L56 63ZM28 35H35V28V21L28 21H21V28L21 35L28 35ZM21 49V56L21 63L28 63H35V56V49L28 49H21ZM7 77H14H21H28V84H21H14C6.26801 84 0 77.732 0 70V63V56H7L7 63L7 70L7 77ZM70 84C77.732 84 84 77.732 84 70V63V56H77V63V70V77H70H63H56V84H63H70ZM7 7L7 14L7 21L7 28H0V21V14C1.93187e-06 6.26801 6.26802 0 14 0H21H28V7L21 7L14 7L7 7Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.90246 15.9332C8.90246 15.9332 15.4128 13.1912 17.6706 12.2256C18.5362 11.8394 21.4715 10.6036 21.4715 10.6036C21.4715 10.6036 22.8262 10.0629 22.7133 11.376C22.6757 11.9167 22.3746 13.8091 22.0736 15.8559C21.622 18.7525 21.1328 21.9194 21.1328 21.9194C21.1328 21.9194 21.0575 22.8077 20.4178 22.9621C19.7781 23.1166 18.7243 22.4215 18.5362 22.267C18.3856 22.1511 15.7138 20.4132 14.7354 19.5635C14.4719 19.3318 14.1709 18.8684 14.773 18.3277C16.1278 17.0532 17.7459 15.4698 18.7243 14.4656C19.1759 14.0022 19.6275 12.9208 17.7459 14.2339C15.0741 16.1263 12.4398 17.9029 12.4398 17.9029C12.4398 17.9029 11.8377 18.289 10.7088 17.9414C9.57979 17.5939 8.26268 17.1304 8.26268 17.1304C8.26268 17.1304 7.35957 16.5511 8.90246 15.9332Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 845 B

+23
View File
@@ -0,0 +1,23 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="25"
viewBox="0 0 24 25"
fill="none"
>
<g clipPath="url(#clip0_2549_7563)">
<path
d="M20.4841 4.01607C15.8041 -0.67593 8.19607 -0.67593 3.51607 4.01607C-1.17593 8.70807 -1.17593 16.3041 3.51607 20.9841C8.20807 25.6761 15.8041 25.6761 20.4841 20.9841C25.1761 16.3041 25.1761 8.69607 20.4841 4.01607ZM19.4521 19.9521C15.3361 24.0681 8.65207 24.0681 4.53607 19.9521C0.42007 15.8361 0.42007 9.15207 4.53607 5.03607C8.65207 0.92007 15.3361 0.92007 19.4521 5.03607C23.5801 9.16407 23.5801 15.8361 19.4521 19.9521Z"
fill="black"
/>
<path
d="M18.48 19.4965V5.50447C17.868 4.92847 17.184 4.42447 16.452 4.02847V17.4085L7.62002 3.98047C6.85202 4.38847 6.14402 4.89247 5.52002 5.49247V19.4965C6.13202 20.0725 6.81602 20.5765 7.54802 20.9725V7.59247L16.38 21.0205C17.148 20.6125 17.856 20.0965 18.48 19.4965Z"
fill="black"
/>
</g>
<defs>
<clipPath id="clip0_2549_7563">
<rect width="24" height="24" fill="white" transform="translate(0 0.5)" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.4206 15.4955L22.9798 9.03333H21.6625L16.8354 14.6444L12.98 9.03333H8.5332L14.3633 17.5182L8.5332 24.2948H9.85065L14.9482 18.3694L19.0198 24.2948H23.4665L17.4202 15.4955H17.4206ZM15.6161 17.593L15.0254 16.7481L10.3253 10.0251H12.3489L16.1419 15.4507L16.7326 16.2956L21.6631 23.3482H19.6396L15.6161 17.5933V17.593Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 453 B

+5
View File
@@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.9004 12.0986C24.6915 11.3132 24.0734 10.6936 23.2871 10.4812C21.8652 10.1001 16.1605 10.1001 16.1605 10.1001C16.1605 10.1001 10.4587 10.1001 9.03399 10.4812C8.25053 10.6906 7.63247 11.3103 7.42065 12.0986C7.04053 13.5241 7.04053 16.5001 7.04053 16.5001C7.04053 16.5001 7.04053 19.4761 7.42065 20.9016C7.62957 21.687 8.24763 22.3066 9.03399 22.519C10.4587 22.9001 16.1605 22.9001 16.1605 22.9001C16.1605 22.9001 21.8652 22.9001 23.2871 22.519C24.0705 22.3096 24.6886 21.6899 24.9004 20.9016C25.2805 19.4761 25.2805 16.5001 25.2805 16.5001C25.2805 16.5001 25.2805 13.5241 24.9004 12.0986Z" fill="currentColor"/>
<path d="M14.3383 19.2434L19.0767 16.5001L14.3383 13.7568V19.2434Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 810 B

@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.9004 12.0986C24.6915 11.3132 24.0734 10.6936 23.2871 10.4812C21.8652 10.1001 16.1605 10.1001 16.1605 10.1001C16.1605 10.1001 10.4587 10.1001 9.03399 10.4812C8.25053 10.6906 7.63247 11.3103 7.42065 12.0986C7.04053 13.5241 7.04053 16.5001 7.04053 16.5001C7.04053 16.5001 7.04053 19.4761 7.42065 20.9016C7.62957 21.687 8.24763 22.3066 9.03399 22.519C10.4587 22.9001 16.1605 22.9001 16.1605 22.9001C16.1605 22.9001 21.8652 22.9001 23.2871 22.519C24.0705 22.3096 24.6886 21.6899 24.9004 20.9016C25.2805 19.4761 25.2805 16.5001 25.2805 16.5001C25.2805 16.5001 25.2805 13.5241 24.9004 12.0986Z" fill="currentColor"/>
<path d="M14.3383 19.2434L19.0767 16.5001L14.3383 13.7568V19.2434Z" fill="#242B2D"/>
</svg>

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

@@ -0,0 +1,62 @@
// import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
import SectionHeading from "@/components/headings/SectionHeading";
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
import { Box, Typography } from "@mui/material";
import Grid from "@mui/material/Grid2";
import Markdown from "react-markdown";
export default async function Account({
params,
}: {
params: Promise<{ address: string }>;
}) {
try {
const address = (await params).address;
return (
<ContentLayout>
<Grid container columnSpacing={5} rowSpacing={5}>
<Grid size={12}>
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<SectionHeading title="Nym Node Details" />
<ExplorerButtonGroup
onPage="Account"
options={[
{
label: "Nym Node",
isSelected: true,
link: `/account/${address}/not-found/`,
},
{
label: "Account",
isSelected: false,
link: `/account/${address}`,
},
]}
/>
</Box>
</Grid>
</Grid>
<Typography variant="h5">
<Markdown className="reactMarkDownLink">
This account doest have a Nym node bonded. Is this your account?
Start [setting up your node](https://nym.com/docs) today!
</Markdown>
</Typography>
{/* <Grid container columnSpacing={5} rowSpacing={5}>
<Grid size={12}>
<SectionHeading title="Onboarding" />
</Grid>
<BlogArticlesCards ids={[1]} />
</Grid> */}
</ContentLayout>
);
} catch (error) {
let errorMessage = "An error occurred";
if (error instanceof Error) {
errorMessage = error.message;
}
throw new Error(errorMessage);
}
}
@@ -0,0 +1,67 @@
import { fetchNodes } from "@/app/api";
import type { NodeData } from "@/app/api/types";
import { Box, Typography } from "@mui/material";
import Grid from "@mui/material/Grid2";
import { AccountBalancesCard } from "../../../../components/accountPageComponents/AccountBalancesCard";
import { AccountInfoCard } from "../../../../components/accountPageComponents/AccountInfoCard";
import { ContentLayout } from "../../../../components/contentLayout/ContentLayout";
import SectionHeading from "../../../../components/headings/SectionHeading";
import ExplorerButtonGroup from "../../../../components/toggleButton/ToggleButton";
export default async function Account({
params,
}: {
params: Promise<{ address: string }>;
}) {
try {
const address = (await params).address;
const nymNodes: NodeData[] = await fetchNodes();
const nymNode = nymNodes.find(
(node) => node.bond_information.owner === address,
);
return (
<ContentLayout>
<Grid container columnSpacing={5} rowSpacing={5}>
<Grid size={6}>
<SectionHeading title="Account Details" />
</Grid>
<Grid size={6} justifyContent="flex-end">
<Box sx={{ display: "flex", justifyContent: "end" }}>
<ExplorerButtonGroup
onPage="Account"
options={[
{
label: "Nym Node",
isSelected: false,
link: nymNode
? `/nym-node/${nymNode.node_id}`
: `/account/${address}/not-found`,
},
{
label: "Account",
isSelected: true,
link: `/account/${address}`,
},
]}
/>
</Box>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
<AccountInfoCard address={address} />
</Grid>
<Grid size={{ xs: 12, md: 8 }}>
<AccountBalancesCard address={address} />
</Grid>
</Grid>
</ContentLayout>
);
} catch (error) {
console.error("error :>> ", error);
return <Typography>Error loading account data</Typography>;
}
}
@@ -0,0 +1,128 @@
import { fetchNodeIdByIdentityKey, fetchNodeInfo } from "@/app/api";
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
import SectionHeading from "@/components/headings/SectionHeading";
import { BasicInfoCard } from "@/components/nymNodePageComponents/BasicInfoCard";
import { NodeDataCard } from "@/components/nymNodePageComponents/NodeDataCard";
// import { NodeChatCard } from "@/components/nymNodePageComponents/ChatCard";
import NodeDelegationsCard from "@/components/nymNodePageComponents/NodeDelegationsCard";
import { NodeParametersCard } from "@/components/nymNodePageComponents/NodeParametersCard";
import { NodeProfileCard } from "@/components/nymNodePageComponents/NodeProfileCard";
import { NodeRoleCard } from "@/components/nymNodePageComponents/NodeRoleCard";
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
import { Box } from "@mui/material";
import Grid from "@mui/material/Grid2";
export default async function NymNode({
params,
}: {
params: Promise<{ id: string }>; // node_id or identity_key
}) {
try {
let id: string | number;
const paramsId = (await params).id;
// check if the params id is a node_id or identity_key
if (paramsId.length > 10) {
id = await fetchNodeIdByIdentityKey(paramsId);
} else {
id = Number(paramsId);
}
const observatoryNymNode = await fetchNodeInfo(id);
if (!observatoryNymNode) {
return null;
}
return (
<ContentLayout>
<Grid container columnSpacing={5} rowSpacing={5}>
<Grid size={12}>
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<SectionHeading title="Nym Node Details" />
{observatoryNymNode.bonding_address && (
<ExplorerButtonGroup
onPage="Nym Node"
options={[
{
label: "Nym Node",
isSelected: true,
link: `/nym-node/${id}`,
},
{
label: "Account",
isSelected: false,
link: `/account/${observatoryNymNode.bonding_address}`,
},
]}
/>
)}
</Box>
</Grid>
<Grid
size={{
xs: 12,
md: 4,
}}
>
<NodeProfileCard id={id} />
</Grid>
<Grid
size={{
xs: 12,
md: 4,
}}
>
<BasicInfoCard id={id} />
</Grid>
<Grid
size={{
xs: 12,
md: 4,
}}
>
<NodeRoleCard id={id} />
</Grid>
<Grid
size={{
xs: 12,
md: 6,
}}
>
<NodeParametersCard id={id} />
</Grid>
<Grid
size={{
xs: 12,
md: 6,
}}
>
<NodeDataCard id={id} />
</Grid>
<Grid
size={{
xs: 12,
}}
>
<NodeDelegationsCard id={id} />
</Grid>
{/*
<Grid
size={{
xs: 12,
}}
>
<NodeChatCard />
</Grid> */}
</Grid>
</ContentLayout>
);
} catch (error) {
let errorMessage = "An error occurred";
if (error instanceof Error) {
errorMessage = error.message;
}
throw new Error(errorMessage);
}
}
@@ -0,0 +1,155 @@
import TableOfContents from "@/components/blogs/TableOfContents";
import type BlogArticle from "@/components/blogs/types";
import { Breadcrumbs } from "@/components/breadcrumbs/Breadcrumbs";
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
import SectionHeading from "@/components/headings/SectionHeading";
import { Link } from "@/components/muiLink";
import { Wrapper } from "@/components/wrapper";
import { Box, Stack, Typography } from "@mui/material";
import Grid from "@mui/material/Grid2";
import { format } from "date-fns";
import Image from "next/image";
import Markdown from "react-markdown";
export default async function BlogPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
try {
const blogArticle: BlogArticle = await import(`@/data/${slug}.json`);
const breadcrumbItems = [
{
label: "Onboarding",
href: "/onboarding",
},
{ label: blogArticle.title, isCurrentPage: true },
];
return (
<ContentLayout>
<Wrapper>
<Grid container spacing={5}>
<Grid size={{ xs: 12, md: 8 }}>
<Stack spacing={4}>
<Breadcrumbs items={breadcrumbItems} />
<SectionHeading title={blogArticle.title} />
<Box
sx={{
borderTop: "1px dashed",
paddingBlockStart: "10px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography
variant="subtitle3"
sx={{
display: "flex",
gap: "20px",
alignItems: "center",
}}
>
<Box
sx={{
display: "flex",
gap: "10px",
alignItems: "center",
}}
>
Author
{(blogArticle?.attributes?.blogAuthors?.length ?? 0) > 1
? "s"
: ""}
:{" "}
{blogArticle?.attributes?.blogAuthors?.map(
(author: string) => (
<Typography key={author} variant="subtitle3">
{author}
</Typography>
)
)}
</Box>
<time dateTime={blogArticle?.attributes?.date.toString()}>
{format(
new Date(blogArticle?.attributes?.date),
"MMMM dd, yyyy"
)}
</time>
</Typography>
<Typography variant="subtitle3">
{blogArticle.attributes.readingTime}{" "}
{blogArticle.attributes.readingTime > 1 ? "mins" : "min"}{" "}
read
</Typography>
</Box>
<Image
src={blogArticle.image}
alt="blog-image"
width={120}
height={60}
sizes="100vw"
style={{
width: "100%",
height: "auto",
}}
/>
<Box>
{blogArticle.overview.content.map(({ text }) => (
<Box key={text} sx={{ mt: 3 }}>
<Typography variant="body2" component="span">
<Markdown className="reactMarkDownLink reactMarkDownList">
{text}
</Markdown>
</Typography>
</Box>
))}
</Box>
{blogArticle.sections.map((section) => (
<Box key={section.heading} id={section.id}>
<SectionHeading title={section.heading} />
{section.text.map(({ text }) => (
<Box key={text} sx={{ mt: 3 }}>
<Typography variant="body2" component="span">
<Markdown className="reactMarkDownLink reactMarkDownList">
{text}
</Markdown>
</Typography>
</Box>
))}
</Box>
))}
</Stack>
</Grid>
<Grid size={{ md: 4 }}>
<TableOfContents
headings={blogArticle.sections.map((section) => ({
heading: section.heading,
id: section.id,
}))}
/>
</Grid>
</Grid>
</Wrapper>
</ContentLayout>
);
} catch (error) {
console.log(error);
return (
<ContentLayout>
<Wrapper>
<SectionHeading title={"Off the grid, like your data"} />
<Typography variant="body2">
Oops! Looks like the page youre looking for got mixed up in the
noise. Dont worry, your privacy is intact. Lets get you
<Link href="/">back to the homepage.</Link>
</Typography>
</Wrapper>
</ContentLayout>
);
}
}
@@ -0,0 +1,15 @@
import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
import SectionHeading from "@/components/headings/SectionHeading";
import Grid from "@mui/material/Grid2";
export default function OnboardingPage() {
return (
<ContentLayout>
<SectionHeading title="Onboarding page" />
<Grid container spacing={4}>
<BlogArticlesCards ids={[1, 2]} />
</Grid>
</ContentLayout>
);
}
@@ -0,0 +1,24 @@
// import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
// import Grid from "@mui/material/Grid2";
import { ContentLayout } from "../../../components/contentLayout/ContentLayout";
import SectionHeading from "../../../components/headings/SectionHeading";
import OverviewCards from "../../../components/staking/OverviewCards";
import StakeTableWithAction from "../../../components/staking/StakeTableWithAction";
import SubHeaderRow from "../../../components/staking/SubHeaderRow";
export default async function StakingPage() {
return (
<ContentLayout>
<SectionHeading title="Staking" />
<SubHeaderRow />
<OverviewCards />
<StakeTableWithAction />
{/* <Grid container columnSpacing={5} rowSpacing={5}>
<Grid size={12}>
<SectionHeading title="Onboarding" />
</Grid>
<BlogArticlesCards ids={[1]} />
</Grid> */}
</ContentLayout>
);
}
@@ -0,0 +1,30 @@
// import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
import SectionHeading from "@/components/headings/SectionHeading";
import NodeTableWithAction from "@/components/nodeTable/NodeTableWithAction";
import NodeAndAddressSearch from "@/components/search/NodeAndAddressSearch";
import { Wrapper } from "@/components/wrapper";
import { Box, Stack } from "@mui/material";
// import Grid from "@mui/material/Grid2";
export default function ExplorerPage() {
return (
<ContentLayout>
<Wrapper>
<Stack gap={5}>
<SectionHeading title="Explorer" />
<NodeAndAddressSearch />
</Stack>
<Box sx={{ mt: 5 }}>
<NodeTableWithAction />
</Box>
{/* <Grid container columnSpacing={5} rowSpacing={5} mt={10}>
<Grid size={12}>
<SectionHeading title="Onboarding" />
</Grid>
<BlogArticlesCards limit={2} />
</Grid> */}
</Wrapper>
</ContentLayout>
);
}
+271
View File
@@ -0,0 +1,271 @@
import { addSeconds } from "date-fns";
/* eslint-disable @typescript-eslint/no-explicit-any */
import type {
CurrentEpochData,
ExplorerData,
GatewayStatus,
IAccountBalancesInfo,
IObservatoryNode,
IPacketsAndStakingData,
NodeData,
NodeRewardDetails,
NymTokenomics,
ObservatoryBalance,
} from "./types";
import {
CURRENT_EPOCH,
CURRENT_EPOCH_REWARDS,
DATA_OBSERVATORY_BALANCES_URL,
DATA_OBSERVATORY_NODES_DELEGATIONS_URL,
DATA_OBSERVATORY_NODES_URL,
NS_API_MIXNODES_STATS,
NYM_ACCOUNT_ADDRESS,
NYM_NODES,
NYM_PRICES_API,
OBSERVATORY_GATEWAYS_URL,
} from "./urls";
// Fetch function for epoch rewards
export const fetchEpochRewards = async (): Promise<
ExplorerData["currentEpochRewardsData"]
> => {
const response = await fetch(CURRENT_EPOCH_REWARDS, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
cache: "no-store", // Ensures fresh data on every request
});
if (!response.ok) {
throw new Error("Failed to fetch epoch rewards");
}
return response.json();
};
// Fetch gateway status based on identity key
export const fetchGatewayStatus = async (
identityKey: string,
): Promise<GatewayStatus | null> => {
const response = await fetch(`${OBSERVATORY_GATEWAYS_URL}/${identityKey}`);
if (!response.ok) {
throw new Error("Failed to fetch gateway status");
}
return response.json();
};
export const fetchNodeInfo = async (
id: number,
): Promise<IObservatoryNode | undefined> => {
const nodes = await fetchObservatoryNodes();
return nodes?.find((node) => node.node_id === id);
};
export const fetchNodeIdByIdentityKey = async (
identity_key: string,
): Promise<number> => {
const nodes = await fetchObservatoryNodes();
const node = nodes?.find((node) => node.identity_key === identity_key);
return node?.node_id || 0;
};
export const fetchNodeDelegations = async (
id: number,
): Promise<NodeRewardDetails[]> => {
const response = await fetch(
`${DATA_OBSERVATORY_NODES_DELEGATIONS_URL}/${id}/delegations`,
{
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
},
);
if (!response.ok) {
throw new Error("Failed to fetch delegations");
}
return response.json();
};
export const fetchCurrentEpoch = async () => {
const response = await fetch(CURRENT_EPOCH, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
cache: "no-store", // Ensures fresh data on every request
});
if (!response.ok) {
throw new Error("Failed to fetch current epoch data");
}
const data: CurrentEpochData = await response.json();
const epochEndTime = addSeconds(
new Date(data.current_epoch_start),
data.epoch_length.secs,
).toISOString();
return { ...data, current_epoch_end: epochEndTime };
};
// Fetch balances based on the address
export const fetchBalances = async (address: string): Promise<number> => {
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
});
if (!response.ok) {
throw new Error("Failed to fetch balances");
}
const balances: ObservatoryBalance = await response.json();
// Calculate total stake
return (
Number(balances.rewards.staking_rewards.amount) +
Number(balances.delegated.amount)
);
};
// Fetch function to get total staker rewards
export const fetchTotalStakerRewards = async (
address: string,
): Promise<number> => {
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
});
if (!response.ok) {
throw new Error("Failed to fetch balances");
}
const balances: ObservatoryBalance = await response.json();
// Return the staking rewards amount
return Number(balances.rewards.staking_rewards.amount);
};
// Fetch function to get the original stake
export const fetchOriginalStake = async (address: string): Promise<number> => {
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
});
if (!response.ok) {
throw new Error("Failed to fetch balances");
}
const balances: ObservatoryBalance = await response.json();
// Return the delegated amount
return Number(balances.delegated.amount);
};
export const fetchNoise = async (): Promise<IPacketsAndStakingData[]> => {
const response = await fetch(NS_API_MIXNODES_STATS, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
});
const data: IPacketsAndStakingData[] = await response.json();
return data;
};
// Fetch Account Balance
export const fetchAccountBalance = async (
address: string,
): Promise<IAccountBalancesInfo> => {
const res = await fetch(`${NYM_ACCOUNT_ADDRESS}/${address}`, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
});
if (!res.ok) {
throw new Error("Failed to fetch account balance error from api");
}
const data: IAccountBalancesInfo = await res.json();
return data;
};
// 🔹 Fetch Nodes
export const fetchNodes = async (): Promise<NodeData[]> => {
const res = await fetch(NYM_NODES, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
});
if (!res.ok) {
throw new Error("Failed to fetch nodes");
}
const data: NodeData[] = await res.json();
return data;
};
export const fetchObservatoryNodes = async (): Promise<IObservatoryNode[]> => {
const allNodes: IObservatoryNode[] = [];
let page = 1;
const PAGE_SIZE = 200;
let hasMoreData = true;
while (hasMoreData) {
const response = await fetch(
`${DATA_OBSERVATORY_NODES_URL}?page=${page}&limit=${PAGE_SIZE}`,
{
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
},
);
if (!response.ok) {
throw new Error(`Failed to fetch observatory nodes (page ${page})`);
}
const nodes: IObservatoryNode[] = await response.json();
allNodes.push(...nodes);
if (nodes.length < PAGE_SIZE) {
hasMoreData = false; // Stop fetching when the last page has fewer than 200 items
} else {
page++; // Move to the next page
}
}
return allNodes;
};
// 🔹 Fetch NYM Price
export const fetchNymPrice = async (): Promise<NymTokenomics> => {
const res = await fetch(NYM_PRICES_API, {
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",
},
});
if (!res.ok) {
throw new Error("Failed to fetch NYM price");
}
const data: NymTokenomics = await res.json();
return data;
};
+23417
View File
File diff suppressed because it is too large Load Diff
+482
View File
@@ -0,0 +1,482 @@
export type API_RESPONSE<T> = {
data: T[];
};
export type Denom = "unym" | "nym";
export interface IPacketsAndStakingData {
date_utc: string;
total_packets_received: number;
total_packets_sent: number;
total_packets_dropped: number;
total_stake: number;
}
export interface CurrentEpochData {
id: number;
current_epoch_id: number;
current_epoch_start: string;
epoch_length: { secs: number; nanos: number };
epochs_in_interval: number;
total_elapsed_epochs: number;
}
export interface ExplorerData {
circulatingNymSupplyData: {
circulating_supply: { denom: Denom; amount: string };
mixmining_reserve: { denom: Denom; amount: string };
total_supply: { denom: Denom; amount: string };
vesting_tokens: { denom: Denom; amount: string };
};
nymNodesData: {
gateways: {
bonded: { count: number; last_updated_utc: string };
blacklisted: { count: number; last_updated_utc: string };
historical: { count: number; last_updated_utc: string };
explorer: { count: number; last_updated_utc: string };
};
mixnodes: {
bonded: {
count: number;
active: number;
inactive: number;
reserve: number;
last_updated_utc: string;
};
blacklisted: {
count: number;
last_updated_utc: string;
};
historical: { count: number; last_updated_utc: string };
};
};
packetsAndStakingData: IPacketsAndStakingData[];
currentEpochRewardsData: {
interval: {
reward_pool: string;
staking_supply: string;
staking_supply_scale_factor: string;
epoch_reward_budget: string;
stake_saturation_point: string;
active_set_work_factor: string;
interval_pool_emission: string;
sybil_resistance: string;
};
rewarded_set: {
entry_gateways: number;
exit_gateways: number;
mixnodes: number;
standby: number;
};
};
}
export type NodeDescription = {
last_polled: string;
host_information: {
ip_address: string[];
hostname: string;
keys: {
ed25519: string;
x25519: string;
x25519_noise: string | null;
};
};
declared_role: {
mixnode: boolean;
entry: boolean;
exit_nr: boolean;
exit_ipr: boolean;
};
auxiliary_details: {
location: string;
announce_ports: {
verloc_port: number | null;
mix_port: number | null;
};
accepted_operator_terms_and_conditions: boolean;
};
build_information: {
binary_name: string;
build_timestamp: string;
build_version: string;
commit_sha: string;
commit_timestamp: string;
commit_branch: string;
rustc_version: string;
rustc_channel: string;
cargo_profile: string;
cargo_triple: string;
};
network_requester: {
address: string;
uses_exit_policy: boolean;
};
ip_packet_router: {
address: string;
};
authenticator: {
address: string;
};
wireguard: string | null;
mixnet_websockets: {
ws_port: number;
wss_port: number | null;
};
} | null;
export type BondInformation = {
node_id: number;
owner: string;
original_pledge: {
denom: string;
amount: string;
};
bonding_height: number;
is_unbonding: boolean;
node: {
host: string;
custom_http_port: number;
identity_key: string;
};
};
export type RewardingDetails = {
cost_params: {
profit_margin_percent: string;
interval_operating_cost: {
denom: string;
amount: string;
};
};
operator: string;
delegates: string;
total_unit_reward: string;
unit_delegation: string;
last_rewarded_epoch: number;
unique_delegations: number;
};
export type Location = {
two_letter_iso_country_code?: string;
three_letter_iso_country_code?: string;
country_name?: string;
latitude?: number;
longitude?: number;
};
export type NodeData = {
node_id: number;
contract_node_type: string;
description: NodeDescription;
bond_information: BondInformation;
rewarding_details: RewardingDetails;
location: Location;
};
// ACCOUNT BALANCES
export interface IRewardDetails {
amount_staked: IAmountDetails;
node_id: number;
node_still_fully_bonded: boolean;
rewards: IAmountDetails;
}
export interface IAmountDetails {
denom: string;
amount: string;
}
export interface IDelegationDetails {
node_id: number;
delegated: IAmountDetails;
height: number;
proxy: null | string;
}
export interface IAccountBalancesInfo {
accumulated_rewards: IRewardDetails[];
address: string;
balances: IAmountDetails[];
claimable_rewards: IAmountDetails;
delegations: IDelegationDetails[];
operator_rewards?: null | IAmountDetails;
total_delegations: IAmountDetails;
total_value: IAmountDetails;
vesting_account?: null | string;
}
export interface IObservatoryNode {
accepted_tnc: boolean;
bonded: boolean;
bonding_address: string;
description: {
authenticator: {
address: string;
};
auxiliary_details: {
accepted_operator_terms_and_conditions: boolean;
announce_ports: {
mix_port: number | null;
verloc_port: number | null;
};
location: string | null;
};
build_information: {
binary_name: string;
build_timestamp: string;
build_version: string;
cargo_profile: string;
cargo_triple: string;
commit_branch: string;
commit_sha: string;
commit_timestamp: string;
rustc_channel: string;
rustc_version: string;
};
declared_role: {
entry: boolean;
exit_ipr: boolean;
exit_nr: boolean;
mixnode: boolean;
};
host_information: {
hostname: string | null;
ip_address: string[];
};
keys: {
ed25519: string;
x25519: string;
x25519_noise: string | null;
};
ip_packet_router: {
address: string;
};
last_polled: string;
mixnet_websockets: {
ws_port: number;
wss_port: number | null;
};
network_requester: {
address: string;
uses_exit_policy: boolean;
};
wireguard: string | null;
geoip: {
city: string;
country: string;
ip_address: string;
loc: string;
node_id: number;
org: string;
postal: string;
region: string;
};
};
identity_key: string;
ip_address: string;
node_id: number;
node_type: string;
original_pledge: number;
rewarding_details: {
cost_params: {
interval_operating_cost: {
amount: string;
denom: string;
};
profit_margin_percent: string;
};
delegates: string;
last_rewarded_epoch: number;
operator: string;
total_unit_reward: string;
unique_delegations: number;
unit_delegation: string;
};
self_description: {
details: string;
moniker: string;
security_contact: string;
website: string;
};
total_stake: number;
uptime: number;
}
export interface NodeRewardDetails {
amount: {
amount: string;
denom: string;
};
cumulative_reward_ratio: string;
height: number;
node_id: number;
owner: string;
}
export type LastProbeResult = {
gateway: string;
outcome: {
as_entry: {
can_connect: boolean;
can_route: boolean;
};
as_exit: {
can_connect: boolean;
can_route_ip_external_v4: boolean;
can_route_ip_external_v6: boolean;
can_route_ip_v4: boolean;
can_route_ip_v6: boolean;
};
wg: {
can_handshake_v4: boolean;
can_handshake_v6: boolean;
can_register: boolean;
can_resolve_dns_v4: boolean;
can_resolve_dns_v6: boolean;
ping_hosts_performance_v4: number;
ping_hosts_performance_v6: number;
ping_ips_performance_v4: number;
ping_ips_performance_v6: number;
};
};
};
export type GatewayStatus = {
blacklisted: boolean;
bonded: boolean;
config_score: number;
description: {
details: string;
moniker: string;
security_contact: string;
website: string;
};
explorer_pretty_bond: {
identity_key: string;
location: {
latitude: number;
longitude: number;
two_letter_iso_country_code: string;
};
owner: string;
pledge_amount: {
amount: string;
denom: string;
};
};
gateway_identity_key: string;
last_probe_log: string;
last_probe_result: LastProbeResult; // Reference to the separate type
last_testrun_utc: string;
last_updated_utc: string;
performance: number;
routing_score: number;
self_described: {
authenticator: {
address: string;
};
auxiliary_details: {
accepted_operator_terms_and_conditions: boolean;
announce_ports: {
mix_port: number | null;
verloc_port: number | null;
};
location: string;
};
build_information: {
binary_name: string;
build_timestamp: string;
build_version: string;
cargo_profile: string;
cargo_triple: string;
};
declared_role: {
entry: boolean;
exit_ipr: boolean;
exit_nr: boolean;
mixnode: boolean;
};
host_information: {
hostname: string;
ip_address: string[];
keys: {
ed25519: string;
x25519: string;
x25519_noise: string | null;
};
};
ip_packet_router: {
address: string;
};
last_polled: string;
mixnet_websockets: {
ws_port: number;
wss_port: number | null;
};
network_requester: {
address: string;
uses_exit_policy: boolean;
};
wireguard: {
port: number;
public_key: string;
};
};
};
type BalanceDetails = {
amount: number;
denom: string;
};
export type ObservatoryRewards = {
operator_commissions: BalanceDetails;
staking_rewards: BalanceDetails;
unlocked: BalanceDetails;
};
export type ObservatoryBalance = {
delegated: BalanceDetails;
locked: BalanceDetails;
rewards: ObservatoryRewards;
self_bonded: BalanceDetails;
spendable: BalanceDetails;
};
export type Quote = {
ath_date: string;
ath_price: number;
market_cap: number;
market_cap_change_24h: number;
percent_change_12h: number;
percent_change_15m: number;
percent_change_1h: number;
percent_change_1y: number;
percent_change_24h: number;
percent_change_30d: number;
percent_change_30m: number;
percent_change_6h: number;
percent_change_7d: number;
percent_from_price_ath: number;
price: number;
volume_24h: number;
volume_24h_change_24h: number;
};
export type Quotes = {
USD: Quote;
};
export type NymTokenomics = {
beta_value: number;
first_data_at: string;
id: string;
last_updated: string;
max_supply: number;
name: string;
quotes: Quotes;
rank: number;
symbol: string;
total_supply: number;
};
+28
View File
@@ -0,0 +1,28 @@
export const NYM_NODES =
"https://explorer.nymtech.net/api/v1/tmp/unstable/nym-nodes";
export const HARBOURMASTER_API_MIXNODES_STATS =
"https://harbourmaster.nymtech.net/v2/mixnodes/stats";
export const NS_API_MIXNODES_STATS =
"https://staging-node-status-api.nymte.ch/v2/mixnodes/stats";
export const CURRENT_EPOCH =
"https://validator.nymtech.net/api/v1/epoch/current";
export const CURRENT_EPOCH_REWARDS =
"https://validator.nymtech.net/api/v1/epoch/reward_params";
export const NYM_NODE_BONDED =
"https://validator.nymtech.net/api/v1/nym-nodes/bonded";
export const NYM_ACCOUNT_ADDRESS =
"https://explorer.nymtech.net/api/v1/tmp/unstable/account";
export const NYM_PRICES_API = "https://api.nym.spectredao.net/api/v1/nym-price";
export const VALIDATOR_BASE_URL =
process.env.NEXT_PUBLIC_VALIDATOR_URL || "https://rpc.nymtech.net";
export const DATA_OBSERVATORY_NODES_URL =
"https://api.nym.spectredao.net/api/v1/nodes";
export const DATA_OBSERVATORY_NODES_DELEGATIONS_URL =
"https://api.nym.spectredao.net/api/v1/nodes";
export const DATA_OBSERVATORY_DELEGATIONS_URL =
"https://api.nym.spectredao.net/api/v1/delegations";
export const DATA_OBSERVATORY_BALANCES_URL =
"https://api.nym.spectredao.net/api/v1/balances";
export const OBSERVATORY_GATEWAYS_URL =
"https://mainnet-node-status-api.nymtech.cc/v2/gateways";
+1
View File
@@ -0,0 +1 @@
export const TABLET_WIDTH = "(min-width:700px)";
+27
View File
@@ -0,0 +1,27 @@
"use client";
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
import { Link } from "@/components/muiLink";
import { Button, Stack, Typography } from "@mui/material";
const ErrorPage = ({ error }: { error: Error }) => {
return (
<ContentLayout>
<Stack spacing={2} justifyContent="flex-start">
<Typography variant="body1">
An error occurred: {error.message}
</Typography>
<Typography variant="body2">
Please try again later or contact support
</Typography>
<Link href="/" underline="none">
<Button variant="contained" sx={{ maxWidth: 100 }} size="small">
Home
</Button>
</Link>
</Stack>
</ContentLayout>
);
};
export default ErrorPage;
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,34 @@
// API
import { client } from "../../../lib/strapiClient";
// Types
import type { Languages } from "../../../i18n";
import type { components } from "@/app/lib/strapi";
// Constants
import { footerApiPath } from "../../footer/config/constants";
// Fetch footer data
export const getFooter = async (
locale: Languages,
): Promise<{
id?: number;
attributes?: components["schemas"]["Footer"];
} | null> => {
const footer = await client.GET(footerApiPath, {
params: {
query: {
locale,
// @ts-expect-error - populate is not typed correctly?
populate: {
linkBlocks: {
populate: "*",
},
},
},
},
});
return footer?.data?.data ? footer?.data?.data : null;
};
@@ -0,0 +1,72 @@
// Types
import type { components } from "../../../lib/strapi";
// Components
import { Link } from "@/components/muiLink";
// MUI Components
import { Box, Grid2, Typography } from "@mui/material";
export const FooterLinks = ({
linkBlocks = [],
}: {
linkBlocks: components["schemas"]["FooterLinkBlockComponent"][];
}) => {
return (
<Grid2
container
spacing={{ xs: 2, md: 3 }}
columns={{ xs: 1, sm: 8, md: 5 }}
>
{linkBlocks?.map((block) => {
return (
<Grid2 key={block.id} size={{ xs: 1, sm: 4, md: 1 }}>
<Typography
component={block?.heading?.level || "h3"}
variant="subtitle1"
sx={{ mb: 4 }}
>
{block?.heading?.title}
</Typography>
<Box
component={"ul"}
sx={{
display: "flex",
flexDirection: "column",
gap: 2,
}}
>
{block?.links?.map((link) => {
const isLinkExternal = link.url?.startsWith("http");
return (
<Box
sx={{
listStyle: "none",
}}
component={"li"}
key={link.id}
>
<Link
href={link?.url || ""}
sx={{
textDecoration: "none",
"&:hover": {
textDecoration: "underline",
},
}}
>
<Typography variant="body3">
{link.title}
{isLinkExternal ? " ↗" : ""}
</Typography>
</Link>
</Box>
);
})}
</Box>
</Grid2>
);
})}
</Grid2>
);
};
@@ -0,0 +1 @@
export const footerApiPath = "/footer";
+68
View File
@@ -0,0 +1,68 @@
:root {
--max-width: 1120px;
--border-radius: 8px;
}
@media (prefers-color-scheme: dark) {
:root {
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
.MuiCardActionArea-focusHighlight {
background: transparent;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar {
right: 0;
width: 8px;
height: 8px;
}
::-webkit-scrollbar-thumb {
background-color: #aaa;
border-radius: 8px;
width: 8px;
height: 8px;
}
.reactMarkDownLink a {
color: #000000;
display: inline;
font-weight: 700;
}
.reactMarkDownList ul {
margin-left: 20px;
}
.reactMarkDownList ol {
margin-left: 20px;
}
+41
View File
@@ -0,0 +1,41 @@
import { createInstance } from "i18next";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next/initReactI18next";
import { getOptions } from "./settings";
import type { Languages } from "./types";
const initI18next = async ({ lng, ns }: { lng: Languages; ns?: string }) => {
const i18nInstance = createInstance();
await i18nInstance
.use(initReactI18next)
.use(
resourcesToBackend(
(language: string, namespace: string) =>
import(`./locales/${language}/${namespace}.json`),
),
)
.init(getOptions(lng, ns));
return i18nInstance;
};
export const useTranslation = async ({
lng,
ns,
options,
}: {
lng: Languages;
ns?: string;
options?: { keyPrefix: string };
}) => {
const i18nextInstance = await initI18next({ lng, ns });
return {
t: i18nextInstance.getFixedT(
lng,
Array.isArray(ns) ? ns[0] : ns,
options?.keyPrefix,
),
i18n: i18nextInstance,
};
};
export type { Languages, GeneralTranslations } from "./types";
@@ -0,0 +1,92 @@
{
"langSelectorLabel": "Language",
"socialLinks": [
{ "name": "Telegram", "link": "https://nymtech.net/go/telegram" },
{ "name": "Twitter", "link": "https://nymtech.net/go/x" },
{
"name": "Discord",
"link": "https://nymtech.net/go/discord"
},
{ "name": "GitHub", "link": "https://nymtech.net/go/github" },
{ "name": "YouTube", "link": "https://nymtech.net/go/youtube" }
],
"links": [
{
"title": "Get started",
"links": [
{
"name": "Download NymVPN",
"link": "download"
},
{
"name": "Test NymVPN",
"link": "alpha"
},
{
"name": "Become an operator",
"link": "https://nymtech.net/operators"
},
{ "name": "Visit nymtech.net", "link": "https://nymtech.net" },
{
"name": "Subscribe to Nyms newsletter",
"link": "https://eepurl.com/gdor_f"
}
]
},
{
"title": "Popular blog posts",
"links": [
{ "name": "Decentralized VPNs", "link": "blog/decentralized-vpns" },
{
"name": "Blockchain-based VPNs",
"link": "blog/blockchain-based-vpns-all-you-need-to-know"
},
{
"name": "Privacy protection with VPN",
"link": "blog/how-does-a-vpn-protect-you-and-your-privacy"
},
{
"name": "Tracking prevention with VPN",
"link": "blog/can-you-be-tracked-while-using-a-vpn"
},
{
"name": "Online privacy threats",
"link": "blog/internet-privacy-main-threats-and-protections"
}
]
},
{
"title": "Resources",
"links": [
{ "name": "NymVPN blog", "link": "blog" },
{
"name": "NymVPN public roadmap",
"link": "https://trello.com/b/qVhBo3e2/nymvpn-public-roadmap"
},
{
"name": "NymVPN localization",
"link": "https://weblate.nymte.ch/projects/nymvpn/"
}
]
},
{
"title": "Company",
"links": [
{ "name": "Contact", "link": "contact" },
{
"name": "Careers",
"link": "https://nym.teamtailor.com/"
},
{
"name": "Support",
"link": "https://support.nymvpn.com/hc/en-us"
},
{ "name": "Imprint", "link": "imprint" },
{ "name": "Privacy statements", "link": "privacy" },
{ "name": "Terms of use", "link": "terms" }
]
}
],
"trademarkText": "WireGuard is a registered trademark of Jason A. Donenfeld",
"rightsText": "© 2024 Nym Technologies S.A., all rights reserved"
}
@@ -0,0 +1,12 @@
{
"socialChannels": [
{ "name": "Telegram", "link": "https://nym.com/go/telegram" },
{ "name": "Twitter", "link": "https://nym.com/go/x" },
{
"name": "Discord",
"link": "https://nym.com/go/discord"
},
{ "name": "GitHub", "link": "https://nym.com/go/github" },
{ "name": "YouTube", "link": "https://nym.com/go/youtube" }
]
}
+16
View File
@@ -0,0 +1,16 @@
export const fallbackLng = "en";
export const languages = [fallbackLng, "es"] as const;
export const defaultNS = "translation";
export const cookieName = "i18next";
export function getOptions(lng = fallbackLng, ns = defaultNS) {
return {
// debug: true,
supportedLngs: languages,
fallbackLng,
lng,
fallbackNS: defaultNS,
defaultNS,
ns,
};
}
@@ -0,0 +1,46 @@
export interface RedeemFreePassCopy {
errors: {
invalidCode: string;
alreadyRedeemed: string;
unknown: string;
};
}
type GetStarted = {
title: string;
description: string;
ctaText: string;
inputLabel: string;
prompt: string;
};
export type AlphaPageSteps = {
title: string;
description: string;
};
export type AlphaPageTranslations = {
hero: {
title: {
firstLine: string;
secondLine?: string;
};
getCredential: GetStarted;
signUp: GetStarted;
imageAlt: string;
stepsTitle: string;
finalSteps: AlphaPageSteps[];
finalStepsAlert: string;
credentialsTitle: string;
credentialsButton: string;
};
redeemFreePass: RedeemFreePassCopy;
firstSection: {
title: string;
content: string[];
};
secondSection: {
title: string;
content: string[];
};
};
+13
View File
@@ -0,0 +1,13 @@
import type { languages } from "../settings";
export type Languages = (typeof languages)[number];
// They don't belong to any specific page, that's why I defined them here, but open to suggestions.
export type GeneralTranslations = {
alert: string;
copyToClipboard: {
copy: string;
copied: string;
copiedTimeLimitation: string;
};
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+30
View File
@@ -0,0 +1,30 @@
import { Header } from "@/components/header";
import { Wrapper } from "@/components/wrapper";
import Providers from "@/providers";
import type { Metadata } from "next";
import "./globals.css";
import "@interchain-ui/react/styles";
import { Footer } from "@/components/footer";
export const metadata: Metadata = {
title: "Nym Explorer V2",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<Providers>
<Header />
<Wrapper>{children}</Wrapper>
<Footer />
</Providers>
</body>
</html>
);
}
+23417
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
import createClient from "openapi-fetch";
import qs from "qs";
import type { paths } from "./strapi";
if (!process.env.NEXT_PUBLIC_CMS_API_URL) {
throw new Error(
"NEXT_PUBLIC_CMS_API_URL environment variable is not defined",
);
}
if (!process.env.NEXT_PUBLIC_CMS_API_NEXT_REVALIDATE) {
throw new Error(
"NEXT_PUBLIC_CMS_API_NEXT_REVALIDATE environment variable is not defined",
);
}
const client = createClient<paths>({
baseUrl: process.env.NEXT_PUBLIC_CMS_API_URL,
headers: {
Accept: "application/json",
},
fetch: (request: unknown) => {
const req = request as Request;
const url = new URL(req.url, process.env.NEXT_PUBLIC_CMS_API_URL);
return fetch(new Request(url, req), {
next: {
revalidate: Number(process.env.NEXT_PUBLIC_CMS_API_NEXT_REVALIDATE),
},
});
},
querySerializer(params) {
return qs.stringify(params, {
encodeValuesOnly: true,
});
},
});
export { client };
+67
View File
@@ -0,0 +1,67 @@
import { Stack, Typography } from "@mui/material";
import Grid from "@mui/material/Grid2";
import BlogArticlesCards from "../components/blogs/BlogArticleCards";
import { ContentLayout } from "../components/contentLayout/ContentLayout";
import SectionHeading from "../components/headings/SectionHeading";
import { CurrentEpochCard } from "../components/landingPageComponents/CurrentEpochCard";
import { NetworkStakeCard } from "../components/landingPageComponents/NetworkStakeCard";
import { NoiseCard } from "../components/landingPageComponents/NoiseCard";
import { RewardsCard } from "../components/landingPageComponents/StakersNumberCard";
import { TokenomicsCard } from "../components/landingPageComponents/TokenomicsCard";
import NodeTable from "../components/nodeTable/NodeTableWithAction";
import NodeAndAddressSearch from "../components/search/NodeAndAddressSearch";
export default async function Home() {
return (
<ContentLayout>
<Stack gap={5}>
<Typography variant="h1" textTransform={"uppercase"}>
Mixnet in your hands
</Typography>
<NodeAndAddressSearch />
</Stack>
<Grid container columnSpacing={5} rowSpacing={5}>
<Grid size={12}>
<SectionHeading title="Noise Generating Mixnet Overview" />
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<NoiseCard />
</Grid>
<Grid
container
columnSpacing={5}
rowSpacing={5}
size={{ xs: 12, sm: 6, md: 3 }}
>
<Grid size={12}>
<RewardsCard />
</Grid>
<Grid size={12}>
<CurrentEpochCard />
</Grid>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<NetworkStakeCard />
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<TokenomicsCard />
</Grid>
</Grid>
<Grid container>
<Grid size={12}>
<SectionHeading title="Nym Nodes" />
</Grid>
<Grid size={12}>
<NodeTable />
</Grid>
</Grid>
<Grid container columnSpacing={5} rowSpacing={5}>
<Grid size={12}>
<SectionHeading title="Onboarding" />
</Grid>
<BlogArticlesCards ids={[1, 2]} />
</Grid>
</ContentLayout>
);
}
@@ -0,0 +1,219 @@
"use client";
import { fetchAccountBalance, fetchNymPrice } from "@/app/api";
import { Skeleton, Stack, Typography } from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import type { IRewardDetails } from "../../app/api/types";
import ExplorerCard from "../cards/ExplorerCard";
import { AccountBalancesTable } from "./AccountBalancesTable";
export interface IAccontStatsRowProps {
type: string;
allocation: number;
amount: number;
value: number;
history?: { type: string; amount: number }[];
isLastRow?: boolean;
progressBarColor?: string;
}
interface IAccountBalancesCardProps {
address: string;
}
const getNymsFormated = (unyms: number): number => {
if (unyms === 0) {
return 0;
}
const balance = unyms / 1000000;
return balance;
};
const getPriceInUSD = (unyms: number, usdPrice: number): number => {
if (unyms === 0) {
return 0;
}
const balanceInUSD = (unyms / 1000000) * usdPrice;
const balanceFormated = Number(balanceInUSD.toFixed(2));
return balanceFormated;
};
const getAllocation = (unyms: number, totalUnyms: number): number => {
if (unyms === 0) {
return 0;
}
const allocationPercentage = (unyms * 100) / totalUnyms;
return Number(allocationPercentage.toFixed(2));
};
const calculateStakingRewards = (
accumulatedRewards: IRewardDetails[],
): number => {
if (accumulatedRewards.length > 0) {
const totalRewards = accumulatedRewards.reduce((total, rewardDetail) => {
return total + Number.parseFloat(rewardDetail.rewards.amount);
}, 0);
const result = getNymsFormated(totalRewards);
return result;
}
return 0;
};
export const AccountBalancesCard = (props: IAccountBalancesCardProps) => {
const { address } = props;
const {
data: accountInfo,
isLoading,
isError,
} = useQuery({
queryKey: ["accountBalance", address],
queryFn: () => fetchAccountBalance(address),
enabled: !!address,
});
const {
data: nymPrice,
isLoading: isLoadingPrice,
error: priceError,
} = useQuery({
queryKey: ["nymPrice"],
queryFn: fetchNymPrice,
});
if (isLoading || isLoadingPrice) {
return (
<ExplorerCard label="Total value">
<Stack gap={1}>
<Skeleton variant="text" height={38} />
<Skeleton variant="text" height={380} />
</Stack>
</ExplorerCard>
);
}
if (isError || priceError || !accountInfo || !nymPrice) {
return (
<ExplorerCard label="Total value">
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
Failed to account data.
</Typography>
<Skeleton variant="text" height={238} />
</ExplorerCard>
);
}
const nymPriceData = nymPrice.quotes.USD.price;
const totalBalanceUSD = getPriceInUSD(
Number(accountInfo.total_value.amount),
nymPriceData,
);
const spendableNYM =
accountInfo.balances.length > 0
? getNymsFormated(Number(accountInfo.balances[0].amount))
: 0;
const spendableUSD =
accountInfo.balances.length > 0
? getPriceInUSD(Number(accountInfo.balances[0].amount), nymPriceData)
: 0;
const spendableAllocation =
accountInfo.balances.length > 0
? getAllocation(
Number(accountInfo.balances[0].amount),
Number(accountInfo.total_value.amount),
)
: 0;
const delegationsNYM = getNymsFormated(
Number(accountInfo.total_delegations.amount),
);
const delegationsUSD = getPriceInUSD(
Number(accountInfo.total_delegations.amount),
nymPriceData,
);
const delegationsAllocation = getAllocation(
Number(accountInfo.total_delegations.amount),
Number(accountInfo.total_value.amount),
);
const operatorRewardsAllocation = getAllocation(
Number(accountInfo.operator_rewards?.amount || 0),
Number(accountInfo.total_value.amount),
);
const operatorRewardsNYM = getNymsFormated(
Number(accountInfo.operator_rewards?.amount || 0),
);
const operatorRewardsUSD = getPriceInUSD(
Number(accountInfo.operator_rewards?.amount || 0),
nymPriceData,
);
const claimableNYM = getNymsFormated(
Number(accountInfo.claimable_rewards.amount),
);
const claimableUSD = getPriceInUSD(
Number(accountInfo.claimable_rewards.amount),
nymPriceData,
);
const claimableAllocation = getAllocation(
Number(accountInfo.claimable_rewards.amount),
Number(accountInfo.total_value.amount),
);
const stakingRewards =
accountInfo.accumulated_rewards.length > 0
? calculateStakingRewards(accountInfo.accumulated_rewards)
: 0;
const tableRows = [
{
type: "Spendable",
allocation: spendableAllocation,
amount: spendableNYM,
value: spendableUSD,
},
{
type: "Delegated",
allocation: delegationsAllocation,
amount: delegationsNYM,
value: delegationsUSD,
// history: [
// { type: "Liquid", amount: 6900 },
// { type: "Locked", amount: 6900 },
// ],
},
{
type: "Claimable",
allocation: claimableAllocation,
amount: claimableNYM,
value: claimableUSD,
history: [
// { type: "Unlocked", amount: 6900 },
{
type: "Staking rewards",
amount: stakingRewards,
},
{ type: "Operator comission", amount: 0 },
],
},
{
type: "Operator Rewards",
allocation: operatorRewardsAllocation,
amount: operatorRewardsNYM,
value: operatorRewardsUSD,
},
];
return (
<ExplorerCard
label="Total value"
title={`$ ${totalBalanceUSD}`}
sx={{ height: "100%" }}
>
<AccountBalancesTable rows={tableRows} />
</ExplorerCard>
);
};
@@ -0,0 +1,321 @@
"use client";
import CircleIcon from "@mui/icons-material/Circle";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Typography from "@mui/material/Typography";
import useMediaQuery from "@mui/material/useMediaQuery";
import * as React from "react";
import { TABLET_WIDTH } from "../../app/constants";
import { MultiSegmentProgressBar } from "../progressBars/MultiSegmentProgressBar";
import { StaticProgressBar } from "../progressBars/StaticProgressBar";
export interface IAccontStatsRowProps {
type: string;
allocation: number;
amount: number;
value: number;
history?: { type: string; amount: number }[];
isLastRow?: boolean;
progressBarColor?: string;
}
const progressBarColours = [
"#BEF885",
"#7FB0FF",
"#00D17D",
"#004650",
"#FEECB3",
];
const Row = (props: IAccontStatsRowProps) => {
const tablet = useMediaQuery(TABLET_WIDTH);
const {
type,
allocation,
amount,
value,
history,
isLastRow,
progressBarColor,
} = props;
const [open, setOpen] = React.useState(false);
return (
<React.Fragment>
{/* Main Row */}
{tablet ? (
<TableRow>
<TableCell
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "25%",
}}
>
<Typography variant="body4" sx={{ color: "pine.950" }}>
{type}
</Typography>
</TableCell>
<TableCell
align="right"
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "25%",
}}
>
<Box>
<Typography variant="body4" sx={{ color: "pine.950" }}>
{allocation}%
</Typography>
<StaticProgressBar
value={allocation}
color={progressBarColor || "green"}
/>
</Box>
</TableCell>
<TableCell
align="right"
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "20%",
}}
>
<Typography variant="body4" sx={{ color: "pine.950" }}>
{amount.toFixed(4)} NYM
</Typography>
</TableCell>
<TableCell
align="right"
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "20%",
}}
>
<Typography
variant="subtitle2"
sx={{ color: "pine.950", fontWeight: 700 }}
>
${value}
</Typography>
</TableCell>
<TableCell
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "10%",
}}
>
{history && (
<IconButton
aria-label="expand row"
size="small"
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
)}
</TableCell>
</TableRow>
) : (
// MOBILE VIEW
<TableRow>
<TableCell
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "45%",
}}
>
<Box display={"flex"} gap={1} alignItems={"center"}>
<CircleIcon sx={{ color: progressBarColor, fontSize: 12 }} />
{type}
</Box>
</TableCell>
<TableCell
align="right"
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "45%",
}}
>
<Typography>{amount.toFixed(4)} NYM</Typography>
<Typography>$ {value}</Typography>
</TableCell>
<TableCell
sx={{
borderBottom: isLastRow
? "none"
: "1px solid rgba(224, 224, 224, 1)",
width: "10%",
}}
>
{history && (
<IconButton
aria-label="expand row"
size="small"
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
)}
</TableCell>
</TableRow>
)}
{/* History Rows */}
{history &&
open &&
history.map((historyRow) => (
<TableRow key={historyRow.type}>
<TableCell
sx={{
display: "flex",
alignItems: "center",
pl: 5,
borderBottom: "none", // Explicitly remove border
}}
>
<Typography variant="body4" sx={{ color: "pine.950" }}>
<span style={{ marginRight: 8 }}></span>
{historyRow.type}
</Typography>
</TableCell>
{tablet && (
<TableCell
sx={{
borderBottom: "none", // Explicitly remove border
}}
/>
)}
<TableCell
align="right"
sx={{
borderBottom: "none", // Explicitly remove border
}}
>
<Typography variant="body4" sx={{ color: "pine.950" }}>
{historyRow.amount.toFixed(4)} NYM
</Typography>
</TableCell>
<TableCell
sx={{
borderBottom: "none", // Explicitly remove border
}}
/>
</TableRow>
))}
</React.Fragment>
);
};
export interface IAccountBalancesTableProps {
rows: Array<IAccontStatsRowProps>;
}
export const AccountBalancesTable = (props: IAccountBalancesTableProps) => {
const { rows } = props;
const tablet = useMediaQuery(TABLET_WIDTH);
const progressBarPercentages = () => {
return rows.map((row) => row.allocation);
};
const getProgressValues = () => {
const percentages = progressBarPercentages();
const result: Array<{ percentage: number; color: string }> = [];
percentages.map((value, i) => {
result.push({
percentage: value,
color: progressBarColours[i],
});
});
return result;
};
const progressValues = getProgressValues();
return (
<Box>
{!tablet && <MultiSegmentProgressBar values={progressValues} />}
<TableContainer>
<Table aria-label="collapsible table" sx={{ marginBottom: 3 }}>
<TableHead>
{tablet ? (
<TableRow>
<TableCell>
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
Type
</Typography>
</TableCell>
<TableCell align="right">
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
Allocation
</Typography>
</TableCell>
<TableCell align="right">
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
Amount
</Typography>
</TableCell>
<TableCell align="right">
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
Value
</Typography>
</TableCell>
<TableCell />
</TableRow>
) : (
<TableRow>
<TableCell>
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
Type
</Typography>
</TableCell>
<TableCell align="right">
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
Amount / Value
</Typography>
</TableCell>
<TableCell />
</TableRow>
)}
</TableHead>
<TableBody>
{rows.map((row, i) => (
<Row
key={row.type}
{...row}
isLastRow={i === rows.length - 1}
progressBarColor={progressBarColours[i]}
/>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
);
};
@@ -0,0 +1,84 @@
"use client";
import { fetchAccountBalance } from "@/app/api";
import { Box, Skeleton, Stack, Typography } from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import ExplorerCard from "../cards/ExplorerCard";
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
import ExplorerListItem from "../list/ListItem";
import { CardQRCode } from "../qrCode/QrCode";
interface IAccountInfoCardProps {
address: string;
}
export const AccountInfoCard = (props: IAccountInfoCardProps) => {
const { address } = props;
const { data, isLoading, isError } = useQuery({
queryKey: ["accountBalance", address],
queryFn: () => fetchAccountBalance(address),
enabled: !!address,
});
if (isLoading) {
return (
<ExplorerCard label="Total NYM">
<Stack gap={1}>
<Skeleton variant="text" height={38} />
<Skeleton variant="rectangular" height={128} width={128} />
<Skeleton variant="text" height={300} />
</Stack>
</ExplorerCard>
);
}
if (isError || !data) {
return (
<ExplorerCard label="Total NYM">
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
Failed to account data.
</Typography>
<Skeleton variant="text" height={238} />
</ExplorerCard>
);
}
const balance =
data.balances.length > 0 ? Number(data.total_value.amount) / 1000000 : 0;
const balanceFormated = `${balance.toFixed(4)} NYM`;
return (
<ExplorerCard
label="Total NYM"
title={balanceFormated}
sx={{ height: "100%" }}
>
<Stack gap={5}>
<Box display={"flex"} justifyContent={"flex-start"}>
<CardQRCode url={data.address} />
</Box>
<ExplorerListItem
label="Address"
value={
<Stack
direction="row"
gap={0.1}
alignItems="center"
justifyContent="space-between"
width="100%"
>
<Typography
variant="body4"
sx={{ wordWrap: "break-word", maxWidth: "85%" }}
>
{data.address}
</Typography>
<CopyToClipboard text={data.address} />
</Stack>
}
/>
</Stack>
</ExplorerCard>
);
};
@@ -0,0 +1,86 @@
import fs from "node:fs/promises";
import path from "node:path";
import Grid from "@mui/material/Grid2";
import ExplorerHeroCard from "../cards/ExplorerHeroCard";
import type { BlogArticleWithLink } from "./types";
import { icons, IconName } from "@/utils/getIconByName";
// TODO: Articles should be sorted by date
const BlogArticlesCards = async ({
limit,
ids,
}: {
limit?: number;
ids?: Array<number>;
}) => {
const blogsDir = path.join(process.cwd(), "/src/data");
const blogsDirFilenames = await fs.readdir(blogsDir);
// Read all blog articles from the data directory
const blogArticles: BlogArticleWithLink[] = await Promise.all(
blogsDirFilenames.map(async (filename) => {
const filePath = path.join(blogsDir, filename);
const fileContent = await fs.readFile(filePath, "utf-8");
const blogArticle = JSON.parse(fileContent);
return {
...blogArticle,
link: `/onboarding/${filename.replace(".json", "")}`,
};
}),
);
const limitedOrFilteredBlogArticles = (
blogArticles: BlogArticleWithLink[],
limit?: number,
ids?: number[],
): BlogArticleWithLink[] => {
let filteredArticles = blogArticles;
// Filter by IDs if provided
if (ids && ids.length > 0) {
filteredArticles = filteredArticles.filter((article) =>
ids.includes(article.id),
);
}
// Apply limit if provided
if (limit) {
filteredArticles = filteredArticles.slice(0, limit);
}
return filteredArticles;
};
const articles = limitedOrFilteredBlogArticles(blogArticles, limit, ids);
return articles
.sort((a, b) => {
// sort by date
return (
new Date(b.attributes.date).getTime() -
new Date(a.attributes.date).getTime()
);
})
.map((blogArticle) => {
return (
<Grid
size={{
sm: 12,
md: 6,
}}
key={blogArticle.title}
>
<ExplorerHeroCard
label={blogArticle.label}
title={blogArticle.title}
description={blogArticle.description}
icon={icons[blogArticle.icon as IconName]?.src}
link={blogArticle.link || ""}
sx={{ height: "100%" }}
/>
</Grid>
);
});
};
export default BlogArticlesCards;
@@ -0,0 +1,37 @@
import { Card, CardContent, CardHeader, Typography } from "@mui/material";
import { Link } from "../muiLink";
const TableOfContents = ({
headings,
}: {
headings: { id: string; heading: string }[];
}) => {
return (
<Card
elevation={0}
sx={{
width: "100%",
display: {
xs: "none",
md: "block",
},
p: 4,
position: "sticky",
top: 50,
}}
>
<CardHeader title="Table of contents" />
<CardContent>
{headings.map((heading) => (
<Link href={`#${heading.id}`} key={heading.id}>
<Typography variant="body2" sx={{ mb: 3 }}>
{heading.heading}
</Typography>
</Link>
))}
</CardContent>
</Card>
);
};
export default TableOfContents;
+29
View File
@@ -0,0 +1,29 @@
type Content = { type: string; text: string };
type BlogArticle = {
id: number;
title: string;
label: string;
description: string;
image: string;
icon: string;
attributes: {
blogAuthors: string[];
date: Date;
readingTime: number;
};
overview: {
content: Content[];
};
sections: {
id: string;
heading: string;
text: Content[];
}[];
};
export type BlogArticleWithLink = BlogArticle & {
link: string;
};
export default BlogArticle;
@@ -0,0 +1,70 @@
import BreadcrumbsMUI from "@mui/material/Breadcrumbs";
import Typography from "@mui/material/Typography";
import Link from "next/link";
interface BreadcrumbComponentProps {
items: BreadcrumbItemType[];
}
interface BreadcrumbItemType {
label: string;
href?: string;
isCurrentPage?: boolean;
}
export const Breadcrumbs = ({ items }: BreadcrumbComponentProps) => {
return (
<BreadcrumbsMUI
sx={{
color: "primary.main",
display: "flex",
margin: "0",
padding: "0",
alignItems: "center",
}}
separator={
<Typography
sx={{ display: "flex", height: "100%" }}
variant="subtitle3"
>
/
</Typography>
}
aria-label="breadcrumb"
>
{items.map((item) => {
// Check if it's the current page
if (item.isCurrentPage) {
return (
<Typography
sx={{
display: "flex",
}}
variant="subtitle3"
key={item.label}
>
{item.label}
</Typography>
);
}
// If it's not the current page, render a clickable link
return (
<Link key={item.label} href={item.href || "#"} passHref>
<Typography
sx={{
display: "flex",
"&:hover": {
textDecoration: "underline",
},
}}
variant="subtitle3"
>
{item.label}
</Typography>
</Link>
);
})}
</BreadcrumbsMUI>
);
};
+124
View File
@@ -0,0 +1,124 @@
"use client";
import { Button as MUIButton } from "@mui/material";
import CircularProgress from "@mui/material/CircularProgress";
import Typography from "@mui/material/Typography";
import type { Theme } from "@mui/material/styles";
import Link from "next/link";
import { colours } from "../../theme/colours";
import type { ButtonProps } from "./types";
export const Button = ({
text,
handleClick,
goToLink,
isSuccess,
isDisabled,
isTextButton,
isSmall,
isXtraSmall,
isFullWidth,
isSecondary,
isContrast,
sx,
buttonId,
type,
isLoading,
startIcon,
endIcon,
}: ButtonProps) => {
const onClick = () => {
if (goToLink) {
} else if (handleClick) {
handleClick();
}
};
const fontColor = (t: Theme) => {
if (isTextButton) return "primary.main";
if (isSecondary)
return isContrast ? "common.white" : "primary.contrastText";
return t.palette.mode === "light"
? t.palette.common.white
: t.palette.common.black;
};
const bgColor = (t: Theme) => {
if (isTextButton || isSecondary) return "transparent";
if (isDisabled)
return t.palette.mode === "light" ? colours.pine[200] : colours.pine[800];
return isSuccess ? "success.main" : "primary.main";
};
const hoverBgColor = () => {
if (isTextButton || isSecondary) return "transparent";
return isSuccess
? colours.green[500]
: colours.pine[isDisabled ? 200 : 950];
};
const padding = isTextButton
? "none"
: isXtraSmall
? "10px 23px"
: isSmall
? "11px 23px"
: "15px 23px";
const CustomButtom = (
<MUIButton
type={type}
sx={{
textDecoration: "none",
textAlign: "center",
p: padding,
borderRadius: 2,
width: isFullWidth ? "100%" : "fit-content",
height: isSmall ? 48 : 64,
color: (t) => fontColor(t),
letterSpacing: 0.45,
backgroundColor: (t) => bgColor(t),
border: isSecondary
? (t) =>
isDisabled
? `1px solid ${t.palette.primary.main}`
: `1px solid ${t.palette.primary.contrastText}`
: "1px solid transparent",
"&:hover": {
backgroundColor: hoverBgColor,
},
"&:disabled": {
color: "text.disabled",
},
...sx,
}}
disabled={isDisabled}
onClick={onClick}
id={buttonId}
startIcon={startIcon}
endIcon={endIcon}
>
{!isLoading ? (
<Typography
variant={isSmall ? "body1" : isXtraSmall ? "body2" : "button"}
sx={{
textTransform: "none",
fontWeight: "inherit",
letterSpacing: 0,
}}
>
{text}
</Typography>
) : (
<CircularProgress
size={isSmall ? 20 : 24}
sx={{ color: (t) => fontColor(t) }}
/>
)}
</MUIButton>
);
return goToLink ? (
<Link href={goToLink.path} target={goToLink.target}>
{CustomButtom}
</Link>
) : (
CustomButtom
);
};
@@ -0,0 +1,31 @@
import type { SxProps } from "@mui/system"; // Asegúrate de importar SxProps si estás usando @mui/system
export type CommonButtonProps = {
text: string;
isSuccess?: boolean;
isDisabled?: boolean;
isTextButton?: boolean;
isSmall?: boolean;
isXtraSmall?: boolean;
isFullWidth?: boolean;
isSecondary?: boolean;
isContrast?: boolean;
sx?: SxProps;
isLoading?: boolean;
buttonId?: string;
type?: "button" | "submit" | "reset";
startIcon?: React.ReactNode;
endIcon?: React.ReactNode;
};
export type ButtonWithClick = CommonButtonProps & {
handleClick: (() => void) | (() => Promise<void>) | (() => null);
goToLink?: never;
};
export type ButtonWithLink = CommonButtonProps & {
goToLink: { path: string; target: "_self" | "_blank" | "_parent" | "_top" };
handleClick?: never;
};
export type ButtonProps = ButtonWithClick | ButtonWithLink;
@@ -0,0 +1,63 @@
import Button, {
type ButtonProps as MUIButtonProps,
} from "@mui/material/Button";
import Link from "next/link";
import type React from "react";
interface ButtonOrLinkProps extends Omit<MUIButtonProps, "variant"> {
href?: string;
variant?: "outlined" | "contained" | "text";
isSelected?: boolean;
}
export const ButtonOrLink: React.FC<ButtonOrLinkProps> = ({
href,
variant = "contained",
isSelected,
children,
...props
}) => {
const selectedStyle = {
contained: {
outline: "1px solid",
outlineColor: "primary.main",
borderColor: "primary.main",
},
outlined: {
borderStyle: "solid",
backgroundColor: "primary.main",
color: "background.main",
"&:hover": {
backgroundColor: "primary.main",
},
},
text: {
color: "primary.main",
},
};
return href ? (
<Button
sx={{
...(isSelected && selectedStyle[variant]),
textAlign: "center",
}}
component={Link}
href={href}
variant={variant}
{...props}
>
{children}
</Button>
) : (
<Button
sx={{
...(isSelected && selectedStyle[variant]),
textAlign: "center",
}}
variant={variant}
{...props}
>
{children}
</Button>
);
};
@@ -0,0 +1,42 @@
import { Card, CardContent, CardHeader, type SxProps } from "@mui/material";
const cardStyles = {
p: 2,
};
const cardTitleStyles: SxProps = {
variant: "h5",
color: "pine.600",
letterSpacing: 0.7,
};
const cardSubtitleStyles: SxProps = {
variant: "h2",
mt: 3,
color: "pine.400",
};
const ExplorerCard = ({
label,
title,
children,
sx,
}: {
label: string | React.ReactNode;
title?: string;
children: React.ReactNode;
sx?: SxProps;
}) => {
return (
<Card elevation={0} sx={{ ...cardStyles, ...sx }}>
<CardHeader
title={label}
titleTypographyProps={cardTitleStyles}
subheader={title}
subheaderTypographyProps={cardSubtitleStyles}
/>
<CardContent>{children}</CardContent>
</Card>
);
};
export default ExplorerCard;
@@ -0,0 +1,71 @@
import {
Card,
CardContent,
CardHeader,
Stack,
type SxProps,
Typography,
} from "@mui/material";
import Image from "next/image";
import ArrowUpRight from "../../components/icons/ArrowUpRight";
import { Link } from "../muiLink";
const cardStyles = {
p: 3,
cursor: "pointer",
"&:hover": {
bgcolor: "accent.main",
},
};
const cardContentStyles = {
mt: 10,
};
const titleStyles = {
letterSpacing: 0.7,
};
const ExplorerHeroCard = ({
title,
label,
description,
icon,
link,
sx,
}: {
title: string;
label: string;
description: string;
icon: string;
link: string;
sx?: SxProps;
}) => {
return (
<Link href={link} sx={{ textDecoration: "none", height: "100%" }}>
<Card sx={{ ...cardStyles, ...sx }} elevation={0}>
<CardHeader
title={
<Stack direction="row" justifyContent="space-between">
<Typography variant="body4" sx={titleStyles}>
{label}
</Typography>
<ArrowUpRight />
</Stack>
}
/>
<CardContent sx={cardContentStyles}>
<Stack spacing={4}>
<Image
src={icon}
alt={"explorer-blog-image"}
width={84}
height={84}
/>
<Typography variant="h2">{title}</Typography>
<Typography variant="body3">{description}</Typography>
</Stack>
</CardContent>
</Card>
</Link>
);
};
export default ExplorerHeroCard;
@@ -0,0 +1,18 @@
"use client";
import { useIsClient } from "@uidotdev/usehooks";
/**
* Hack to work around next.js hydration
* @see https://github.com/uidotdev/usehooks/issues/218
*/
import type React from "react";
type ClientOnlyProps = {
children: React.ReactNode;
};
export const ClientOnly: React.FC<ClientOnlyProps> = ({ children }) => {
const isClient = useIsClient();
// Render children if on client side, otherwise return null
return isClient ? <>{children}</> : null;
};
@@ -0,0 +1,82 @@
"use client";
import { Box } from "@mui/material";
import Script from "next/script";
import { useEffect } from "react";
declare global {
interface Window {
remark_config: {
host: string;
site_id: string;
components: string[];
max_shown_comments: number;
theme: string;
locale: string;
show_email_subscription: boolean;
simple_view: boolean;
no_footer: boolean;
};
REMARK42: {
createInstance: (config: typeof window.remark_config) => void;
changeTheme: (theme: "light" | "dark") => void;
};
}
}
export const Remark42Comments = () => {
useEffect(() => {
if (typeof window !== "undefined") {
// Set Remark42 configuration on the window object
window.remark_config = {
host: "https://remark42.nymte.ch",
site_id: "remark",
components: ["embed", "last-comments"],
max_shown_comments: 100,
theme: "light",
locale: "en",
show_email_subscription: false,
simple_view: true,
no_footer: true,
};
// Dynamically load the Remark42 script if it doesn't exist
if (!document.getElementById("remark42-script")) {
const script = document.createElement("script");
script.src = `${window.remark_config.host}/web/embed.js`;
script.async = true;
script.defer = true;
script.id = "remark42-script";
document.body.appendChild(script);
} else if (window.REMARK42) {
// Re-initialize if the script is already loaded
window.REMARK42.createInstance(window.remark_config);
}
}
}, []);
// React to mode changes and update Remark42 theme
// useEffect(() => {
// if (window.REMARK42 && window.REMARK42.changeTheme) {
// window.REMARK42.changeTheme(mode === "dark" ? "dark" : "light");
// }
// }, [mode]);
return (
<Box>
<div id="remark42" className="remark" />
<Script
id="remark-init"
strategy="afterInteractive"
// biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>
dangerouslySetInnerHTML={{
__html: `
if (window.REMARK42) {
window.REMARK42.createInstance(window.remark_config);
}
`,
}}
/>
</Box>
);
};
@@ -0,0 +1,25 @@
import { Box, type BoxProps } from "@mui/material";
export const ContentLayout = ({
children,
className,
sx,
...rest
}: BoxProps) => {
return (
<Box
component={"main"}
sx={{
display: "flex",
flexDirection: "column",
gap: { xs: 3, md: 10 },
py: { xs: 3, md: 10 },
...sx,
}}
className={className}
{...rest}
>
{children}
</Box>
);
};
@@ -0,0 +1,47 @@
"use client";
import { IconButton, Typography } from "@mui/material";
import { useCopyToClipboard } from "@uidotdev/usehooks";
import { useEffect } from "react";
import CopyFile from "../icons/CopyFile";
const CLEAR_AFTER_MS = 10_000;
const CopyToClipboard = ({
text,
Icon,
size = "medium",
}: {
text: string;
Icon?: React.ReactNode;
size?: "small" | "medium" | "large";
}) => {
const [copiedText, setCopiedText] = useCopyToClipboard();
const hasCopied = Boolean(copiedText);
useEffect(() => {
if (hasCopied) {
const timeout = setTimeout(() => {
setCopiedText("");
}, CLEAR_AFTER_MS);
return () => clearTimeout(timeout);
}
}, [hasCopied, setCopiedText]);
if (hasCopied) {
return (
<Typography sx={{ color: "pine.950" }} variant="h6" color="textSecondary">
Copied
</Typography>
);
}
return (
<IconButton size={size} onClick={() => setCopiedText(text)}>
{Icon || <CopyFile />}
</IconButton>
);
};
export default CopyToClipboard;
@@ -0,0 +1,22 @@
import { Stack, Typography } from "@mui/material";
import Flag from "react-world-flags";
interface ICountryFlag {
countryCode: string;
countryName?: string | JSX.Element;
}
const CountryFlag = ({ countryCode, countryName }: ICountryFlag) => {
return (
<Stack direction="row" gap={1}>
<Flag code={countryCode} width="19" />
<Typography variant="h6" sx={{ color: "pine.950" }}>
{countryName}
</Typography>
</Stack>
);
};
export default CountryFlag;
export type { ICountryFlag };
@@ -0,0 +1,76 @@
"use client";
import {
type EpochResponseData,
useEpochContext,
} from "@/providers/EpochProvider";
import { AccessTime } from "@mui/icons-material";
import { Stack, Typography } from "@mui/material";
import { differenceInMinutes } from "date-fns";
import { useCallback, useEffect, useState } from "react";
const calculateMinutesRemaining = (epochEndTime: string) => {
const endDate = new Date(epochEndTime);
const difference = differenceInMinutes(endDate, new Date());
return difference;
};
const NextEpochTime = () => {
const { data, isLoading, isError, epochStatus } = useEpochContext();
const [minutesRemaining, setMinutesRemaining] = useState(0);
const updateState = useCallback((data: EpochResponseData) => {
if (!data) return;
const minutesRemaining = calculateMinutesRemaining(data.current_epoch_end);
setMinutesRemaining(minutesRemaining);
}, []);
useEffect(() => {
updateState(data);
const interval = setInterval(() => {
updateState(data);
}, 30_000);
return () => clearInterval(interval);
}, [data, updateState]);
if (isLoading) {
return (
<Stack direction="row" spacing={1}>
<AccessTime />
<Typography variant="h5" fontWeight="light">
Loading next mixnet epoch...
</Typography>
</Stack>
);
}
if (isError || !data) {
return (
<Stack direction="row" spacing={1}>
<AccessTime />
<Typography variant="h5" fontWeight="light">
Failed to load next epoch.
</Typography>
</Stack>
);
}
return (
<Stack direction="row" spacing={1}>
<AccessTime />
{epochStatus === "pending" ? (
<Typography variant="h5" fontWeight="light">
Waiting for next mixnet epoch to start...
</Typography>
) : (
<Typography variant="h5" fontWeight="light">
Next mixnet epoch starts in: {minutesRemaining} min
</Typography>
)}
</Stack>
);
};
export default NextEpochTime;
@@ -0,0 +1,56 @@
import {
FavoriteBorder as FavoriteBorderIcon,
Favorite as FavoriteIcon,
} from "@mui/icons-material";
import { IconButton } from "@mui/material";
import { useLocalStorage } from "@uidotdev/usehooks";
const Favorite = ({ address }: { address: string }) => {
const [favorites, saveFavorites] = useLocalStorage<string[]>(
"nym-node-favorites",
[],
);
const onFavorite = (address: string) => {
saveFavorites([...favorites, address]);
};
if (favorites.includes(address)) {
return <UnFavorite address={address} />;
}
return (
<IconButton
onClick={(e) => {
e.stopPropagation();
onFavorite(address);
}}
>
<FavoriteBorderIcon sx={{ color: "accent.main" }} />
</IconButton>
);
};
const UnFavorite = ({ address }: { address: string }) => {
const [favorites, saveFavorites] = useLocalStorage<string[]>(
"nym-node-favorites",
[],
);
const handleUnfavorite = (address: string) => {
saveFavorites(favorites.filter((favorite) => favorite !== address));
};
return (
<IconButton
onClick={(e) => {
e.stopPropagation();
handleUnfavorite(address);
}}
>
<FavoriteIcon sx={{ color: "accent.main" }} />
</IconButton>
);
};
export { Favorite, UnFavorite };
@@ -0,0 +1,28 @@
// Imports
import { Link as MuiLink, type LinkProps as MuiLinkProps } from "@mui/material";
import NextLink, { type LinkProps as NextLinkProps } from "next/link";
import type { ReactNode } from "react";
interface CustomLinkProps extends NextLinkProps, Omit<MuiLinkProps, "href"> {
href: string;
children: ReactNode;
}
// Component definition
export const Link = ({ href, children, ...props }: CustomLinkProps) => {
return (
<MuiLink
component={NextLink}
href={href}
{...props}
sx={{
display: "flex",
alignItems: "center",
...(props.sx || {}),
}}
>
{children}
</MuiLink>
);
};
@@ -0,0 +1,63 @@
"use client";
// Types
import { icons } from "@/utils/getIconByName";
// MUI Components
import { Box, Button, Input } from "@mui/material";
import Image from "next/image";
export const NewsletterSignUp = () => {
return (
<Box
sx={{
display: "flex",
gap: "10px",
justifyContent: "space-between",
alignItems: "center",
border: "1px solid",
borderColor: "background.main",
padding: "3px 4px 3px 20px",
borderRadius: "54px",
boxShadow: "2px 2px 4px 0px rgba(0, 0, 0, 0.25) inset",
"&:focus-within": {
border: "1px solid #000",
},
":has(input:error)": {
border: "1px solid red",
},
backgroundColor: "base.white",
flexGrow: 1,
maxWidth: "400px",
}}
>
<Input
placeholder="Enter your email address"
type="email"
required
disableUnderline={true}
sx={{
width: "100%",
position: "relative",
top: "1px",
}}
/>
<Button
sx={{
width: "47px",
height: "47px",
backgroundColor: "accent.main",
borderRadius: "50%",
minWidth: "47px",
color: "background.main",
padding: "15px",
"&:hover": {
backgroundColor: "accent.main",
color: "background.main",
},
}}
>
<Image src={icons.arrow} alt={"arrow icon"} width={15} height={15} />
</Button>
</Box>
);
};
@@ -0,0 +1,40 @@
"use client";
import { Box } from "@mui/material";
import socialChannels from "../../app/i18n/locales/en/social-channels.json";
import { Link } from "./MuiLink";
import { SocialIcon } from "./SocialIcon";
export const SocialChannels = () => {
return (
<Box
sx={{
display: "flex",
gap: "30px",
}}
>
{socialChannels.socialChannels.map(
(channel: { name: string; link: string }) => {
return (
<Link
key={channel.name}
href={channel?.link}
sx={{
width: "32px",
height: "32px",
color: "background.main",
backgroundColor: "light.main",
"&:hover": {
backgroundColor: "background.default",
},
borderRadius: "50%",
}}
>
<SocialIcon channel={channel?.name} />
</Link>
);
},
)}
</Box>
);
};
@@ -0,0 +1,21 @@
"use client";
import Image from "next/image";
import { icons } from "@/utils/getIconByName";
// import { useMainContext } from "@/context";
export const SocialIcon = ({ channel }: { channel: string }): JSX.Element => {
// const { mode } = useMainContext();
const modeType = "light";
// ✅ Normalize channel name & apply theme logic
const normalizedChannel = channel.toLowerCase();
const iconName =
modeType === "light" && normalizedChannel === "youtube"
? "youTubeInverted"
: (normalizedChannel as keyof typeof icons);
// ✅ Get the icon source dynamically
const iconSrc = icons[iconName]?.src || "/icons/default-icon.svg";
return <Image src={iconSrc} alt={channel} width={32} height={32} />;
};
@@ -0,0 +1,21 @@
import { Box } from "@mui/material";
import type { BoxProps } from "@mui/system";
export type WrapperProps = BoxProps;
export function Wrapper({ children, sx, ...props }: WrapperProps) {
return (
<Box
sx={{
mx: "auto",
maxWidth: "1378px",
width: "100%",
px: "30px",
...sx,
}}
{...props}
>
{children}
</Box>
);
}
@@ -0,0 +1,81 @@
// Types
import type { components } from "../../app/lib/strapi";
// Components
import { Link } from "./MuiLink";
// MUI Components
import { Box, Typography } from "@mui/material";
import Grid2 from "@mui/material/Grid2";
export const FooterLinks = ({
linkBlocks = [],
}: {
linkBlocks: components["schemas"]["FooterLinkBlockComponent"][];
}) => {
return (
<Grid2
container
spacing={{ xs: 2, md: 3 }}
columns={{ xs: 1, sm: 8, md: 5 }}
>
{linkBlocks?.map((block) => {
return (
<Grid2 key={block.id} size={{ xs: 1, sm: 4, md: 1 }}>
<Typography
component={block?.heading?.level || "h3"}
variant="subtitle1"
sx={{ mb: 4 }}
>
{block?.heading?.title}
</Typography>
<Box
component={"ul"}
sx={{
display: "flex",
flexDirection: "column",
gap: 2,
}}
>
{block?.links?.map((link) => {
const isLinkExternal = link.url?.startsWith("http");
return (
<Box
sx={{
listStyle: "none",
}}
component={"li"}
key={link.id}
>
<Link
href={
isLinkExternal
? (link.url ?? "/")
: link.url
? `https://nym.com${link.url}`
: "/"
}
target="_blank"
rel="noopener noreferrer"
sx={{
textDecoration: "none",
"&:hover": {
textDecoration: "underline",
},
}}
>
<Typography variant="body3">
{link.title}
{isLinkExternal ? " ↗" : ""}
</Typography>
</Link>
</Box>
);
})}
</Box>
</Grid2>
);
})}
</Grid2>
);
};
+155
View File
@@ -0,0 +1,155 @@
// API
import { SocialChannels } from "./SocialChannels";
// Components
import { Wrapper } from "./Wrapper";
// MUI Components
import { Box, Typography } from "@mui/material";
import { getFooter } from "../../app/features/footer/api/getFooter";
import { Link } from "./MuiLink";
// import { NewsletterSignUp } from "./NewsLetterSignUp";
import { FooterLinks } from "./footer-links";
export async function Footer() {
const locale = "en";
const footerData = await getFooter(locale);
const legalContent1 =
"Nym Noise Generating Mixnet Explorer, V 2.1.0 Public Beta release.";
const legalContent2 = footerData?.attributes?.legalContent2 || false;
const footerLinkBlocks = footerData?.attributes?.linkBlocks || [];
const links = [
{ id: 1, title: "Imprint", url: "https://nym.com/imprint" },
{
id: 2,
title: "nym.com Privacy statement",
url: "https://nym.com/nym-com-privacy-statement",
},
{ id: 3, title: "Nym Docs", url: "https://nym.com/docs" },
{
id: 4,
title: "Nym Operators and Validators Terms",
url: "https://nym.com/operators-validators-terms",
},
];
return (
<>
<Box
component={"footer"}
sx={{
py: 10,
backgroundColor: "medium.main",
}}
>
<Wrapper>
<Box
sx={{
borderBottom: "1px solid",
borderColor: "background.default",
pb: 7.5,
mb: 7.5,
display: { md: "flex" },
flexDirection: { xs: "column", md: "row" },
justifyContent: { xs: "center", md: "space-between" },
alignItems: "center",
gap: 2,
}}
>
{/* <Box
sx={{
display: "flex",
flexDirection: { xs: "column", md: "row" },
alignItems: "center",
gap: 2,
flexGrow: 1,
justifyContent: { xs: "center", md: "flex-start" },
pb: { xs: 2, md: 0 },
}}
>
<Typography variant="h2">Nym Newsletter</Typography>
<NewsletterSignUp />
</Box> */}
<Box
sx={{
display: "flex",
flexDirection: { xs: "column", md: "row" },
justifyContent: { xs: "center", md: "space-between" },
alignItems: "center",
gap: 2,
}}
>
<Box
sx={{
display: "flex",
justifyContent: { xs: "center", md: "flex-start" },
}}
>
<SocialChannels />
</Box>
</Box>
</Box>
<FooterLinks linkBlocks={footerLinkBlocks} />
{/* Hardcoded links */}
<Box
sx={{
display: "flex",
flexDirection: { xs: "column", md: "row" },
gap: { xs: 3, md: 5 },
mt: 9,
mb: 6,
}}
>
{links.map((link) => {
return (
<Box
sx={{
listStyle: "none",
}}
key={link.id}
>
<Link
href={link.url}
target="_blank"
rel="noopener noreferrer"
sx={{
textDecoration: "none",
"&:hover": {
textDecoration: "underline",
},
}}
>
<Typography variant="body5">{link.title}</Typography>
</Link>
</Box>
);
})}
</Box>
<Box
sx={{
py: 5,
display: "flex",
flexDirection: { xs: "column", md: "row" },
gap: { xs: 2, md: 0 },
justifyContent: "space-between",
textAlign: { xs: "center", md: "left" },
}}
>
{legalContent1 ? (
<Typography variant="subtitle3">{legalContent1}</Typography>
) : null}
{legalContent2 ? (
<Typography variant="subtitle3">{legalContent2}</Typography>
) : null}
</Box>
</Wrapper>
</Box>
</>
);
}
@@ -0,0 +1,57 @@
import { Box, Divider } from "@mui/material";
import NymLogo from "../../components/icons/NymLogo";
import { Link } from "../../components/muiLink";
import { Wrapper } from "../../components/wrapper";
import ConnectWallet from "../wallet/ConnectWallet";
import HeaderItem from "./HeaderItem";
import MENU_DATA from "./menuItems";
export const DesktopHeader = () => {
return (
<Box
sx={{
display: { xs: "none", lg: "block" },
height: "115px",
alignItems: "center",
}}
>
<Wrapper
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "42px",
height: "100%",
}}
>
<Link
href={"/"}
style={{
display: "flex",
alignItems: "center",
width: "100px",
aspectRatio: "89/25",
}}
>
<NymLogo />
</Link>
<Box
sx={{
display: "flex",
flexGrow: 1,
alignItems: "center",
justifyContent: "start",
height: "100%",
gap: 5,
}}
>
{MENU_DATA.map((menu) => (
<HeaderItem key={menu.id} menu={menu} />
))}
</Box>
<ConnectWallet size="small" />
</Wrapper>
<Divider variant="fullWidth" sx={{ width: "100%" }} />
</Box>
);
};
@@ -0,0 +1,31 @@
"use client";
import { Circle } from "@mui/icons-material";
import { Button, Stack } from "@mui/material";
import Link from "next/link";
import { usePathname } from "next/navigation";
import type { MenuItem } from "./menuItems";
type HeaderItemProps = {
menu: MenuItem;
};
const HeaderItem = ({ menu }: HeaderItemProps) => {
const pathname = usePathname();
return (
<Stack direction="row" gap={2} key={menu.id} alignItems="center">
{pathname.includes(menu.url) && <Circle sx={{ fontSize: 10 }} />}
<Link href={menu.url} passHref>
<Button
sx={{
padding: 0,
}}
>
{menu.title}
</Button>
</Link>
</Stack>
);
};
export default HeaderItem;
@@ -0,0 +1,155 @@
"use client";
import { Close as CloseIcon, Menu as MenuIcon } from "@mui/icons-material";
import { Box, Drawer, IconButton, Typography } from "@mui/material";
import { useState } from "react";
import { Link } from "../../components/muiLink";
import { Wrapper } from "../../components/wrapper";
import NymLogo from "../icons/NymLogo";
import ConnectWallet from "../wallet/ConnectWallet";
import MENU_DATA from "./menuItems";
export const MobileHeader = () => {
const [drawerOpen, setDrawerOpen] = useState(false);
// Mobile menu handlers
const toggleDrawer = (open: boolean) => {
setDrawerOpen(open);
};
return (
<>
<Box sx={{ display: { xs: "block", lg: "none" } }}>
<MobileMenuHeader toggleDrawer={toggleDrawer} drawerOpen={drawerOpen} />
<Drawer
anchor="left"
open={drawerOpen}
onClose={() => toggleDrawer(false)}
sx={{
display: { xs: "block", lg: "none" },
}}
PaperProps={{
sx: {
width: "100%",
height: "100%",
bgcolor: "base.white",
overflow: "hidden",
position: "relative",
color: "background.main",
},
}}
>
<MobileMenuHeader
toggleDrawer={toggleDrawer}
drawerOpen={drawerOpen}
/>
{/* Sliding Animation */}
<Box
sx={{
display: "flex",
width: "200%",
height: "100%",
transition: "transform 0.3s ease-in-out",
position: "relative",
transform: "translateX(0%)",
}}
>
{/* Main Menu */}
<Box sx={{ width: "50%", height: "100%" }}>
{MENU_DATA.map((menu) => (
<Box key={menu.title} sx={{ marginBottom: 3 }}>
<Link
onClick={() => toggleDrawer(false)}
href={menu.url || ""}
target={menu?.url?.startsWith("http") ? "_blank" : "_self"}
sx={{
display: "flex",
width: "100%",
padding: 3.75,
color: "background.main",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
gap: 1.25,
}}
>
<Box
sx={{
display: "block",
width: "10px",
height: "10px",
borderRadius: "100%",
bgcolor: "primary.main",
}}
/>
<Typography color="primary" variant="h4">
{menu.title}
</Typography>
</Box>
</Link>
</Box>
))}
</Box>
</Box>
</Drawer>
</Box>
</>
);
};
const MobileMenuHeader = ({
toggleDrawer,
drawerOpen,
}: {
toggleDrawer: (open: boolean) => void;
drawerOpen: boolean;
}) => {
return (
<Wrapper
sx={{
backgroundColor: "background.default",
}}
>
<Box
sx={{
height: "115px",
alignItems: "center",
display: "flex",
justifyContent: "space-between",
}}
>
<Link
onClick={() => toggleDrawer(false)}
href={"/"}
style={{
display: "flex",
alignItems: "center",
width: "100px",
aspectRatio: "89/25",
}}
>
<NymLogo />
</Link>
<Box
sx={{
display: "flex",
gap: 2.5,
alignItems: "center",
}}
>
<IconButton sx={{}} onClick={() => toggleDrawer(!drawerOpen)}>
{drawerOpen ? <CloseIcon /> : <MenuIcon />}
</IconButton>
</Box>
</Box>
{!drawerOpen && <ConnectWallet size="small" />}
<Box height={40} />
</Wrapper>
);
};
@@ -0,0 +1,12 @@
import { DesktopHeader } from "./DesktopHeader";
import { MobileHeader } from "./MobileHeader";
export const Header = async () => {
return (
<header>
<DesktopHeader />
<MobileHeader />
{/* Mobile header will go here */}
</header>
);
};
@@ -0,0 +1,25 @@
export type MenuItem = {
id: number;
title: string;
url: string;
};
const MENU_DATA: MenuItem[] = [
{
id: 1,
title: "Explorer",
url: "/table",
},
{
id: 2,
title: "Stake",
url: "/stake",
},
{
id: 3,
title: "Onboarding",
url: "/onboarding",
},
];
export default MENU_DATA;
@@ -0,0 +1,6 @@
import { icons } from "@/utils/getIconByName";
export const NavigationIcons = {
article: icons.document,
download: icons.download,
};

Some files were not shown because too many files have changed in this diff Show More