Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39c36d3415 | |||
| 287eca5421 | |||
| d1ac4f1cfb | |||
| f2e98f4f16 | |||
| 7c09295e43 | |||
| 09fd8f6d9d | |||
| f69c6707c7 | |||
| 223f008bee | |||
| c2086eef9e | |||
| bc849c7c46 | |||
| f94f3b1a1d | |||
| c3bcb96bad | |||
| 9b6816e265 | |||
| 68223000ea | |||
| 9f5e153ddb | |||
| cf87ced120 | |||
| 7560837f01 | |||
| 16b9bcb91d | |||
| 07e946a195 | |||
| 648223a235 |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals"]
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
@@ -25,8 +29,8 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
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
|
||||
|
||||
@@ -18,7 +18,7 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
|
||||
|
||||
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/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
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
|
||||
|
||||
@@ -27,10 +27,10 @@ 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!
|
||||
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/deployment) for more details.
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"$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
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -1,30 +1,34 @@
|
||||
{
|
||||
"name": "@nymproject/network-explorer",
|
||||
"version": "1.0.0",
|
||||
"name": "@nymproject/explorer_v2",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"build:prod": "yarn --cwd .. build && next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "biome check --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nymproject/react": "^1.0.0",
|
||||
"@nymproject/nym-validator-client": "0.18.0",
|
||||
"next": "14.1.4",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"material-react-table": "^2.12.1",
|
||||
"@mui/x-date-pickers": "7.1.1",
|
||||
"@mui/x-data-grid": "7.1.1"
|
||||
"@emotion/cache": "^11.13.5",
|
||||
"@emotion/react": "^11.13.5",
|
||||
"@emotion/styled": "^11.13.5",
|
||||
"@mui/material": "^6.1.10",
|
||||
"@mui/material-nextjs": "^6.1.9",
|
||||
"next": "15.0.3",
|
||||
"react": "19.0.0-rc-66855b96-20241106",
|
||||
"react-dom": "19.0.0-rc-66855b96-20241106",
|
||||
"react-world-flags": "^1.6.0",
|
||||
"qrcode.react": "^4.1.0",
|
||||
"@nivo/line": "^0.88.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.1.4",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"lefthook": "^1.8.5",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 3.7 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 629 B After Width: | Height: | Size: 128 B |
@@ -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,245 @@
|
||||
import {
|
||||
CIRCULATING_NYM_SUPPLY,
|
||||
CURRENT_EPOCH,
|
||||
CURRENT_EPOCH_REWARDS,
|
||||
HARBOURMASTER_API_MIXNODES_STATS,
|
||||
HARBOURMASTER_API_SUMMARY,
|
||||
} from "./urls";
|
||||
|
||||
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 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[];
|
||||
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;
|
||||
};
|
||||
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 interface ExplorerCache {
|
||||
data?: ExplorerData;
|
||||
lastUpdated?: Date;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// Extend the global object with our custom property
|
||||
let explorerCache: ExplorerCache | undefined;
|
||||
}
|
||||
|
||||
const CACHE_TIME_SECONDS = 60 * 5; // 5 minutes
|
||||
|
||||
const getExplorerData = async () => {
|
||||
// FETCH NYMNODES
|
||||
const fetchNymNodes = await fetch(HARBOURMASTER_API_SUMMARY, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
// FETCH CURRENT EPOCH
|
||||
const fetchCurrentEpoch = await fetch(CURRENT_EPOCH, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
// FETCH CURRENT EPOCH REWARDS
|
||||
const fetchCurrentEpochRewards = await fetch(CURRENT_EPOCH_REWARDS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
// FETCH CIRCULATING NYM SUPPLY
|
||||
const fetchCirculatingNymSupply = await fetch(CIRCULATING_NYM_SUPPLY, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
// FETCH PACKETS AND STAKING
|
||||
const fetchPacketsAndStaking = await fetch(HARBOURMASTER_API_MIXNODES_STATS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
const [
|
||||
circulatingNymSupplyRes,
|
||||
nymNodesRes,
|
||||
packetsAndStakingRes,
|
||||
currentEpochRes,
|
||||
currentEpochRewardsRes,
|
||||
] = await Promise.all([
|
||||
fetchCirculatingNymSupply,
|
||||
fetchNymNodes,
|
||||
fetchPacketsAndStaking,
|
||||
fetchCurrentEpoch,
|
||||
fetchCurrentEpochRewards,
|
||||
]);
|
||||
|
||||
const [
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
currentEpochData,
|
||||
currentEpochRewardsData,
|
||||
] = await Promise.all([
|
||||
circulatingNymSupplyRes.json(),
|
||||
nymNodesRes.json(),
|
||||
packetsAndStakingRes.json(),
|
||||
currentEpochRes.json(),
|
||||
currentEpochRewardsRes.json(),
|
||||
]);
|
||||
|
||||
return [
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
currentEpochData,
|
||||
currentEpochRewardsData,
|
||||
];
|
||||
};
|
||||
|
||||
export async function ensureCacheExists() {
|
||||
// makes sure the cache exists in global memory
|
||||
let doUpdate = false;
|
||||
const now = new Date();
|
||||
if (!global.explorerCache) {
|
||||
global.explorerCache = {};
|
||||
doUpdate = true;
|
||||
}
|
||||
if (
|
||||
global.explorerCache.lastUpdated &&
|
||||
now.getDate() - global.explorerCache.lastUpdated.getDate() >
|
||||
CACHE_TIME_SECONDS
|
||||
) {
|
||||
doUpdate = true;
|
||||
}
|
||||
|
||||
// if the cache has expired or never existed, get it from API's
|
||||
if (doUpdate) {
|
||||
const [
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
currentEpochData,
|
||||
currentEpochRewardsData,
|
||||
] = await getExplorerData();
|
||||
|
||||
packetsAndStakingData.pop();
|
||||
|
||||
global.explorerCache.data = {
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
currentEpochData,
|
||||
currentEpochRewardsData,
|
||||
};
|
||||
global.explorerCache.lastUpdated = now;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCacheExplorerData() {
|
||||
await ensureCacheExists();
|
||||
|
||||
if (!global.explorerCache?.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log("global.explorerCache.data :>> ", global.explorerCache.data);
|
||||
return global.explorerCache.data || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a custom API route that returns metadata from Strapi about images: height, width, strapi download url.
|
||||
*
|
||||
* The response from Strapi is cached in memory for CACHE_TIME_SECONDS.
|
||||
*/
|
||||
// export default async function handler(
|
||||
// req: NextApiRequest,
|
||||
// res: NextApiResponse
|
||||
// ) {
|
||||
// // return cached data
|
||||
// const data = await getCacheExplorerData();
|
||||
// if (data) {
|
||||
// res.status(200).json(data);
|
||||
// res.end();
|
||||
// }
|
||||
|
||||
// // catch-all
|
||||
// res.status(404).end();
|
||||
// }
|
||||
@@ -0,0 +1,18 @@
|
||||
export const HARBOURMASTER_API_SUMMARY =
|
||||
"https://harbourmaster.nymtech.net/v2/summary";
|
||||
export const EXPLORER_API = "https://explorer.nymtech.net/api/v1/countries";
|
||||
export const VALIDATOR_API_SUPPLY =
|
||||
"https://validator.nymtech.net/api/v1/circulating-supply";
|
||||
export const COSMOS_API =
|
||||
"https://api.nymtech.net/cosmos/bank/v1beta1/balances/n1a53udazy8ayufvy0s434pfwjcedzqv34yg485t";
|
||||
export const VALIDATOR_API_EPOCH =
|
||||
"https://validator.nymtech.net/api/v1/epoch/reward_params";
|
||||
export const HARBOURMASTER_API_MIXNODES_STATS =
|
||||
"https://harbourmaster.nymtech.net/v2/mixnodes/stats";
|
||||
export const HARBOURMASTER_API_BASE = "https://harbourmaster.nymtech.net";
|
||||
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 CIRCULATING_NYM_SUPPLY =
|
||||
"https://validator.nymtech.net/api/v1/circulating-supply";
|
||||
@@ -0,0 +1 @@
|
||||
export const TABLET_WIDTH = "(min-width:700px)";
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
export default function ExplorerPage() {
|
||||
return (
|
||||
<div>
|
||||
<main>
|
||||
<Box sx={{ p: 5 }}>
|
||||
<Wrapper>
|
||||
<Typography fontWeight="light">Explorer page</Typography>
|
||||
</Wrapper>
|
||||
</Box>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,37 @@
|
||||
: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;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
.MuiCardActionArea-focusHighlight {
|
||||
background: transparent;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Header } from "@/components/header";
|
||||
import ThemeProvider from "@/providers/ThemeProvider";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<ThemeProvider>
|
||||
<Header />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
export default function OnboardingPage() {
|
||||
return (
|
||||
<div>
|
||||
<main>
|
||||
<Box sx={{ p: 5 }}>
|
||||
<Wrapper>
|
||||
<Typography fontWeight="light">Onboarding page</Typography>
|
||||
</Wrapper>
|
||||
</Box>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
AccountStatsCard,
|
||||
type IAccountStatsCardProps,
|
||||
} from "@/components/Cards/AccountStatsCard";
|
||||
import ExplorerCard from "@/components/Cards/ExplorerCard";
|
||||
import ExplorerHeroCard from "@/components/Cards/ExplorerHeroCard";
|
||||
import ExplorerListItem from "@/components/List/ListItem";
|
||||
import ProgressBar from "@/components/RatingMeter/RatingMeter";
|
||||
import StarRarating from "@/components/StarRating/StarRating";
|
||||
import TwoSidedSwitch from "@/components/TwoSidedButtonSwitch";
|
||||
import CopyFile from "@/components/icons/CopyFile";
|
||||
import Gateway from "@/components/icons/Gateway";
|
||||
import { CurrentEpochCard } from "@/components/landingPageComponents/CurrentEpochCard";
|
||||
import { NetworkStakeCard } from "@/components/landingPageComponents/NetworkStakeCard";
|
||||
import { NoiseCard } from "@/components/landingPageComponents/NoiseCard";
|
||||
import { RewardsCard } from "@/components/landingPageComponents/RewardsCard";
|
||||
import { TokenomicsCard } from "@/components/landingPageComponents/TokenomicsCard";
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import { Container, Grid2, IconButton, Stack, Typography } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { type ContentCardProps, MonoCard } from "../components/cards/MonoCard";
|
||||
import { type ExplorerData, getCacheExplorerData } from "./api";
|
||||
|
||||
const explorerCard: ContentCardProps = {
|
||||
overTitle: "SINGLE",
|
||||
profileImage: {},
|
||||
title: "SINGLE",
|
||||
profileCountry: {
|
||||
countryCode: "NO",
|
||||
countryName: "Norway",
|
||||
},
|
||||
upDownLine: {
|
||||
percentage: 10,
|
||||
numberWentUp: true,
|
||||
},
|
||||
titlePrice: {
|
||||
price: 1.15,
|
||||
upDownLine: {
|
||||
percentage: 10,
|
||||
numberWentUp: true,
|
||||
},
|
||||
},
|
||||
dataRows: {
|
||||
rows: [
|
||||
{ key: "Market cap", value: "$ 1000000" },
|
||||
{ key: "24H VOL", value: "$ 1000000" },
|
||||
],
|
||||
},
|
||||
graph: {
|
||||
data: [
|
||||
{
|
||||
date_utc: "2024-11-20",
|
||||
numericData: 10,
|
||||
},
|
||||
{
|
||||
date_utc: "2024-11-21",
|
||||
numericData: 12,
|
||||
},
|
||||
{
|
||||
date_utc: "2024-11-22",
|
||||
numericData: 9,
|
||||
},
|
||||
{
|
||||
date_utc: "2024-11-23",
|
||||
numericData: 11,
|
||||
},
|
||||
],
|
||||
color: "#00CA33",
|
||||
label: "Label",
|
||||
},
|
||||
nymAddress: {
|
||||
address: "n1w7tfthyfkhh3au3mqpy294p4dk65dzal2h04su",
|
||||
title: "Nym address",
|
||||
},
|
||||
identityKey: {
|
||||
address: "n1w7tfthyfkhh3au3mqpy294p4dk65dzal2h04su",
|
||||
title: "Nym address",
|
||||
},
|
||||
qrCode: {
|
||||
url: "https://nymtech.net",
|
||||
},
|
||||
ratings: {
|
||||
ratings: [
|
||||
{ title: "Rating-1", numberOfStars: 4 },
|
||||
{ title: "Rating-2", numberOfStars: 2 },
|
||||
{ title: "Rating-3", numberOfStars: 3 },
|
||||
],
|
||||
},
|
||||
progressBar: {
|
||||
overTitle: "Current NGM epoch",
|
||||
start: "2024-12-08T12:26:19Z",
|
||||
showEpoch: true,
|
||||
},
|
||||
comments: true,
|
||||
paragraph: "Additional line",
|
||||
stakeButton: {
|
||||
label: "Stake on node",
|
||||
identityKey: "n1w7tfthyfkhh3au3mqpy294p4dk65dzal2h04su",
|
||||
},
|
||||
};
|
||||
|
||||
const accountStatsCard: IAccountStatsCardProps = {
|
||||
overTitle: "Total value",
|
||||
priceTitle: 1990.0174,
|
||||
rows: [
|
||||
{ type: "Spendable", allocation: 15.53, amount: 12800, value: 1200 },
|
||||
{
|
||||
type: "Delegated",
|
||||
allocation: 15.53,
|
||||
amount: 12800,
|
||||
value: 1200,
|
||||
history: [
|
||||
{ type: "Liquid", amount: 6900 },
|
||||
{ type: "Locked", amount: 6900 },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "Claimable",
|
||||
allocation: 15.53,
|
||||
amount: 12800,
|
||||
value: 1200,
|
||||
history: [
|
||||
{ type: "Unlocked", amount: 6900 },
|
||||
{ type: "Staking rewards", amount: 6900 },
|
||||
{ type: "Operator comission", amount: 6900 },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "Self bonded",
|
||||
allocation: 15.53,
|
||||
amount: 12800,
|
||||
value: 1200,
|
||||
},
|
||||
{
|
||||
type: "Locked",
|
||||
allocation: 15.53,
|
||||
amount: 12800,
|
||||
value: 1200,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
const [explorerData, setExplorerData] = useState<ExplorerData>();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const data = await getCacheExplorerData();
|
||||
setExplorerData(data);
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<main>
|
||||
<Wrapper>
|
||||
<Container maxWidth="lg">
|
||||
<Stack spacing={4}>
|
||||
<ExplorerCard title="Explorer Card" subtitle="Cryptosailors">
|
||||
<ExplorerListItem
|
||||
label="Identity Key"
|
||||
value="n1w7tfthyfkhh3au3mqpy294p4dk65dzal2h04su"
|
||||
/>
|
||||
<ExplorerListItem
|
||||
label="Nym Address"
|
||||
value={
|
||||
<Stack direction="row" gap={0.1} alignItems="center">
|
||||
<Typography variant="body4">
|
||||
n1w7tfthyfkhh3au3mqpy294p4dk65dzal2h04su
|
||||
</Typography>
|
||||
<IconButton size="small">
|
||||
<CopyFile />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
label="Star Rating"
|
||||
value={<StarRarating value={3} />}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
label="Progress bar"
|
||||
value={<ProgressBar value={50} color="secondary" />}
|
||||
/>
|
||||
</ExplorerCard>
|
||||
<Typography variant="h1" textTransform={"uppercase"} mb={5}>
|
||||
Mixnet in your hands
|
||||
</Typography>
|
||||
<Grid
|
||||
container
|
||||
rowSpacing={3}
|
||||
columnSpacing={2}
|
||||
mb={2}
|
||||
alignItems="stretch"
|
||||
>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
{explorerData && <NoiseCard explorerData={explorerData} />}
|
||||
</Grid>
|
||||
<Grid container rowSpacing={3} size={{ xs: 12, md: 3 }}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<RewardsCard />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
{explorerData && (
|
||||
<CurrentEpochCard explorerData={explorerData} />
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3 }} height={"100%"}>
|
||||
{explorerData && (
|
||||
<NetworkStakeCard explorerData={explorerData} />
|
||||
)}
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<TokenomicsCard />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container gap={2} alignItems={"flex-start"}>
|
||||
<Grid size={{ xs: 12, md: 5 }}>
|
||||
<MonoCard {...explorerCard} />
|
||||
</Grid>
|
||||
<Grid container size={{ xs: 6 }}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TwoSidedSwitch
|
||||
leftLabel="Account"
|
||||
rightLabel="Mixnode"
|
||||
// onSwitch={() => console.log("object :>> ")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<AccountStatsCard {...accountStatsCard} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid2 container spacing={4}>
|
||||
<Grid2 size={6}>
|
||||
<ExplorerHeroCard
|
||||
label="Onboarding"
|
||||
title="How to select Nym vpn gateway?"
|
||||
description="Stake your tokens to well performing mix nodes, and earn a share of operator rewards!"
|
||||
image={<Gateway />}
|
||||
link={"/onboarding"}
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2 size={6}>
|
||||
<ExplorerHeroCard
|
||||
label="Onboarding"
|
||||
title="How to select Nym vpn gateway?"
|
||||
description="Stake your tokens to well performing mix nodes, and earn a share of operator rewards!"
|
||||
image={<Gateway />}
|
||||
link={"/onboarding"}
|
||||
/>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Wrapper>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
export default function OnboardingPage() {
|
||||
return (
|
||||
<div>
|
||||
<main>
|
||||
<Box sx={{ p: 5 }}>
|
||||
<Wrapper>
|
||||
<Typography fontWeight="light">Stake page</Typography>
|
||||
</Wrapper>
|
||||
</Box>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export const formatBigNum = (num: number) => {
|
||||
if (typeof num === "number") {
|
||||
if (num >= 1000000000) {
|
||||
return `${(num / 1000000000).toFixed(1).replace(/\.0$/, "")}B`;
|
||||
}
|
||||
if (num >= 1000000) {
|
||||
return `${(num / 1000000).toFixed(1).replace(/\.0$/, "")}M`;
|
||||
}
|
||||
if (num >= 1000) {
|
||||
return `${(num / 1000).toFixed(1).replace(/\.0$/, "")}K`;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,308 @@
|
||||
"use client";
|
||||
|
||||
import { TABLET_WIDTH } from "@/app/constants";
|
||||
import CircleIcon from "@mui/icons-material/Circle";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import { Card, CardContent } from "@mui/material";
|
||||
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 { 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>{type}</Typography>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "25%",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography>{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%",
|
||||
}}
|
||||
>
|
||||
{amount} NYM
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "20%",
|
||||
}}
|
||||
>
|
||||
$ {value}
|
||||
</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="small" />
|
||||
{type}
|
||||
</Box>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "45%",
|
||||
}}
|
||||
>
|
||||
<Typography>{amount} 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
|
||||
}}
|
||||
>
|
||||
<span style={{ marginRight: 8 }}>•</span>
|
||||
{historyRow.type}
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: "none", // Explicitly remove border
|
||||
}}
|
||||
>
|
||||
{historyRow.amount}
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: "none", // Explicitly remove border
|
||||
}}
|
||||
>
|
||||
{/* Any additional content */}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export interface IAccountStatsCardProps {
|
||||
rows: Array<IAccontStatsRowProps>;
|
||||
overTitle?: string;
|
||||
priceTitle?: number;
|
||||
}
|
||||
|
||||
export const AccountStatsCard = (props: IAccountStatsCardProps) => {
|
||||
const { rows, overTitle, priceTitle } = 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 (
|
||||
<Card sx={{ height: "100%", borderRadius: "unset" }}>
|
||||
<CardContent>
|
||||
{overTitle && (
|
||||
<Typography fontSize={14} mb={3} textTransform={"uppercase"}>
|
||||
{overTitle}
|
||||
</Typography>
|
||||
)}
|
||||
{priceTitle && (
|
||||
<Typography fontSize={24} mb={3}>
|
||||
${priceTitle}
|
||||
</Typography>
|
||||
)}
|
||||
{!tablet && <MultiSegmentProgressBar values={progressValues} />}
|
||||
<TableContainer>
|
||||
<Table aria-label="collapsible table" sx={{ marginBottom: 3 }}>
|
||||
<TableHead>
|
||||
{tablet ? (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography textTransform={"uppercase"}>Type</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography textTransform={"uppercase"}>
|
||||
Allocation
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography textTransform={"uppercase"}>Amount</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography textTransform={"uppercase"}>Value</Typography>
|
||||
</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography textTransform={"uppercase"}>Type</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography textTransform={"uppercase"}>
|
||||
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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Card, CardContent, CardHeader, type SxProps } from "@mui/material";
|
||||
|
||||
const cardStyles = {
|
||||
p: 3,
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "stretch",
|
||||
flexGrow: 1,
|
||||
};
|
||||
|
||||
const cardTitleStyles: SxProps = {
|
||||
variant: "h5",
|
||||
color: "pine.600",
|
||||
letterSpacing: 0.7,
|
||||
};
|
||||
const cardSubtitleStyles: SxProps = {
|
||||
variant: "h3",
|
||||
mt: 3,
|
||||
color: "pine.400",
|
||||
};
|
||||
|
||||
const ExplorerCard = ({
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<Card elevation={0} sx={cardStyles}>
|
||||
<CardHeader
|
||||
title={title}
|
||||
titleTypographyProps={cardTitleStyles}
|
||||
subheader={subtitle}
|
||||
subheaderTypographyProps={cardSubtitleStyles}
|
||||
sx={{ padding: 0 }}
|
||||
/>
|
||||
<CardContent sx={{ padding: 0 }}>{children}</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExplorerCard;
|
||||
@@ -0,0 +1,62 @@
|
||||
import ArrowUpRight from "@/components/icons/ArrowUpRight";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
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,
|
||||
image,
|
||||
link,
|
||||
}: {
|
||||
title: string;
|
||||
label: string;
|
||||
description: string;
|
||||
image: React.ReactNode;
|
||||
link: string;
|
||||
}) => {
|
||||
return (
|
||||
<Link href={link} sx={{ textDecoration: "none" }}>
|
||||
<Card sx={cardStyles} 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}
|
||||
<Typography variant="h2">{title}</Typography>
|
||||
<Typography variant="body3">{description}</Typography>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
export default ExplorerHeroCard;
|
||||
@@ -0,0 +1,374 @@
|
||||
"use client";
|
||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
||||
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
|
||||
import StarIcon from "@mui/icons-material/Star";
|
||||
import { Box, Button, Card, CardContent, Typography } from "@mui/material";
|
||||
// import { CopyToClipboard } from "@nymproject/react/clipboard/CopyToClipboard";
|
||||
import Image from "next/image";
|
||||
import { QRCodeCanvas } from "qrcode.react";
|
||||
import type React from "react";
|
||||
import type { FC, ReactElement } from "react";
|
||||
import Flag from "react-world-flags";
|
||||
import profileImagePlaceholder from "../../../public/profileImagePlaceholder.png";
|
||||
import { NymTokenSVG } from "../icons/NymTokenSVG";
|
||||
// import { Remark42Comments } from "../comments";
|
||||
import { type ILineChartData, LineChart } from "../lineChart";
|
||||
import {
|
||||
DynamicProgressBar,
|
||||
type IDynamicProgressBarProps,
|
||||
} from "../progressBars/DynamicProgressBar";
|
||||
|
||||
interface ICardUpDownPriceLineProps {
|
||||
percentage: number;
|
||||
numberWentUp: boolean;
|
||||
}
|
||||
const CardUpDownPriceLine = (
|
||||
props: ICardUpDownPriceLineProps,
|
||||
): ReactElement => {
|
||||
const { percentage, numberWentUp } = props;
|
||||
return (
|
||||
<Box display={"flex"} alignItems={"center"}>
|
||||
{numberWentUp ? (
|
||||
<ArrowUpwardIcon sx={{ color: "#00CA33", fontSize: 13 }} />
|
||||
) : (
|
||||
<ArrowDownwardIcon sx={{ color: "#DF1400", fontSize: 13 }} />
|
||||
)}
|
||||
<Typography
|
||||
fontSize={13}
|
||||
sx={{ color: numberWentUp ? "#00CA33" : "#DF1400" }}
|
||||
>
|
||||
{percentage}% (24H)
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardTitlePriceProps {
|
||||
price: number;
|
||||
upDownLine: ICardUpDownPriceLineProps;
|
||||
}
|
||||
const CardTitlePrice = (props: ICardTitlePriceProps): React.ReactNode => {
|
||||
const { price, upDownLine } = props;
|
||||
return (
|
||||
<Box display={"flex"} flexDirection={"column"} alignItems={"flex-end"}>
|
||||
<Box display={"flex"} justifyContent={"space-between"} width={"100%"}>
|
||||
<Box display={"flex"} gap={1} alignItems={"center"}>
|
||||
<NymTokenSVG />
|
||||
<Typography>NYM</Typography>
|
||||
</Box>
|
||||
<Typography>${price}</Typography>
|
||||
</Box>
|
||||
<CardUpDownPriceLine {...upDownLine} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export interface ICardDataRowsProps {
|
||||
rows: Array<{ key: string; value: string }>;
|
||||
}
|
||||
export const CardDataRows = (props: ICardDataRowsProps): React.ReactNode => {
|
||||
const { rows } = props;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{rows.map((row, i) => {
|
||||
return (
|
||||
<Box
|
||||
key={row.key}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
display={"flex"}
|
||||
justifyContent={"space-between"}
|
||||
borderBottom={i === 0 ? "1px solid #C3D7D7" : "none"}
|
||||
>
|
||||
<Typography fontSize={14} textTransform={"uppercase"}>
|
||||
{row.key}
|
||||
</Typography>
|
||||
<Typography fontSize={14} textTransform={"uppercase"}>
|
||||
{row.value}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
interface ICardProileImage {
|
||||
url?: string;
|
||||
}
|
||||
const CardProfileImage = (props: ICardProileImage) => {
|
||||
const { url } = props;
|
||||
return (
|
||||
<Box display={"flex"} justifyContent={"flex-start"} mb={3}>
|
||||
{url ? (
|
||||
<Image src={url} alt="linkedIn" width={80} height={80} />
|
||||
) : (
|
||||
<Image
|
||||
src={profileImagePlaceholder}
|
||||
alt="linkedIn"
|
||||
width={80}
|
||||
height={80}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardProfileCountry {
|
||||
countryCode: string;
|
||||
countryName: string;
|
||||
}
|
||||
|
||||
const CardProfileCountry = (props: ICardProfileCountry) => {
|
||||
const { countryCode, countryName } = props;
|
||||
return (
|
||||
<Box display={"flex"} justifyContent={"flex-start"} gap={2}>
|
||||
<Flag code={countryCode} width="20" />
|
||||
<Typography textTransform={"uppercase"}>{countryName}</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardCopyAddressProps {
|
||||
title: string;
|
||||
address: string;
|
||||
}
|
||||
|
||||
const CardCopyAddress = (props: ICardCopyAddressProps) => {
|
||||
const { title, address } = props;
|
||||
return (
|
||||
<Box
|
||||
paddingTop={2}
|
||||
paddingBottom={2}
|
||||
display={"flex"}
|
||||
flexDirection={"column"}
|
||||
gap={2}
|
||||
borderBottom={"1px solid #C3D7D7"}
|
||||
>
|
||||
<Typography textTransform={"uppercase"}>{title}</Typography>
|
||||
<Box display={"flex"} justifyContent={"space-between"}>
|
||||
<Typography maxWidth={"90%"} sx={{ wordWrap: "break-word" }}>
|
||||
{address}
|
||||
</Typography>
|
||||
|
||||
{/* <CopyToClipboard
|
||||
sx={{ mr: 0.5, color: "grey.400" }}
|
||||
smallIcons
|
||||
value={address}
|
||||
tooltip={`Copy identity key ${address} to clipboard`}
|
||||
/>*/}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardQRCodeProps {
|
||||
url: string;
|
||||
}
|
||||
|
||||
const CardQRCode = (props: ICardQRCodeProps) => {
|
||||
const { url } = props;
|
||||
return (
|
||||
<Box display={"flex"} justifyContent={"flex-start"}>
|
||||
<Box
|
||||
padding={2}
|
||||
border={"1px solid #C3D7D7"}
|
||||
display={"block"}
|
||||
width={"unset"}
|
||||
>
|
||||
<QRCodeCanvas value={url} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardRatingsProps {
|
||||
ratings: Array<{ title: string; numberOfStars: number }>;
|
||||
}
|
||||
|
||||
const CardRatings = (props: ICardRatingsProps) => {
|
||||
const { ratings } = props;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{ratings.map((rating, i) => {
|
||||
const Stars = () => {
|
||||
const stars = [];
|
||||
for (let i = 0; i < rating.numberOfStars; i++) {
|
||||
stars.push(
|
||||
<StarIcon sx={{ color: "#14E76F" }} fontSize="small" key={i} />,
|
||||
);
|
||||
}
|
||||
return stars;
|
||||
};
|
||||
const RatingTitle = () => {
|
||||
switch (rating.numberOfStars) {
|
||||
case 1:
|
||||
case 2:
|
||||
return <Typography>Bad</Typography>;
|
||||
case 3:
|
||||
return <Typography>ok</Typography>;
|
||||
case 4:
|
||||
return <Typography>Good</Typography>;
|
||||
default:
|
||||
return <Typography>Excellent</Typography>;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
key={rating.title}
|
||||
paddingTop={2}
|
||||
paddingBottom={2}
|
||||
display={"flex"}
|
||||
justifyContent={"space-between"}
|
||||
borderBottom={i < ratings.length - 1 ? "1px solid #C3D7D7" : "none"}
|
||||
>
|
||||
<Typography>{rating.title}</Typography>
|
||||
<Box display={"flex"} gap={1} alignItems={"center"}>
|
||||
<Stars />
|
||||
<RatingTitle />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export type ContentCardProps = {
|
||||
overTitle?: string;
|
||||
profileImage?: ICardProileImage;
|
||||
title?: string | number;
|
||||
profileCountry?: ICardProfileCountry;
|
||||
upDownLine?: ICardUpDownPriceLineProps;
|
||||
titlePrice?: ICardTitlePriceProps;
|
||||
dataRows?: ICardDataRowsProps;
|
||||
graph?: { data: Array<ILineChartData>; color: string; label: string };
|
||||
progressBar?: IDynamicProgressBarProps;
|
||||
paragraph?: string;
|
||||
nymAddress?: ICardCopyAddressProps;
|
||||
identityKey?: ICardCopyAddressProps;
|
||||
qrCode?: ICardQRCodeProps;
|
||||
ratings?: ICardRatingsProps;
|
||||
comments?: boolean;
|
||||
stakeButton?: {
|
||||
label: string;
|
||||
identityKey: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const MonoCard: FC<ContentCardProps> = ({
|
||||
title,
|
||||
titlePrice,
|
||||
overTitle,
|
||||
upDownLine,
|
||||
dataRows,
|
||||
graph,
|
||||
progressBar,
|
||||
paragraph,
|
||||
profileImage,
|
||||
profileCountry,
|
||||
nymAddress,
|
||||
identityKey,
|
||||
qrCode,
|
||||
ratings,
|
||||
// comments,
|
||||
stakeButton,
|
||||
}) => (
|
||||
<Card sx={{ height: "100%", borderRadius: "unset", padding: 1 }}>
|
||||
<CardContent
|
||||
sx={{
|
||||
paddingBottom: "0px !important",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
{overTitle && (
|
||||
<Typography fontSize={14} mb={3} textTransform={"uppercase"}>
|
||||
{overTitle}
|
||||
</Typography>
|
||||
)}
|
||||
{profileImage && <CardProfileImage {...profileImage} />}
|
||||
{title && (
|
||||
<Typography fontSize={24} mb={upDownLine ? 0 : 3}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
{profileCountry && (
|
||||
<Box mb={3}>
|
||||
<CardProfileCountry {...profileCountry} />
|
||||
</Box>
|
||||
)}
|
||||
{upDownLine && (
|
||||
<Box mb={3}>
|
||||
<CardUpDownPriceLine {...upDownLine} />
|
||||
</Box>
|
||||
)}
|
||||
{titlePrice && <CardTitlePrice {...titlePrice} />}
|
||||
</Box>
|
||||
{qrCode && (
|
||||
<Box mb={3}>
|
||||
<CardQRCode {...qrCode} />
|
||||
</Box>
|
||||
)}
|
||||
{nymAddress && (
|
||||
<Box mb={3}>
|
||||
<CardCopyAddress {...nymAddress} />
|
||||
</Box>
|
||||
)}
|
||||
{identityKey && (
|
||||
<Box mb={3}>
|
||||
<CardCopyAddress {...identityKey} />
|
||||
</Box>
|
||||
)}
|
||||
{dataRows && (
|
||||
<Box mb={3}>
|
||||
<CardDataRows {...dataRows} />
|
||||
</Box>
|
||||
)}
|
||||
{ratings && (
|
||||
<Box mb={3}>
|
||||
<CardRatings {...ratings} />
|
||||
</Box>
|
||||
)}
|
||||
{graph && (
|
||||
<Box mb={3}>
|
||||
<LineChart
|
||||
data={graph.data}
|
||||
color={graph.color}
|
||||
label={graph.label}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{progressBar && (
|
||||
<Box mb={3}>
|
||||
<DynamicProgressBar {...progressBar} />
|
||||
</Box>
|
||||
)}
|
||||
{paragraph && <Typography mb={3}>{paragraph}</Typography>}
|
||||
{/* {comments && (
|
||||
<Box mb={3}>
|
||||
<Remark42Comments />
|
||||
</Box>
|
||||
)} */}
|
||||
{stakeButton && (
|
||||
<Box mb={3}>
|
||||
<Button
|
||||
onClick={() =>
|
||||
console.log(
|
||||
"stakeButton.identityKey :>> ",
|
||||
stakeButton.identityKey,
|
||||
)
|
||||
}
|
||||
variant="contained"
|
||||
>
|
||||
{stakeButton.label}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Divider, Stack, type SxProps, Typography } from "@mui/material";
|
||||
|
||||
const listItemLabelStyle: SxProps = {
|
||||
color: "pine.600",
|
||||
letterSpacing: 0.7,
|
||||
flexGrow: 1,
|
||||
};
|
||||
|
||||
const listItemValueStyle: SxProps = {
|
||||
fontSize: "small",
|
||||
fontWeight: 300,
|
||||
color: "pine.950",
|
||||
};
|
||||
|
||||
const ExplorerListItem = ({
|
||||
label,
|
||||
value,
|
||||
row,
|
||||
divider,
|
||||
}: {
|
||||
label?: string;
|
||||
value: string | React.ReactNode;
|
||||
row?: boolean;
|
||||
divider?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
direction={row ? "row" : "column"}
|
||||
alignItems={row ? "center" : "flex-start"}
|
||||
justifyContent="space-between"
|
||||
gap={1}
|
||||
>
|
||||
{label && (
|
||||
<Typography variant="h6" sx={listItemLabelStyle}>
|
||||
{label}
|
||||
</Typography>
|
||||
)}
|
||||
{typeof value === "string" ? (
|
||||
<Typography variant="body3" sx={listItemValueStyle}>
|
||||
{value}
|
||||
</Typography>
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
</Stack>
|
||||
{divider && <Divider variant="fullWidth" sx={{ my: 2 }} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExplorerListItem;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { LinearProgress, type LinearProgressProps } from "@mui/material";
|
||||
|
||||
const ProgressBar = ({
|
||||
value,
|
||||
color,
|
||||
}: {
|
||||
value: number;
|
||||
color: LinearProgressProps["color"];
|
||||
}) => {
|
||||
return (
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={value}
|
||||
sx={{ height: 8, borderRadius: 5, width: "100%" }}
|
||||
color={color}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default ProgressBar;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { StarOutlineRounded, StarRounded } from "@mui/icons-material/";
|
||||
import { Rating } from "@mui/material";
|
||||
|
||||
const StarRating = ({
|
||||
value,
|
||||
defaultValue,
|
||||
max = 5,
|
||||
size = "medium",
|
||||
}: {
|
||||
value: number;
|
||||
defaultValue?: number;
|
||||
max?: number;
|
||||
size?: "small" | "medium" | "large";
|
||||
}) => {
|
||||
return (
|
||||
<Rating
|
||||
size={size}
|
||||
sx={{ color: "accent.main" }}
|
||||
value={value}
|
||||
defaultValue={defaultValue}
|
||||
max={max}
|
||||
readOnly
|
||||
icon={<StarRounded fontSize={size} />}
|
||||
emptyIcon={
|
||||
<StarOutlineRounded fontSize={size} sx={{ color: "accent.main" }} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default StarRating;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Box, Button } from "@mui/material";
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface TwoSidedSwitchProps {
|
||||
leftLabel: string; // Label for the left side
|
||||
rightLabel: string; // Label for the right side
|
||||
onSwitch?: (side: "left" | "right") => void; // Callback when switched
|
||||
}
|
||||
|
||||
const TwoSidedSwitch: React.FC<TwoSidedSwitchProps> = ({
|
||||
leftLabel,
|
||||
rightLabel,
|
||||
onSwitch,
|
||||
}) => {
|
||||
const [selectedSide, setSelectedSide] = useState<"left" | "right">("left");
|
||||
|
||||
const handleSwitch = (side: "left" | "right") => {
|
||||
setSelectedSide(side);
|
||||
if (onSwitch) onSwitch(side);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
borderRadius: "20px",
|
||||
overflow: "hidden",
|
||||
width: "200px",
|
||||
height: "40px",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={() => handleSwitch("left")}
|
||||
sx={{
|
||||
flex: 1,
|
||||
backgroundColor: selectedSide === "left" ? "black" : "transparent",
|
||||
color: selectedSide === "left" ? "white" : "black",
|
||||
border: "1px dashed black",
|
||||
borderRight: "none",
|
||||
borderBottomRightRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
// "&:hover": {
|
||||
// backgroundColor: selectedSide === "left" ? "black" : "lightgray",
|
||||
// },
|
||||
}}
|
||||
>
|
||||
{leftLabel}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleSwitch("right")}
|
||||
sx={{
|
||||
flex: 1,
|
||||
backgroundColor: selectedSide === "right" ? "black" : "transparent",
|
||||
color: selectedSide === "right" ? "white" : "black",
|
||||
border: "1px dashed black",
|
||||
borderLeft: "none",
|
||||
borderBottomLeftRadius: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
// "&:hover": {
|
||||
// backgroundColor: selectedSide === "right" ? "black" : "lightgray",
|
||||
// },
|
||||
}}
|
||||
>
|
||||
{rightLabel}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TwoSidedSwitch;
|
||||
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
import { colours } from "@/theme/colours";
|
||||
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 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,105 @@
|
||||
"use client";
|
||||
|
||||
import NymLogo from "@/components/icons/NymLogo";
|
||||
import { Link } from "@/components/muiLink";
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import { subtitles } from "@/theme/typography";
|
||||
import { Box, Button, Divider } from "@mui/material";
|
||||
import type React from "react";
|
||||
|
||||
const DUMMY_MENU_DATA = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Explorer",
|
||||
url: "/explorer",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Stake",
|
||||
url: "/stake",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Onboarding",
|
||||
url: "/onboarding",
|
||||
},
|
||||
];
|
||||
|
||||
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,
|
||||
}}
|
||||
>
|
||||
{DUMMY_MENU_DATA.map((menu) => (
|
||||
<Button
|
||||
key={menu.title}
|
||||
href={menu.url}
|
||||
sx={{
|
||||
borderRadius: 0,
|
||||
padding: 0,
|
||||
minWidth: "auto",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
height: "100%",
|
||||
...subtitles.subtitle1,
|
||||
"& .MuiButton-endIcon": {
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
},
|
||||
"& .MuiButton-startIcon": {
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
},
|
||||
"&:hover": {
|
||||
textDecoration: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{menu.title}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
<Button variant="contained" size="small">
|
||||
Connect Wallet
|
||||
</Button>
|
||||
</Wrapper>
|
||||
<Divider variant="fullWidth" sx={{ width: "100%" }} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { DesktopHeader } from "./DesktopHeader";
|
||||
|
||||
export const Header = async () => {
|
||||
return (
|
||||
<header>
|
||||
<DesktopHeader />
|
||||
{/* Mobile header will go here */}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import Document from "@/public/icons/document.svg";
|
||||
import Download from "@/public/icons/download.svg";
|
||||
|
||||
export const NavigationIcons = {
|
||||
article: Document,
|
||||
download: Download,
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import Image from "next/image";
|
||||
|
||||
const ArrowUpRight = () => (
|
||||
<Image
|
||||
src="icons/arrow-up-right.svg"
|
||||
alt="Arrow Up Right"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
);
|
||||
|
||||
export default ArrowUpRight;
|
||||
@@ -0,0 +1,14 @@
|
||||
import Image from "next/image";
|
||||
|
||||
const ChevronMenu = () => {
|
||||
return (
|
||||
<Image
|
||||
src="/icons/chevronMenu.svg"
|
||||
alt="Chevron Menu"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChevronMenu;
|
||||
@@ -0,0 +1,13 @@
|
||||
import Image from "next/image";
|
||||
|
||||
const CopyFile = ({ className }: { className?: string }) => (
|
||||
<Image
|
||||
src="/icons/copy-file.svg"
|
||||
alt="Copyfile"
|
||||
width={16}
|
||||
height={16}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
|
||||
export default CopyFile;
|
||||
@@ -0,0 +1,9 @@
|
||||
import Image from "next/image";
|
||||
|
||||
const Gateway = () => {
|
||||
return (
|
||||
<Image src="/icons/gateway.svg" alt="Chevron Menu" width={84} height={84} />
|
||||
);
|
||||
};
|
||||
|
||||
export default Gateway;
|
||||
@@ -0,0 +1,8 @@
|
||||
import Image from "next/image";
|
||||
|
||||
const NymLogo = () => {
|
||||
return (
|
||||
<Image src="/icons/nym-logo.svg" alt="Nym Logo" width={90} height={25} />
|
||||
);
|
||||
};
|
||||
export default NymLogo;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import * as React from "react";
|
||||
|
||||
export const NymTokenSVG = () => {
|
||||
const theme = useTheme();
|
||||
const color = theme.palette.text.primary;
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title id="nymTokenTitle">Nym Token Icon</title>
|
||||
<g clipPath="url(#clip0_7134_15888)">
|
||||
<path
|
||||
d="M17.07 3.43C13.17 -0.480002 6.83 -0.480002 2.93 3.43C-0.980002 7.34 -0.980002 13.67 2.93 17.57C6.84 21.48 13.17 21.48 17.07 17.57C20.98 13.67 20.98 7.33 17.07 3.43ZM16.21 16.71C12.78 20.14 7.21 20.14 3.78 16.71C0.349997 13.28 0.349997 7.71 3.78 4.28C7.21 0.849997 12.78 0.849997 16.21 4.28C19.65 7.72 19.65 13.28 16.21 16.71Z"
|
||||
fill={color}
|
||||
/>
|
||||
<path
|
||||
d="M15.4 16.33V4.66999C14.89 4.18999 14.32 3.76999 13.71 3.43999V14.59L6.35001 3.39999C5.71001 3.73999 5.12001 4.15999 4.60001 4.65999V16.33C5.11001 16.81 5.68001 17.23 6.29001 17.56V6.40999L13.65 17.6C14.29 17.26 14.88 16.83 15.4 16.33Z"
|
||||
fill={color}
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7134_15888">
|
||||
<rect
|
||||
width="20"
|
||||
height="20"
|
||||
fill={color}
|
||||
transform="translate(0 0.5)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { ExplorerData } from "@/app/api";
|
||||
import { Box } from "@mui/material";
|
||||
import ExplorerCard from "../Cards/ExplorerCard";
|
||||
import ExplorerListItem from "../List/ListItem";
|
||||
import { DynamicProgressBar } from "../progressBars/DynamicProgressBar";
|
||||
|
||||
interface ICurrentEpochCardProps {
|
||||
explorerData: ExplorerData | null;
|
||||
}
|
||||
|
||||
export const CurrentEpochCard = (props: ICurrentEpochCardProps) => {
|
||||
const { explorerData } = props;
|
||||
|
||||
const currentEpochStart =
|
||||
explorerData?.currentEpochData.current_epoch_start || "";
|
||||
|
||||
const progressBar = {
|
||||
start: currentEpochStart || "",
|
||||
showEpoch: true,
|
||||
};
|
||||
return (
|
||||
<ExplorerCard title="Current NGM epoch">
|
||||
<ExplorerListItem
|
||||
value={
|
||||
<Box mt={3} width={"100%"}>
|
||||
<DynamicProgressBar {...progressBar} />
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
import type { ExplorerData, IPacketsAndStakingData } from "@/app/api";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import ExplorerCard from "../Cards/ExplorerCard";
|
||||
import ExplorerListItem from "../List/ListItem";
|
||||
import { type ILineChartData, LineChart } from "../lineChart";
|
||||
|
||||
interface INetworkStakeCardProps {
|
||||
explorerData: ExplorerData | null;
|
||||
}
|
||||
export const NetworkStakeCard = (props: INetworkStakeCardProps) => {
|
||||
const { explorerData } = props;
|
||||
const [stakeLineGraphData, setStakeLineGraphData] = useState<{
|
||||
color: string;
|
||||
label: string;
|
||||
data: ILineChartData[];
|
||||
}>();
|
||||
const currentStake =
|
||||
Number(explorerData?.currentEpochRewardsData.interval.staking_supply) /
|
||||
1000000 || 0;
|
||||
|
||||
useEffect(() => {
|
||||
const getStakeData = () => {
|
||||
const data: Array<ILineChartData> = [];
|
||||
explorerData?.packetsAndStakingData.map(
|
||||
(item: IPacketsAndStakingData) => {
|
||||
data.push({
|
||||
date_utc: item.date_utc,
|
||||
numericData: item.total_stake / 1000000,
|
||||
});
|
||||
},
|
||||
);
|
||||
return data;
|
||||
};
|
||||
const stakeLineGraphData = {
|
||||
color: "#00CA33",
|
||||
label: "Total stake delegated in NYM",
|
||||
data: getStakeData(),
|
||||
};
|
||||
setStakeLineGraphData(stakeLineGraphData);
|
||||
}, [explorerData]);
|
||||
|
||||
const stakeCard = {
|
||||
overTitle: "Current network stake",
|
||||
title: `${currentStake} NYM` || "",
|
||||
graph: stakeLineGraphData,
|
||||
};
|
||||
const subtitle = `${currentStake} NYM` || "";
|
||||
return (
|
||||
<div>
|
||||
<ExplorerCard title="Current network stake">
|
||||
<ExplorerListItem
|
||||
value={
|
||||
<Box mt={3} width={"100%"} height={"100%"}>
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{ color: "pine.400", wordWrap: "break-word" }}
|
||||
maxWidth={"95%"}
|
||||
>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
{stakeLineGraphData && (
|
||||
<LineChart
|
||||
data={stakeLineGraphData.data}
|
||||
color={stakeLineGraphData.color}
|
||||
label={stakeLineGraphData.label}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ExplorerCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
import type { ExplorerData, IPacketsAndStakingData } from "@/app/api";
|
||||
import { formatBigNum } from "@/app/utils/formatBigNumbers";
|
||||
import { Box } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import ExplorerCard from "../Cards/ExplorerCard";
|
||||
import ExplorerListItem from "../List/ListItem";
|
||||
import { MonoCard } from "../cards/MonoCard";
|
||||
import { type ILineChartData, LineChart } from "../lineChart";
|
||||
import { CardUpDownPriceLine } from "../prices/UpDownPriceLine";
|
||||
import { DynamicProgressBar } from "../progressBars/DynamicProgressBar";
|
||||
|
||||
interface INoiseCardProps {
|
||||
explorerData: ExplorerData;
|
||||
}
|
||||
export const NoiseCard = (props: INoiseCardProps) => {
|
||||
const { explorerData } = props;
|
||||
const [noiseLineGraphData, setNoiseLineGraphData] = useState<{
|
||||
color: string;
|
||||
label: string;
|
||||
data: ILineChartData[];
|
||||
}>();
|
||||
const noiseLast24H =
|
||||
explorerData.packetsAndStakingData[
|
||||
explorerData.packetsAndStakingData.length - 1
|
||||
].total_packets_sent +
|
||||
explorerData.packetsAndStakingData[
|
||||
explorerData.packetsAndStakingData.length - 1
|
||||
].total_packets_received;
|
||||
|
||||
const noisePrevious24H =
|
||||
explorerData.packetsAndStakingData[
|
||||
explorerData.packetsAndStakingData.length - 2
|
||||
].total_packets_sent +
|
||||
explorerData.packetsAndStakingData[
|
||||
explorerData.packetsAndStakingData.length - 2
|
||||
].total_packets_received;
|
||||
|
||||
const calculatePercentageChange = (last24H: number, previous24H: number) => {
|
||||
if (previous24H === 0) {
|
||||
throw new Error(
|
||||
"Cannot calculate percentage change when yesterday's value is zero.",
|
||||
);
|
||||
}
|
||||
|
||||
const change = ((last24H - previous24H) / previous24H) * 100;
|
||||
|
||||
return Number.parseFloat(change.toFixed(2));
|
||||
};
|
||||
|
||||
const percentage = calculatePercentageChange(noiseLast24H, noisePrevious24H);
|
||||
|
||||
useEffect(() => {
|
||||
const getPacketsData = () => {
|
||||
const data: Array<ILineChartData> = [];
|
||||
explorerData?.packetsAndStakingData.map(
|
||||
(item: IPacketsAndStakingData) => {
|
||||
data.push({
|
||||
date_utc: item.date_utc,
|
||||
numericData: item.total_packets_sent + item.total_packets_received,
|
||||
});
|
||||
},
|
||||
);
|
||||
return data;
|
||||
};
|
||||
const noiseLineGraphData = {
|
||||
color: "#8482FD",
|
||||
label: "Total packets sent and received",
|
||||
data: getPacketsData(),
|
||||
};
|
||||
setNoiseLineGraphData(noiseLineGraphData);
|
||||
}, [explorerData]);
|
||||
|
||||
const noiseCard = {
|
||||
percentage: Math.abs(percentage) || 0,
|
||||
numberWentUp: percentage > 0,
|
||||
};
|
||||
const graph = noiseLineGraphData;
|
||||
|
||||
const subtitle = formatBigNum(noiseLast24H)?.toString();
|
||||
return (
|
||||
<ExplorerCard title="Noise generated last 24h" subtitle={subtitle}>
|
||||
<ExplorerListItem
|
||||
value={
|
||||
<Box width={"100%"}>
|
||||
<CardUpDownPriceLine {...noiseCard} />
|
||||
{noiseLineGraphData && (
|
||||
<LineChart
|
||||
data={noiseLineGraphData.data}
|
||||
color={noiseLineGraphData.color}
|
||||
label={noiseLineGraphData.label}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { MonoCard } from "../cards/MonoCard";
|
||||
|
||||
export const RewardsCard = () => {
|
||||
const rewardsCard = {
|
||||
overTitle: "Operator rewards this month",
|
||||
title: "198.841720 NYM",
|
||||
};
|
||||
return <MonoCard {...rewardsCard} />;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { MonoCard } from "../cards/MonoCard";
|
||||
|
||||
export const TokenomicsCard = () => {
|
||||
const tokenomicsCard = {
|
||||
overTitle: "Tokenomics overview",
|
||||
titlePrice: {
|
||||
price: 1.15,
|
||||
upDownLine: {
|
||||
percentage: 10,
|
||||
numberWentUp: true,
|
||||
},
|
||||
},
|
||||
dataRows: {
|
||||
rows: [
|
||||
{ key: "Market cap", value: "$ 1000000" },
|
||||
{ key: "24H VOL", value: "$ 1000000" },
|
||||
],
|
||||
},
|
||||
};
|
||||
return <MonoCard {...tokenomicsCard} />;
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
import { Box, useMediaQuery, useTheme } from "@mui/material";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useState } from "react";
|
||||
import Loading from "../loading";
|
||||
|
||||
const NivoLineChart = dynamic(
|
||||
() => import("@nivo/line").then((m) => m.ResponsiveLine),
|
||||
{
|
||||
loading: () => <Loading />,
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export interface ILineChartData {
|
||||
date_utc: string;
|
||||
numericData?: number;
|
||||
// purpleLineNumericData?: number;
|
||||
}
|
||||
|
||||
interface IAxes {
|
||||
x: Date;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface ILineAxes {
|
||||
id: string;
|
||||
data: Array<IAxes>;
|
||||
}
|
||||
|
||||
export const LineChart = ({
|
||||
data,
|
||||
color,
|
||||
label,
|
||||
}: {
|
||||
data: Array<ILineChartData>;
|
||||
color: string;
|
||||
label: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isDesktop = useMediaQuery(theme.breakpoints.up("lg"));
|
||||
|
||||
const [chartData, setChartData] = useState<Array<ILineAxes>>();
|
||||
|
||||
useEffect(() => {
|
||||
const resultData = transformData(data);
|
||||
if (resultData.length > 0) {
|
||||
setChartData(resultData);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const transformData = (data: Array<ILineChartData>) => {
|
||||
const lineData: ILineAxes = {
|
||||
id: label,
|
||||
data: [],
|
||||
};
|
||||
|
||||
// const purpleLineData: ILineAxes = {
|
||||
// id: "Numeric Data 2",
|
||||
// data: [],
|
||||
// };
|
||||
|
||||
data.map((item: ILineChartData) => {
|
||||
const axesGreenLineData: IAxes = {
|
||||
x: new Date(item.date_utc),
|
||||
y: item.numericData || 0,
|
||||
};
|
||||
|
||||
lineData.data.push(axesGreenLineData);
|
||||
|
||||
// const axesPurpleLineData: IAxes = {
|
||||
// x: new Date(item.date_utc),
|
||||
// y: item.purpleLineNumericData,
|
||||
// };
|
||||
|
||||
// purpleLineData.data.push(axesPurpleLineData);
|
||||
});
|
||||
return [{ ...lineData }];
|
||||
};
|
||||
|
||||
const yformat = (num: number | string | Date) => {
|
||||
if (typeof num === "number") {
|
||||
if (num >= 1000000000) {
|
||||
return `${(num / 1000000000).toFixed(1).replace(/\.0$/, "")}B`;
|
||||
}
|
||||
if (num >= 1000000) {
|
||||
return `${(num / 1000000).toFixed(1).replace(/\.0$/, "")}M`;
|
||||
}
|
||||
if (num >= 1000) {
|
||||
return `${(num / 1000).toFixed(1).replace(/\.0$/, "")}K`;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
throw new Error("Unexpected value");
|
||||
};
|
||||
|
||||
return (
|
||||
<Box width={"100%"} height={isDesktop ? 200 : 150}>
|
||||
{chartData && (
|
||||
<NivoLineChart
|
||||
curve="monotoneX"
|
||||
colors={[color]}
|
||||
data={chartData}
|
||||
animate
|
||||
enableSlices="x"
|
||||
margin={{
|
||||
bottom: 24,
|
||||
left: 30,
|
||||
right: 12,
|
||||
top: 20,
|
||||
}}
|
||||
theme={{
|
||||
grid: { line: { strokeWidth: 0 } },
|
||||
tooltip: { container: { color: "black" } },
|
||||
axis: {
|
||||
domain: {
|
||||
line: { stroke: "#C3D7D7", strokeWidth: 1, strokeOpacity: 1 },
|
||||
},
|
||||
ticks: {
|
||||
text: {
|
||||
fill: "#818386",
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
text: {
|
||||
fill: "#818386",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
xScale={{
|
||||
type: "time",
|
||||
format: "%Y-%m-%d",
|
||||
}}
|
||||
yScale={{ min: 1, type: "linear" }}
|
||||
xFormat="time:%Y-%m-%d"
|
||||
axisLeft={{
|
||||
legendOffset: 12,
|
||||
tickSize: 3,
|
||||
format: yformat,
|
||||
tickValues: 5,
|
||||
}}
|
||||
axisBottom={{
|
||||
format: "%b %d",
|
||||
legendOffset: -12,
|
||||
tickValues:
|
||||
chartData[0].data.length > 7 ? "every 5 days" : "every day",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import Box from "@mui/material/Box";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import * as React from "react";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginY: 10,
|
||||
width: 1,
|
||||
}}
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import { TABLET_WIDTH } from "@/app/constants";
|
||||
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import ErrorOutline from "@mui/icons-material/ErrorOutline";
|
||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Modal,
|
||||
Stack,
|
||||
type SxProps,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
import type React from "react";
|
||||
|
||||
export const modalStyle = (width: number | string = 600) => ({
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
width,
|
||||
transform: "translate(-50%, -50%)",
|
||||
bgcolor: "background.paper",
|
||||
boxShadow: 24,
|
||||
borderRadius: "16px",
|
||||
p: 4,
|
||||
});
|
||||
|
||||
export const StyledBackButton = ({
|
||||
onBack,
|
||||
label,
|
||||
fullWidth,
|
||||
sx,
|
||||
}: {
|
||||
onBack: () => void;
|
||||
label?: string;
|
||||
fullWidth?: boolean;
|
||||
sx?: SxProps;
|
||||
}) => (
|
||||
<Button
|
||||
disableFocusRipple
|
||||
size="large"
|
||||
fullWidth={fullWidth}
|
||||
variant="outlined"
|
||||
onClick={onBack}
|
||||
sx={sx}
|
||||
>
|
||||
{label || <ArrowBackIosNewIcon fontSize="small" />}
|
||||
</Button>
|
||||
);
|
||||
|
||||
export const SimpleModal: FCWithChildren<{
|
||||
open: boolean;
|
||||
hideCloseIcon?: boolean;
|
||||
displayErrorIcon?: boolean;
|
||||
displayInfoIcon?: boolean;
|
||||
headerStyles?: SxProps;
|
||||
subHeaderStyles?: SxProps;
|
||||
buttonFullWidth?: boolean;
|
||||
onClose?: () => void;
|
||||
onOk?: () => Promise<void>;
|
||||
onBack?: () => void;
|
||||
header: string | React.ReactNode;
|
||||
subHeader?: string;
|
||||
okLabel: string;
|
||||
backLabel?: string;
|
||||
backButtonFullWidth?: boolean;
|
||||
okDisabled?: boolean;
|
||||
sx?: SxProps;
|
||||
children?: React.ReactNode;
|
||||
}> = ({
|
||||
open,
|
||||
hideCloseIcon,
|
||||
displayErrorIcon,
|
||||
displayInfoIcon,
|
||||
headerStyles,
|
||||
buttonFullWidth,
|
||||
onClose,
|
||||
okDisabled,
|
||||
onOk,
|
||||
onBack,
|
||||
header,
|
||||
subHeader,
|
||||
okLabel,
|
||||
backLabel,
|
||||
backButtonFullWidth,
|
||||
sx,
|
||||
children,
|
||||
}) => {
|
||||
const isTablet = useMediaQuery(TABLET_WIDTH);
|
||||
const styles = modalStyle(isTablet ? 600 : "90%");
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={{ styles, ...sx }}>
|
||||
{displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />}
|
||||
{displayInfoIcon && <InfoOutlinedIcon sx={{ mb: 2, color: "blue" }} />}
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
{typeof header === "string" ? (
|
||||
<Typography
|
||||
fontSize={20}
|
||||
fontWeight={600}
|
||||
sx={{ color: "text.primary", ...headerStyles }}
|
||||
>
|
||||
{header}
|
||||
</Typography>
|
||||
) : (
|
||||
header
|
||||
)}
|
||||
{!hideCloseIcon && <CloseIcon onClick={onClose} cursor="pointer" />}
|
||||
</Stack>
|
||||
|
||||
<Typography mt={subHeader ? 0.5 : 0} mb={3} fontSize={12}>
|
||||
{subHeader}
|
||||
</Typography>
|
||||
|
||||
{children}
|
||||
|
||||
{(onOk || onBack) && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 2,
|
||||
mt: 2,
|
||||
width: buttonFullWidth ? "100%" : null,
|
||||
}}
|
||||
>
|
||||
{onBack && (
|
||||
<StyledBackButton
|
||||
onBack={onBack}
|
||||
label={backLabel}
|
||||
fullWidth={backButtonFullWidth}
|
||||
/>
|
||||
)}
|
||||
{onOk && (
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
size="large"
|
||||
onClick={onOk}
|
||||
disabled={okDisabled}
|
||||
>
|
||||
{okLabel}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -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,29 @@
|
||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
||||
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
interface ICardUpDownPriceLineProps {
|
||||
percentage: number;
|
||||
numberWentUp: boolean;
|
||||
}
|
||||
export const CardUpDownPriceLine = (
|
||||
props: ICardUpDownPriceLineProps,
|
||||
): ReactElement => {
|
||||
const { percentage, numberWentUp } = props;
|
||||
return (
|
||||
<Box display={"flex"} alignItems={"center"}>
|
||||
{numberWentUp ? (
|
||||
<ArrowUpwardIcon sx={{ color: "#00CA33", fontSize: 13 }} />
|
||||
) : (
|
||||
<ArrowDownwardIcon sx={{ color: "#DF1400", fontSize: 13 }} />
|
||||
)}
|
||||
<Typography
|
||||
fontSize={13}
|
||||
sx={{ color: numberWentUp ? "#00CA33" : "#DF1400" }}
|
||||
>
|
||||
{percentage}% (24H)
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { Typography } from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import LinearProgress from "@mui/material/LinearProgress";
|
||||
import * as React from "react";
|
||||
|
||||
export interface IDynamicProgressBarProps {
|
||||
overTitle?: string;
|
||||
start: string; // Start timestamp as ISO 8601 string
|
||||
showEpoch: boolean;
|
||||
}
|
||||
|
||||
export const DynamicProgressBar = (props: IDynamicProgressBarProps) => {
|
||||
const { start, showEpoch, overTitle } = props;
|
||||
const [progress, setProgress] = React.useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Parse the start timestamp
|
||||
const startTime = new Date(start).getTime();
|
||||
const endTime = startTime + 60 * 60 * 1000; // Add 1 hour to the start time
|
||||
|
||||
// Validate start timestamp
|
||||
if (Number.isNaN(startTime)) {
|
||||
console.error("Invalid start timestamp:", { start });
|
||||
return;
|
||||
}
|
||||
|
||||
// Function to calculate progress
|
||||
const calculateProgress = () => {
|
||||
const currentTime = Date.now();
|
||||
if (currentTime < startTime) {
|
||||
return 0;
|
||||
}
|
||||
if (currentTime >= endTime) {
|
||||
return 100;
|
||||
}
|
||||
const elapsed = currentTime - startTime;
|
||||
const total = endTime - startTime;
|
||||
return (elapsed / total) * 100;
|
||||
};
|
||||
|
||||
// Set initial progress and start timer
|
||||
setProgress(calculateProgress());
|
||||
const timer = setInterval(() => {
|
||||
setProgress(calculateProgress());
|
||||
}, 60000); // Update every minute (60000 milliseconds)
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [start]);
|
||||
|
||||
// Helper function to format date
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are 0-based
|
||||
const year = date.getFullYear();
|
||||
return `${hours}:${minutes}, ${day}/${month}/${year}`;
|
||||
};
|
||||
|
||||
const startTime = new Date(start).getTime();
|
||||
const endTime = startTime + 60 * 60 * 1000;
|
||||
|
||||
return (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
{overTitle && (
|
||||
<Typography fontSize={14} mb={2} textTransform={"uppercase"}>
|
||||
{overTitle}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
sx={{
|
||||
backgroundColor: "#CAD6D7",
|
||||
"& .MuiLinearProgress-bar": {
|
||||
backgroundColor: "#14E76F",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{showEpoch && (
|
||||
<Box mt={2}>
|
||||
<Box display={"flex"} justifyContent={"space-between"}>
|
||||
<Typography fontSize={14} textTransform={"uppercase"}>
|
||||
START:
|
||||
</Typography>
|
||||
<Typography fontSize={14}>
|
||||
{startTime ? formatDate(startTime) : ""}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display={"flex"} justifyContent={"space-between"}>
|
||||
<Typography fontSize={14} textTransform={"uppercase"}>
|
||||
END:
|
||||
</Typography>
|
||||
<Typography fontSize={14}>
|
||||
{endTime ? formatDate(endTime) : ""}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Box } from "@mui/material";
|
||||
import type React from "react";
|
||||
|
||||
export interface MultiSegmentProgressBarProps {
|
||||
values: { percentage: number; color: string }[]; // Array of percentage and color pairs
|
||||
// Optional border radius, default is 4
|
||||
backgroundColor?: string; // Optional background color for the bar, default is light gray
|
||||
}
|
||||
|
||||
export const MultiSegmentProgressBar: React.FC<
|
||||
MultiSegmentProgressBarProps
|
||||
> = ({ values, backgroundColor = "#CAD6D7" }) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
backgroundColor,
|
||||
}}
|
||||
>
|
||||
{values.map((value) => (
|
||||
<Box
|
||||
key={value.color}
|
||||
sx={{
|
||||
width: `${value.percentage}%`,
|
||||
backgroundColor: value.color,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import LinearProgress from "@mui/material/LinearProgress";
|
||||
import * as React from "react";
|
||||
|
||||
export interface IStaticProgressBarProps {
|
||||
color: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export const StaticProgressBar = (props: IStaticProgressBarProps) => {
|
||||
const { color, value } = props;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={value}
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: "#CAD6D7",
|
||||
"& .MuiLinearProgress-bar": {
|
||||
backgroundColor: color,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -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,2 @@
|
||||
Letters from Sweden End User License Agreement (EULA)
|
||||
http://lettersfromsweden.se/license
|
||||
@@ -0,0 +1,2 @@
|
||||
Letters from Sweden End User License Agreement (EULA)
|
||||
http://lettersfromsweden.se/license
|
||||
@@ -0,0 +1,85 @@
|
||||
// Fonts
|
||||
import localFont from "next/font/local";
|
||||
|
||||
// LabGrotesqueMono
|
||||
export const labGrotesqueMono = localFont({
|
||||
src: [
|
||||
{
|
||||
path: "./LabGrotesqueMono/LabGrotesqueMono-Regular.woff2",
|
||||
weight: "400",
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
path: "./LabGrotesqueMono/LabGrotesqueMono-Bold.woff2",
|
||||
weight: "700",
|
||||
style: "normal",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// LabGrotesque
|
||||
export const labGrotesque = localFont({
|
||||
src: [
|
||||
{
|
||||
path: "./LabGrotesque/LabGrotesque-Thin.woff2",
|
||||
weight: "100",
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
path: "./LabGrotesque/LabGrotesque-ThinItalic.woff2",
|
||||
weight: "100",
|
||||
style: "italic",
|
||||
},
|
||||
{
|
||||
path: "./LabGrotesque/LabGrotesque-Light.woff2",
|
||||
weight: "300",
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
path: "./LabGrotesque/LabGrotesque-LightItalic.woff2",
|
||||
weight: "300",
|
||||
style: "italic",
|
||||
},
|
||||
{
|
||||
path: "./LabGrotesque/LabGrotesque-Regular.woff2",
|
||||
weight: "400",
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
path: "./LabGrotesque/LabGrotesque-Italic.woff2",
|
||||
weight: "400",
|
||||
style: "italic",
|
||||
},
|
||||
{
|
||||
path: "./LabGrotesque/LabGrotesque-Medium.woff2",
|
||||
weight: "500",
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
path: "./LabGrotesque/LabGrotesque-MediumItalic.woff2",
|
||||
weight: "500",
|
||||
style: "italic",
|
||||
},
|
||||
|
||||
{
|
||||
path: "./LabGrotesque/LabGrotesque-Bold.woff2",
|
||||
weight: "700",
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
path: "./LabGrotesque/LabGrotesque-BoldItalic.woff2",
|
||||
weight: "700",
|
||||
style: "italic",
|
||||
},
|
||||
{
|
||||
path: "./LabGrotesque/LabGrotesque-Black.woff2",
|
||||
weight: "900",
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
path: "./LabGrotesque/LabGrotesque-BlackItalic.woff2",
|
||||
weight: "900",
|
||||
style: "italic",
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
import { lightTheme } from "@/theme/theme";
|
||||
import { CssBaseline } from "@mui/material";
|
||||
import { ThemeProvider as MUIThemeProvider } from "@mui/material";
|
||||
import { AppRouterCacheProvider } from "@mui/material-nextjs/v15-appRouter";
|
||||
|
||||
const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<AppRouterCacheProvider>
|
||||
<MUIThemeProvider theme={lightTheme}>
|
||||
<CssBaseline />
|
||||
{children}
|
||||
</MUIThemeProvider>
|
||||
</AppRouterCacheProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeProvider;
|
||||
@@ -0,0 +1,7 @@
|
||||
import ThemeProvider from "./ThemeProvider";
|
||||
|
||||
const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
return <ThemeProvider>{children}</ThemeProvider>;
|
||||
};
|
||||
|
||||
export default Providers;
|
||||
@@ -0,0 +1,28 @@
|
||||
export const colours = {
|
||||
alert: {
|
||||
success: "#00CA33",
|
||||
warning: "#F9BE2B",
|
||||
error: "#E01400",
|
||||
},
|
||||
green: {
|
||||
500: "#14e76f",
|
||||
},
|
||||
haze: {
|
||||
200: "#DAE0EB",
|
||||
300: "#C4CCDE",
|
||||
},
|
||||
pine: {
|
||||
200: "#CAD6D7",
|
||||
600: "#4C666A",
|
||||
800: "#3E4A4C",
|
||||
950: "#242B2D",
|
||||
},
|
||||
base: {
|
||||
white: "#FFFFFF",
|
||||
black: "#000000",
|
||||
transparent: "transparent",
|
||||
},
|
||||
gray: {
|
||||
50: "#F5F6F6",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
.reactMarkDown {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
margin-top: 0px;
|
||||
line-height: unset;
|
||||
border: 0px;
|
||||
font-size: 16px;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.reactMarkDown p {
|
||||
margin: 0px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.reactMarkDownLink {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.reactMarkDownLink p {
|
||||
margin: 7px 0px;
|
||||
font-size: 14px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.reactMarkDownLink a {
|
||||
font-size: 14px;
|
||||
color: #fb6e4e;
|
||||
display: inline;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
// Define allowed values for textTransform
|
||||
type TextTransform = "none" | "capitalize" | "uppercase" | "lowercase";
|
||||
|
||||
// Define a custom type for typography variants that includes textTransform
|
||||
type CustomTypography = CSSProperties & { textTransform?: TextTransform };
|
||||
|
||||
declare module "@mui/material/styles" {
|
||||
interface Palette {
|
||||
accent: Palette["primary"];
|
||||
backgroundColor: Palette["background"];
|
||||
medium: Palette["primary"];
|
||||
light: Palette["primary"];
|
||||
gray: Palette["primary"];
|
||||
}
|
||||
interface PaletteOptions {
|
||||
accent?: PaletteOptions["primary"];
|
||||
backgroundColor?: PaletteOptions["background"];
|
||||
medium?: PaletteOptions["primary"];
|
||||
light?: PaletteOptions["primary"];
|
||||
gray?: PaletteOptions["primary"];
|
||||
}
|
||||
|
||||
// Apply CustomTypography to TypographyVariants
|
||||
interface TypographyVariants {
|
||||
display: CustomTypography;
|
||||
h1: CustomTypography;
|
||||
h2: CustomTypography;
|
||||
h3: CustomTypography;
|
||||
h4: CustomTypography;
|
||||
h5: CustomTypography;
|
||||
h6: CustomTypography;
|
||||
subtitle1: CustomTypography;
|
||||
subtitle2: CustomTypography;
|
||||
subtitle3: CustomTypography;
|
||||
body3?: CustomTypography;
|
||||
body4?: CustomTypography;
|
||||
body5?: CustomTypography;
|
||||
prose?: CustomTypography;
|
||||
}
|
||||
|
||||
// Apply CustomTypography to TypographyVariantsOptions
|
||||
interface TypographyVariantsOptions {
|
||||
display?: CustomTypography;
|
||||
h1?: CustomTypography;
|
||||
h2?: CustomTypography;
|
||||
h3?: CustomTypography;
|
||||
h4?: CustomTypography;
|
||||
h5?: CustomTypography;
|
||||
h6?: CustomTypography;
|
||||
subtitle1?: CustomTypography;
|
||||
subtitle2?: CustomTypography;
|
||||
subtitle3?: CustomTypography;
|
||||
body3?: CustomTypography;
|
||||
body4?: CustomTypography;
|
||||
body5?: CustomTypography;
|
||||
prose?: CustomTypography;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "@mui/material/Typography" {
|
||||
interface TypographyPropsVariantOverrides {
|
||||
display: true;
|
||||
subtitle3: true;
|
||||
body3: true;
|
||||
body4: true;
|
||||
body5: true;
|
||||
prose: true;
|
||||
}
|
||||
}
|
||||