Compare commits

...

20 Commits

Author SHA1 Message Date
Yana 39c36d3415 wip 2024-12-11 21:10:06 +07:00
Yana 287eca5421 wip 2024-12-11 19:12:15 +07:00
Yana d1ac4f1cfb wip 2024-12-11 18:29:33 +07:00
Yana f2e98f4f16 wip 2024-12-11 16:21:52 +07:00
Yana 7c09295e43 wip 2024-12-11 16:19:55 +07:00
Yana 09fd8f6d9d wip 2024-12-11 16:02:29 +07:00
Yana f69c6707c7 wip 2024-12-11 15:56:58 +07:00
Yana 223f008bee wip 2024-12-11 15:40:46 +07:00
Yana c2086eef9e WIP 2024-12-11 15:38:27 +07:00
Yana Matrosova bc849c7c46 Merge pull request #5244 from nymtech/feature/base-components
Feature/base components
2024-12-11 15:03:58 +07:00
fmtabbara f94f3b1a1d create some base components 2024-12-11 00:44:26 +00:00
fmtabbara c3bcb96bad add icons 2024-12-11 00:43:43 +00:00
fmtabbara 9b6816e265 update theme 2024-12-11 00:43:02 +00:00
fmtabbara 68223000ea test pre-commit hook 2024-12-04 21:11:29 +00:00
fmtabbara 9f5e153ddb add formatting and precommit hook 2024-12-04 21:10:36 +00:00
fmtabbara cf87ced120 test linter 2024-12-04 21:04:08 +00:00
fmtabbara 7560837f01 check linter 2024-12-04 20:53:57 +00:00
fmtabbara 16b9bcb91d add prod build script 2024-12-04 16:09:13 +00:00
fmtabbara 07e946a195 move next explorer to legacy dir + bootstrap new explorer 2024-12-04 15:44:50 +00:00
fmtabbara 648223a235 remove pnpm lockfile 2024-12-04 15:38:55 +00:00
234 changed files with 12074 additions and 9759 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{
"extends": ["next/core-web-vitals"]
"extends": ["next/core-web-vitals", "next/typescript"]
}
+8 -4
View File
@@ -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
+4 -4
View File
@@ -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.
+30
View File
@@ -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"
}
}
}
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
+18 -14
View File
@@ -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"
}
}
+1
View File
@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

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

After

Width:  |  Height:  |  Size: 1.0 KiB

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

After

Width:  |  Height:  |  Size: 1.4 KiB

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

After

Width:  |  Height:  |  Size: 431 B

@@ -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

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

After

Width:  |  Height:  |  Size: 526 B

@@ -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

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

After

Width:  |  Height:  |  Size: 1.7 KiB

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

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

+1 -1
View File
@@ -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

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

After

Width:  |  Height:  |  Size: 385 B

+245
View File
@@ -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();
// }
+18
View File
@@ -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";
+1
View File
@@ -0,0 +1 @@
export const TABLET_WIDTH = "(min-width:700px)";
+16
View File
@@ -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>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+37
View File
@@ -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;
}
+25
View File
@@ -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>
);
}
+265
View File
@@ -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>
);
}
+16
View File
@@ -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
+85
View File
@@ -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;
+7
View File
@@ -0,0 +1,7 @@
import ThemeProvider from "./ThemeProvider";
const Providers = ({ children }: { children: React.ReactNode }) => {
return <ThemeProvider>{children}</ThemeProvider>;
};
export default Providers;
+28
View File
@@ -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;
}
+71
View File
@@ -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;
}
}

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