Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7eddb12cbe | |||
| e3fdc9aa12 | |||
| 4e7ece13c5 | |||
| 38f2a052d2 | |||
| 565e9c5a40 | |||
| b6465836d8 | |||
| 40916f77da | |||
| 492fe16e55 | |||
| cf3baf9398 | |||
| c3f462c34d | |||
| b95a026ddd | |||
| 48a38b6bf3 | |||
| 57e6fa29db | |||
| 1856ac95c6 | |||
| bc6d4562d0 | |||
| 4887cbbd48 | |||
| 3c44ae89da | |||
| a9c9381cb6 | |||
| fbd8cc5b4d | |||
| 399e4b1abd | |||
| fd62ee8204 | |||
| 147ec12a28 |
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# env files (can opt-in for commiting if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,50 @@
|
||||
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
|
||||
|
||||
Starting Docker:
|
||||
|
||||
```bash
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Stopping Docker:
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev -- -p 8080
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:8080/](http://localhost:8080/) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
@@ -0,0 +1,43 @@
|
||||
version: "2"
|
||||
|
||||
services:
|
||||
remark:
|
||||
# remove the next line in case you want to use this Docker Compose file separately
|
||||
# as otherwise it would complain for absence of Dockerfile
|
||||
build: .
|
||||
image: umputun/remark42:latest
|
||||
container_name: "remark42"
|
||||
hostname: "remark42"
|
||||
restart: always
|
||||
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "5"
|
||||
|
||||
# uncomment to expose directly (no proxy)
|
||||
ports:
|
||||
- "8081:8080"
|
||||
- "443:8443"
|
||||
|
||||
environment:
|
||||
- REMARK_URL=http://localhost:8081
|
||||
- SECRET=secret-key
|
||||
- AUTH_ANON=true
|
||||
- SITE=remark42
|
||||
# - DEBUG=true
|
||||
# - AUTH_GOOGLE_CID
|
||||
# - AUTH_GOOGLE_CSEC
|
||||
# - AUTH_GITHUB_CID
|
||||
# - AUTH_GITHUB_CSEC
|
||||
# - AUTH_FACEBOOK_CID
|
||||
# - AUTH_FACEBOOK_CSEC
|
||||
# - AUTH_DISQUS_CID
|
||||
# - AUTH_DISQUS_CSEC
|
||||
# Enable it only for the initial comment import or for manual backups.
|
||||
# Do not leave the server running with the ADMIN_PASSWD set if you don't have an intention
|
||||
# to keep creating backups manually!
|
||||
- ADMIN_PASSWD=password
|
||||
volumes:
|
||||
- ./var:/srv/var
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "remark42-app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "19.0.0-rc-02c0e824-20241028",
|
||||
"react-dom": "19.0.0-rc-02c0e824-20241028",
|
||||
"next": "15.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.0.2"
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
@@ -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 |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,42 @@
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { Metadata } from "next";
|
||||
import localFont from "next/font/local";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = localFont({
|
||||
src: "./fonts/GeistVF.woff",
|
||||
variable: "--font-geist-sans",
|
||||
weight: "100 900",
|
||||
});
|
||||
const geistMono = localFont({
|
||||
src: "./fonts/GeistMonoVF.woff",
|
||||
variable: "--font-geist-mono",
|
||||
weight: "100 900",
|
||||
});
|
||||
|
||||
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 className={`${geistSans.variable} ${geistMono.variable}`}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
.page {
|
||||
--gray-rgb: 0, 0, 0;
|
||||
--gray-alpha-200: rgba(var(--gray-rgb), 0.08);
|
||||
--gray-alpha-100: rgba(var(--gray-rgb), 0.05);
|
||||
|
||||
--button-primary-hover: #383838;
|
||||
--button-secondary-hover: #f2f2f2;
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: 20px 1fr 20px;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
min-height: 100svh;
|
||||
padding: 80px;
|
||||
gap: 64px;
|
||||
font-family: var(--font-geist-sans);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.page {
|
||||
--gray-rgb: 255, 255, 255;
|
||||
--gray-alpha-200: rgba(var(--gray-rgb), 0.145);
|
||||
--gray-alpha-100: rgba(var(--gray-rgb), 0.06);
|
||||
|
||||
--button-primary-hover: #ccc;
|
||||
--button-secondary-hover: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
grid-row-start: 2;
|
||||
}
|
||||
|
||||
.main ol {
|
||||
font-family: var(--font-geist-mono);
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.01em;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
.main li:not(:last-of-type) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.main code {
|
||||
font-family: inherit;
|
||||
background: var(--gray-alpha-100);
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ctas {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.ctas a {
|
||||
appearance: none;
|
||||
border-radius: 128px;
|
||||
height: 48px;
|
||||
padding: 0 20px;
|
||||
border: none;
|
||||
border: 1px solid transparent;
|
||||
transition:
|
||||
background 0.2s,
|
||||
color 0.2s,
|
||||
border-color 0.2s;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a.primary {
|
||||
background: var(--foreground);
|
||||
color: var(--background);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
a.secondary {
|
||||
border-color: var(--gray-alpha-200);
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
grid-row-start: 3;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer img {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
a.primary:hover {
|
||||
background: var(--button-primary-hover);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
a.secondary:hover {
|
||||
background: var(--button-secondary-hover);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.page {
|
||||
padding: 32px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.main {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main ol {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ctas {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ctas a {
|
||||
font-size: 14px;
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
a.secondary {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.logo {
|
||||
filter: invert();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import Head from "next/head";
|
||||
import Script from "next/script";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>Create Next App</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Box
|
||||
display={"flex"}
|
||||
flexDirection={"column"}
|
||||
maxWidth={"60%"}
|
||||
margin={"50px auto"}
|
||||
>
|
||||
<div>NymNode X</div>
|
||||
<div>Info about NymNode X</div>
|
||||
<div id="remark42"></div>
|
||||
|
||||
{/* Configuration Script */}
|
||||
<Script
|
||||
id="remark-config"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
var remark_config = {
|
||||
host: 'http://localhost:8081', // Updated to match the REMARK_URL
|
||||
site_id: 'remark42',
|
||||
components: ['embed', 'last-comments'],
|
||||
max_shown_comments: 100,
|
||||
theme: 'light',
|
||||
page_title: 'My custom title for a page',
|
||||
locale: 'en',
|
||||
show_email_subscription: false,
|
||||
simple_view: true,
|
||||
no_footer: true
|
||||
};
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Initialization Script */}
|
||||
<Script
|
||||
id="remark-init"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
!function(e,n){for(var o=0;o<e.length;o++){var r=n.createElement("script"),c=".js",d=n.head||n.body;"noModule"in r?(r.type="module",c=".mjs"):r.async=!0,r.defer=!0,r.src=remark_config.host+"/web/"+e[o]+c,d.appendChild(r)}}(remark_config.components||["embed"],document);
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
After Width: | Height: | Size: 992 B |
|
After Width: | Height: | Size: 963 B |
@@ -0,0 +1,190 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import {
|
||||
EXPLORER_API,
|
||||
COSMOS_API,
|
||||
VALIDATOR_API_EPOCH,
|
||||
VALIDATOR_API_SUPPLY,
|
||||
HARBOURMASTER_API_SUMMARY,
|
||||
HARBOURMASTER_API_MIXNODES_STATS,
|
||||
HARBOURMASTER_API_BASE,
|
||||
CURRENT_EPOCH,
|
||||
CURRENT_EPOCH_REWARDS,
|
||||
CIRCULATING_NYM_SUPPLY,
|
||||
} from "../urls";
|
||||
|
||||
export interface ExplorerData {
|
||||
circulatingNymSupplyData: any;
|
||||
nymNodesData: any;
|
||||
packetsAndStakingData: any;
|
||||
currentEpochData: any;
|
||||
currentEpochRewardsData: any;
|
||||
}
|
||||
|
||||
export interface ExplorerCache {
|
||||
data?: ExplorerData;
|
||||
lastUpdated?: Date;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// Extend the global object with our custom property
|
||||
var 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;
|
||||
}
|
||||
|
||||
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,307 @@
|
||||
import * as React from "react";
|
||||
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 KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import { Card, CardContent } from "@mui/material";
|
||||
import { ExplorerStaticProgressBar } from "./ExplorerStaticProgressBar";
|
||||
import { MultiSegmentProgressBar } from "./ExplorerMultiSegmentProgressBar";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import CircleIcon from "@mui/icons-material/Circle";
|
||||
|
||||
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 TABLET_WIDTH = "(min-width:700px)";
|
||||
|
||||
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>
|
||||
<ExplorerStaticProgressBar
|
||||
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, i) => (
|
||||
<TableRow key={i}>
|
||||
<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, i) => 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></TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography textTransform={"uppercase"}>Type</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography textTransform={"uppercase"}>
|
||||
Amount / Value
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, i) => (
|
||||
<Row
|
||||
key={i}
|
||||
{...row}
|
||||
isLastRow={i === rows.length - 1}
|
||||
progressBarColor={progressBarColours[i]}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,391 @@
|
||||
import { Card, CardContent, Typography, Box, Button } from "@mui/material";
|
||||
import React, { FC, ReactElement, ReactEventHandler, useEffect } from "react";
|
||||
import { ExplorerLineChart, IExplorerLineChartData } from "./ExplorerLineChart";
|
||||
import {
|
||||
ExplorerDynamicProgressBar,
|
||||
IExplorerDynamicProgressBarProps,
|
||||
} from "./ExplorerDynamicProgressBar";
|
||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
||||
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
|
||||
import { NymTokenSVG } from "../icons/NymTokenSVG";
|
||||
import { CopyToClipboard } from "@nymproject/react/clipboard/CopyToClipboard";
|
||||
import Image from "next/image";
|
||||
import profileImagePlaceholder from "../../public/profileImagePlaceholder.png";
|
||||
import Flag from "react-world-flags";
|
||||
import { QRCodeCanvas } from "qrcode.react";
|
||||
import StarIcon from "@mui/icons-material/Star";
|
||||
import Script from "next/script";
|
||||
import { useMainContext } from "../context/main";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
remark_config: {
|
||||
host: string;
|
||||
site_id: string;
|
||||
components: string[];
|
||||
max_shown_comments: number;
|
||||
theme: string;
|
||||
locale: string;
|
||||
show_email_subscription: boolean;
|
||||
simple_view: boolean;
|
||||
no_footer: boolean;
|
||||
};
|
||||
REMARK42: {
|
||||
createInstance: (config: typeof window.remark_config) => void;
|
||||
changeTheme: (theme: "light" | "dark") => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface ICardUpDownPriceLineProps {
|
||||
percentage: number;
|
||||
numberWentUp: boolean;
|
||||
}
|
||||
const CardUpDownPriceLine = (
|
||||
props: ICardUpDownPriceLineProps
|
||||
): ReactElement => {
|
||||
const { percentage, numberWentUp } = props;
|
||||
return (
|
||||
<Box mb={3} display={"flex"}>
|
||||
{numberWentUp ? (
|
||||
<ArrowUpwardIcon sx={{ color: "#00CA33" }} fontSize="small" />
|
||||
) : (
|
||||
<ArrowDownwardIcon sx={{ color: "#DF1400" }} fontSize="small" />
|
||||
)}
|
||||
<Typography 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}>
|
||||
<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 mb={3}>
|
||||
{rows.map((row, i) => {
|
||||
return (
|
||||
<Box
|
||||
key={i}
|
||||
paddingTop={2}
|
||||
paddingBottom={2}
|
||||
display={"flex"}
|
||||
justifyContent={"space-between"}
|
||||
borderBottom={i === 0 ? "1px solid #C3D7D7" : "none"}
|
||||
>
|
||||
<Typography>{row.key}</Typography>
|
||||
<Typography>{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} mb={3}>
|
||||
<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>{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"}
|
||||
mb={3}
|
||||
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 mb={3}>
|
||||
{ratings.map((rating, i) => {
|
||||
const Stars = () => {
|
||||
const stars = [];
|
||||
for (let i = 0; i < rating.numberOfStars; i++) {
|
||||
stars.push(<StarIcon sx={{ color: "#14E76F" }} fontSize="small" />);
|
||||
}
|
||||
return stars;
|
||||
};
|
||||
const RatingTitle = () => {
|
||||
if (rating.numberOfStars === 1) {
|
||||
return <Typography>Bad</Typography>;
|
||||
} else if (rating.numberOfStars === 2) {
|
||||
return <Typography>Bad</Typography>;
|
||||
} else if (rating.numberOfStars === 3) {
|
||||
return <Typography>ok</Typography>;
|
||||
} else if (rating.numberOfStars === 4) {
|
||||
return <Typography>Good</Typography>;
|
||||
} else {
|
||||
return <Typography>Excellent</Typography>;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
key={i}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
const CardChat = () => {
|
||||
const { mode } = useMainContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
// Set Remark42 configuration on the window object
|
||||
window.remark_config = {
|
||||
host: "https://remark.blockfend.com",
|
||||
site_id: "nym-explorer-test",
|
||||
components: ["embed", "last-comments"],
|
||||
max_shown_comments: 100,
|
||||
theme: mode === "light" ? "light" : "dark",
|
||||
locale: "en",
|
||||
show_email_subscription: false,
|
||||
simple_view: true,
|
||||
no_footer: true,
|
||||
};
|
||||
|
||||
// Dynamically load the Remark42 script if it doesn't exist
|
||||
if (!document.getElementById("remark42-script")) {
|
||||
const script = document.createElement("script");
|
||||
script.src = `${window.remark_config.host}/web/embed.js`;
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.id = "remark42-script";
|
||||
document.body.appendChild(script);
|
||||
} else if (window.REMARK42) {
|
||||
// Re-initialize if the script is already loaded
|
||||
window.REMARK42.createInstance(window.remark_config);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// React to mode changes and update Remark42 theme
|
||||
useEffect(() => {
|
||||
if (window.REMARK42 && window.REMARK42.changeTheme) {
|
||||
window.REMARK42.changeTheme(mode === "dark" ? "dark" : "light");
|
||||
}
|
||||
}, [mode]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<div id="remark42" className="remark"></div>
|
||||
<Script
|
||||
id="remark-init"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
if (window.REMARK42) {
|
||||
window.REMARK42.createInstance(window.remark_config);
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export type ContentCardProps = {
|
||||
overTitle?: string;
|
||||
profileImage?: ICardProileImage;
|
||||
title?: string | number;
|
||||
profileCountry?: ICardProfileCountry;
|
||||
upDownLine?: ICardUpDownPriceLineProps;
|
||||
titlePrice?: ICardTitlePriceProps;
|
||||
dataRows?: ICardDataRowsProps;
|
||||
graph?: { data: Array<IExplorerLineChartData>; color: string; label: string };
|
||||
progressBar?: IExplorerDynamicProgressBarProps;
|
||||
paragraph?: string;
|
||||
onClick?: ReactEventHandler;
|
||||
nymAddress?: ICardCopyAddressProps;
|
||||
identityKey?: ICardCopyAddressProps;
|
||||
qrCode?: ICardQRCodeProps;
|
||||
ratings?: ICardRatingsProps;
|
||||
chat?: boolean;
|
||||
button?: {
|
||||
onClick: () => void;
|
||||
label: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const ExplorerCard: FC<ContentCardProps> = ({
|
||||
title,
|
||||
titlePrice,
|
||||
overTitle,
|
||||
upDownLine,
|
||||
dataRows,
|
||||
graph,
|
||||
progressBar,
|
||||
paragraph,
|
||||
onClick,
|
||||
profileImage,
|
||||
profileCountry,
|
||||
nymAddress,
|
||||
identityKey,
|
||||
qrCode,
|
||||
ratings,
|
||||
chat,
|
||||
button,
|
||||
}) => (
|
||||
<Card onClick={onClick} sx={{ height: "100%", borderRadius: "unset" }}>
|
||||
<CardContent>
|
||||
{overTitle && (
|
||||
<Typography fontSize={14} mb={3} textTransform={"uppercase"}>
|
||||
{overTitle}
|
||||
</Typography>
|
||||
)}
|
||||
{profileImage && <CardProfileImage {...profileImage} />}
|
||||
{title && (
|
||||
<Typography fontSize={24} mb={3}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
{profileCountry && <CardProfileCountry {...profileCountry} />}
|
||||
{upDownLine && <CardUpDownPriceLine {...upDownLine} />}
|
||||
{titlePrice && <CardTitlePrice {...titlePrice} />}
|
||||
{qrCode && <CardQRCode {...qrCode} />}
|
||||
{nymAddress && <CardCopyAddress {...nymAddress} />}
|
||||
{identityKey && <CardCopyAddress {...identityKey} />}
|
||||
{dataRows && <CardDataRows {...dataRows} />}
|
||||
{ratings && <CardRatings {...ratings} />}
|
||||
{graph && (
|
||||
<Box mb={3}>
|
||||
<ExplorerLineChart
|
||||
data={graph.data}
|
||||
color={graph.color}
|
||||
label={graph.label}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{progressBar && (
|
||||
<Box mb={3}>
|
||||
<ExplorerDynamicProgressBar {...progressBar} />
|
||||
</Box>
|
||||
)}
|
||||
{paragraph && <Typography>{paragraph}</Typography>}
|
||||
{chat && <CardChat />}
|
||||
{button && (
|
||||
<Button onClick={button.onClick} variant="contained">
|
||||
{button.label}
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -0,0 +1,101 @@
|
||||
import * as React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import LinearProgress from "@mui/material/LinearProgress";
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
export interface IExplorerDynamicProgressBarProps {
|
||||
title?: string;
|
||||
start: string; // Start timestamp as ISO 8601 string
|
||||
showEpoch: boolean;
|
||||
}
|
||||
|
||||
export const ExplorerDynamicProgressBar = (
|
||||
props: IExplorerDynamicProgressBarProps
|
||||
) => {
|
||||
const { start, showEpoch, title } = 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 (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%" }}>
|
||||
{title && (
|
||||
<Typography mb={2} textTransform={"uppercase"}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
sx={{
|
||||
backgroundColor: "#CAD6D7",
|
||||
"& .MuiLinearProgress-bar": {
|
||||
backgroundColor: "#14E76F",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{showEpoch && (
|
||||
<Box mt={2}>
|
||||
<Box display={"flex"} justifyContent={"space-between"}>
|
||||
<Typography textTransform={"uppercase"}>START:</Typography>
|
||||
<Typography> {startTime ? formatDate(startTime) : ""}</Typography>
|
||||
</Box>
|
||||
<Box display={"flex"} justifyContent={"space-between"}>
|
||||
<Typography textTransform={"uppercase"}>END:</Typography>
|
||||
<Typography> {endTime ? formatDate(endTime) : ""}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
import { Box, useMediaQuery, useTheme } from "@mui/material";
|
||||
import dynamic from "next/dynamic";
|
||||
import Loading from "./Loading";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const LineChart = dynamic(
|
||||
() => import("@nivo/line").then((m) => m.ResponsiveLine),
|
||||
{
|
||||
loading: () => <Loading />,
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
export interface IExplorerLineChartData {
|
||||
date_utc: string;
|
||||
numericData?: number;
|
||||
// purpleLineNumericData?: number;
|
||||
}
|
||||
|
||||
interface IAxes {
|
||||
x: Date;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface ILineAxes {
|
||||
id: string;
|
||||
data: Array<IAxes>;
|
||||
}
|
||||
|
||||
export const ExplorerLineChart = ({
|
||||
data,
|
||||
color,
|
||||
label,
|
||||
}: {
|
||||
data: Array<IExplorerLineChartData>;
|
||||
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<IExplorerLineChartData>) => {
|
||||
const lineData: ILineAxes = {
|
||||
id: label,
|
||||
data: [],
|
||||
};
|
||||
|
||||
// const purpleLineData: ILineAxes = {
|
||||
// id: "Numeric Data 2",
|
||||
// data: [],
|
||||
// };
|
||||
|
||||
data.map((item: any) => {
|
||||
const axesGreenLineData: IAxes = {
|
||||
x: new Date(item.date_utc),
|
||||
y: item.numericData,
|
||||
};
|
||||
|
||||
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;
|
||||
} else {
|
||||
throw new Error("Unexpected value");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box width={"100%"} height={isDesktop ? 200 : 150}>
|
||||
{chartData && (
|
||||
<LineChart
|
||||
curve="monotoneX"
|
||||
colors={[color]}
|
||||
data={chartData}
|
||||
animate
|
||||
enableSlices="x"
|
||||
margin={{
|
||||
bottom: 24,
|
||||
left: 36,
|
||||
right: 16,
|
||||
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 4 days" : "every day",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
export interface MultiSegmentProgressBarProps {
|
||||
values: { percentage: number; color: string }[]; // Array of percentage and color pairs
|
||||
height?: number; // Optional height, default is 8
|
||||
borderRadius?: number; // 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, height = 8, borderRadius = 4, backgroundColor = "#CAD6D7" }) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
height,
|
||||
borderRadius,
|
||||
overflow: "hidden",
|
||||
backgroundColor,
|
||||
}}
|
||||
>
|
||||
{values.map((value, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
width: `${value.percentage}%`,
|
||||
backgroundColor: value.color,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import LinearProgress from "@mui/material/LinearProgress";
|
||||
|
||||
export interface IExplorerStaticProgressBarProps {
|
||||
color: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export const ExplorerStaticProgressBar = (
|
||||
props: IExplorerStaticProgressBarProps
|
||||
) => {
|
||||
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,65 @@
|
||||
import React, { useState } from "react";
|
||||
import { Box, Button } from "@mui/material";
|
||||
|
||||
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: "8px",
|
||||
overflow: "hidden",
|
||||
width: "200px",
|
||||
height: "40px",
|
||||
border: "2px solid black",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={() => handleSwitch("left")}
|
||||
sx={{
|
||||
flex: 1,
|
||||
backgroundColor: selectedSide === "left" ? "black" : "white",
|
||||
color: selectedSide === "left" ? "white" : "black",
|
||||
borderRadius: 0,
|
||||
"&:hover": {
|
||||
backgroundColor: selectedSide === "left" ? "black" : "lightgray",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{leftLabel}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleSwitch("right")}
|
||||
sx={{
|
||||
flex: 1,
|
||||
backgroundColor: selectedSide === "right" ? "black" : "white",
|
||||
color: selectedSide === "right" ? "white" : "black",
|
||||
borderRadius: 0,
|
||||
"&:hover": {
|
||||
backgroundColor: selectedSide === "right" ? "black" : "lightgray",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{rightLabel}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TwoSidedSwitch;
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from "react";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Box from "@mui/material/Box";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginY: 10,
|
||||
width: 1,
|
||||
}}
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Card, CardContent, Typography, Box, Button } from "@mui/material";
|
||||
import { ICardDataRowsProps } from "./ExplorerCard";
|
||||
|
||||
export interface IStakingCardProps {
|
||||
title?: string;
|
||||
titleCenter?: string;
|
||||
paragraphCenter?: string;
|
||||
closeButton?: {
|
||||
onClick: () => void;
|
||||
};
|
||||
addressInput?: boolean;
|
||||
amountInput?: boolean;
|
||||
dataRows?: ICardDataRowsProps;
|
||||
blockExplorerLink?: string;
|
||||
backButton?: {
|
||||
onClick: () => void;
|
||||
};
|
||||
nextButton?: {
|
||||
onClick: () => void;
|
||||
};
|
||||
}
|
||||
export const StakingCard = (props: IStakingCardProps) => {
|
||||
const {
|
||||
title,
|
||||
titleCenter,
|
||||
closeButton,
|
||||
paragraphCenter,
|
||||
addressInput,
|
||||
amountInput,
|
||||
dataRows,
|
||||
blockExplorerLink,
|
||||
backButton,
|
||||
nextButton,
|
||||
} = props;
|
||||
return (
|
||||
<Card sx={{ height: "100%", borderRadius: "unset" }}>
|
||||
<CardContent></CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as React from "react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
export const NymTokenSVG: FCWithChildren = () => {
|
||||
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"
|
||||
>
|
||||
<g clip-path="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>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { Metadata } from 'next'
|
||||
import '@interchain-ui/react/styles'
|
||||
import { App } from './App'
|
||||
import type { Metadata } from "next";
|
||||
import "@interchain-ui/react/styles";
|
||||
import { App } from "./App";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Nym Network Explorer',
|
||||
}
|
||||
title: "Nym Network Explorer",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
@@ -17,5 +17,5 @@ export default function RootLayout({
|
||||
<App>{children}</App>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import * as React from 'react'
|
||||
import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { GatewayBond } from '@/app/typeDefs/explorer-api'
|
||||
import { ColumnsType, DetailTable } from '@/app/components/DetailTable'
|
||||
import * as React from "react";
|
||||
import { Alert, AlertTitle, Box, CircularProgress, Grid } from "@mui/material";
|
||||
import { useParams } from "next/navigation";
|
||||
import { GatewayBond } from "@/app/typeDefs/explorer-api";
|
||||
import { ColumnsType, DetailTable } from "@/app/components/DetailTable";
|
||||
import {
|
||||
gatewayEnrichedToGridRow,
|
||||
GatewayEnrichedRowType,
|
||||
} from '@/app/components/Gateways/Gateways'
|
||||
import { ComponentError } from '@/app/components/ComponentError'
|
||||
import { ContentCard } from '@/app/components/ContentCard'
|
||||
import { TwoColSmallTable } from '@/app/components/TwoColSmallTable'
|
||||
import { UptimeChart } from '@/app/components/UptimeChart'
|
||||
} from "@/app/components/Gateways/Gateways";
|
||||
import { ComponentError } from "@/app/components/ComponentError";
|
||||
import { ContentCard } from "@/app/components/ContentCard";
|
||||
import { TwoColSmallTable } from "@/app/components/TwoColSmallTable";
|
||||
import { UptimeChart } from "@/app/components/UptimeChart";
|
||||
import {
|
||||
GatewayContextProvider,
|
||||
useGatewayContext,
|
||||
} from '@/app/context/gateway'
|
||||
import { useMainContext } from '@/app/context/main'
|
||||
import { Title } from '@/app/components/Title'
|
||||
} from "@/app/context/gateway";
|
||||
import { useMainContext } from "@/app/context/main";
|
||||
import { Title } from "@/app/components/Title";
|
||||
|
||||
const columns: ColumnsType[] = [
|
||||
{
|
||||
field: 'identity_key',
|
||||
title: 'Identity Key',
|
||||
headerAlign: 'left',
|
||||
field: "identity_key",
|
||||
title: "Identity Key",
|
||||
headerAlign: "left",
|
||||
width: 230,
|
||||
},
|
||||
{
|
||||
field: 'bond',
|
||||
title: 'Bond',
|
||||
headerAlign: 'left',
|
||||
field: "bond",
|
||||
title: "Bond",
|
||||
headerAlign: "left",
|
||||
},
|
||||
{
|
||||
field: 'node_performance',
|
||||
title: 'Routing Score',
|
||||
headerAlign: 'left',
|
||||
field: "node_performance",
|
||||
title: "Routing Score",
|
||||
headerAlign: "left",
|
||||
tooltipInfo:
|
||||
"Gateway's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test",
|
||||
},
|
||||
{
|
||||
field: 'avgUptime',
|
||||
title: 'Avg. Score',
|
||||
headerAlign: 'left',
|
||||
field: "avgUptime",
|
||||
title: "Avg. Score",
|
||||
headerAlign: "left",
|
||||
tooltipInfo: "Gateway's average routing score in the last 24 hours",
|
||||
},
|
||||
{
|
||||
field: 'host',
|
||||
title: 'IP',
|
||||
headerAlign: 'left',
|
||||
field: "host",
|
||||
title: "IP",
|
||||
headerAlign: "left",
|
||||
width: 99,
|
||||
},
|
||||
{
|
||||
field: 'location',
|
||||
title: 'Location',
|
||||
headerAlign: 'left',
|
||||
field: "location",
|
||||
title: "Location",
|
||||
headerAlign: "left",
|
||||
},
|
||||
{
|
||||
field: 'owner',
|
||||
title: 'Owner',
|
||||
headerAlign: 'left',
|
||||
field: "owner",
|
||||
title: "Owner",
|
||||
headerAlign: "left",
|
||||
},
|
||||
{
|
||||
field: 'version',
|
||||
title: 'Version',
|
||||
headerAlign: 'left',
|
||||
field: "version",
|
||||
title: "Version",
|
||||
headerAlign: "left",
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* Shows gateway details
|
||||
@@ -74,26 +74,26 @@ const columns: ColumnsType[] = [
|
||||
const PageGatewayDetailsWithState = ({
|
||||
selectedGateway,
|
||||
}: {
|
||||
selectedGateway: GatewayBond | undefined
|
||||
selectedGateway: GatewayBond | undefined;
|
||||
}) => {
|
||||
const [enrichGateway, setEnrichGateway] =
|
||||
React.useState<GatewayEnrichedRowType>()
|
||||
const [status, setStatus] = React.useState<number[] | undefined>()
|
||||
const { uptimeReport, uptimeStory } = useGatewayContext()
|
||||
React.useState<GatewayEnrichedRowType>();
|
||||
const [status, setStatus] = React.useState<number[] | undefined>();
|
||||
const { uptimeReport, uptimeStory } = useGatewayContext();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (uptimeReport?.data && selectedGateway) {
|
||||
setEnrichGateway(
|
||||
gatewayEnrichedToGridRow(selectedGateway, uptimeReport.data)
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [uptimeReport, selectedGateway])
|
||||
}, [uptimeReport, selectedGateway]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (enrichGateway) {
|
||||
setStatus([enrichGateway.mixPort, enrichGateway.clientsPort])
|
||||
setStatus([enrichGateway.mixPort, enrichGateway.clientsPort]);
|
||||
}
|
||||
}, [enrichGateway])
|
||||
}, [enrichGateway]);
|
||||
|
||||
return (
|
||||
<Box component="main">
|
||||
@@ -115,7 +115,7 @@ const PageGatewayDetailsWithState = ({
|
||||
<ContentCard title="Gateway Status">
|
||||
<TwoColSmallTable
|
||||
loading={false}
|
||||
keys={['Mix port', 'Client WS API Port']}
|
||||
keys={["Mix port", "Client WS API Port"]}
|
||||
values={status.map((each) => each)}
|
||||
icons={status.map((elem) => !!elem)}
|
||||
/>
|
||||
@@ -137,39 +137,40 @@ const PageGatewayDetailsWithState = ({
|
||||
</ContentCard>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={12} md={8}></Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Guard component to handle loadingW and not found states
|
||||
*/
|
||||
const PageGatewayDetailGuard = () => {
|
||||
const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond>()
|
||||
const { gateways } = useMainContext()
|
||||
const { id } = useParams()
|
||||
const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond>();
|
||||
const { gateways } = useMainContext();
|
||||
const { id } = useParams();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (gateways?.data) {
|
||||
setSelectedGateway(
|
||||
gateways.data.find((g) => g.gateway.identity_key === id)
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [gateways, id])
|
||||
}, [gateways, id]);
|
||||
|
||||
if (gateways?.isLoading) {
|
||||
return <CircularProgress />
|
||||
return <CircularProgress />;
|
||||
}
|
||||
|
||||
if (gateways?.error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(gateways?.error)
|
||||
console.error(gateways?.error);
|
||||
return (
|
||||
<Alert severity="error">
|
||||
Oh no! Could not load mixnode <code>{id || ''}</code>
|
||||
Oh no! Could not load mixnode <code>{id || ""}</code>
|
||||
</Alert>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// loaded, but not found
|
||||
@@ -177,31 +178,31 @@ const PageGatewayDetailGuard = () => {
|
||||
return (
|
||||
<Alert severity="warning">
|
||||
<AlertTitle>Gateway not found</AlertTitle>
|
||||
Sorry, we could not find a mixnode with id <code>{id || ''}</code>
|
||||
Sorry, we could not find a mixnode with id <code>{id || ""}</code>
|
||||
</Alert>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return <PageGatewayDetailsWithState selectedGateway={selectedGateway} />
|
||||
}
|
||||
return <PageGatewayDetailsWithState selectedGateway={selectedGateway} />;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper component that adds the mixnode content based on the `id` in the address URL
|
||||
*/
|
||||
const PageGatewayDetail = () => {
|
||||
const { id } = useParams()
|
||||
const { id } = useParams();
|
||||
|
||||
if (!id || typeof id !== 'string') {
|
||||
if (!id || typeof id !== "string") {
|
||||
return (
|
||||
<Alert severity="error">Oh no! No mixnode identity key specified</Alert>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GatewayContextProvider gatewayIdentityKey={id}>
|
||||
<PageGatewayDetailGuard />
|
||||
</GatewayContextProvider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PageGatewayDetail
|
||||
export default PageGatewayDetail;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import * as React from 'react'
|
||||
import * as React from "react";
|
||||
import {
|
||||
Alert,
|
||||
AlertTitle,
|
||||
@@ -8,75 +8,82 @@ import {
|
||||
CircularProgress,
|
||||
Grid,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { ColumnsType, DetailTable } from '@/app/components/DetailTable'
|
||||
import { BondBreakdownTable } from '@/app/components/MixNodes/BondBreakdown'
|
||||
} from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
import { ColumnsType, DetailTable } from "@/app/components/DetailTable";
|
||||
import { BondBreakdownTable } from "@/app/components/MixNodes/BondBreakdown";
|
||||
import {
|
||||
DelegatorsInfoTable,
|
||||
EconomicsInfoColumns,
|
||||
EconomicsInfoRows,
|
||||
} from '@/app/components/MixNodes/Economics'
|
||||
import { ComponentError } from '@/app/components/ComponentError'
|
||||
import { ContentCard } from '@/app/components/ContentCard'
|
||||
import { TwoColSmallTable } from '@/app/components/TwoColSmallTable'
|
||||
import { UptimeChart } from '@/app/components/UptimeChart'
|
||||
import { WorldMap } from '@/app/components/WorldMap'
|
||||
import { MixNodeDetailSection } from '@/app/components/MixNodes/DetailSection'
|
||||
} from "@/app/components/MixNodes/Economics";
|
||||
import { ComponentError } from "@/app/components/ComponentError";
|
||||
import { ContentCard } from "@/app/components/ContentCard";
|
||||
import { TwoColSmallTable } from "@/app/components/TwoColSmallTable";
|
||||
import { UptimeChart } from "@/app/components/UptimeChart";
|
||||
import { WorldMap } from "@/app/components/WorldMap";
|
||||
import { MixNodeDetailSection } from "@/app/components/MixNodes/DetailSection";
|
||||
import {
|
||||
MixnodeContextProvider,
|
||||
useMixnodeContext,
|
||||
} from '@/app/context/mixnode'
|
||||
import { Title } from '@/app/components/Title'
|
||||
import { useIsMobile } from '@/app/hooks/useIsMobile'
|
||||
import { useParams } from 'next/navigation'
|
||||
} from "@/app/context/mixnode";
|
||||
import { Title } from "@/app/components/Title";
|
||||
import { useIsMobile } from "@/app/hooks/useIsMobile";
|
||||
import { useParams } from "next/navigation";
|
||||
import Script from "next/script";
|
||||
import Head from "next/head";
|
||||
import { useEffect } from "react";
|
||||
import { useMainContext } from "@/app/context/main";
|
||||
import { ExplorerCard } from "@/app/components/ExplorerCard";
|
||||
|
||||
const columns: ColumnsType[] = [
|
||||
{
|
||||
field: 'owner',
|
||||
title: 'Owner',
|
||||
width: '15%',
|
||||
field: "owner",
|
||||
title: "Owner",
|
||||
width: "15%",
|
||||
},
|
||||
{
|
||||
field: 'identity_key',
|
||||
title: 'Identity Key',
|
||||
width: '15%',
|
||||
field: "identity_key",
|
||||
title: "Identity Key",
|
||||
width: "15%",
|
||||
},
|
||||
|
||||
{
|
||||
field: 'bond',
|
||||
title: 'Stake',
|
||||
width: '12.5%',
|
||||
field: "bond",
|
||||
title: "Stake",
|
||||
width: "12.5%",
|
||||
},
|
||||
{
|
||||
field: 'stake_saturation',
|
||||
title: 'Stake Saturation',
|
||||
width: '12.5%',
|
||||
field: "stake_saturation",
|
||||
title: "Stake Saturation",
|
||||
width: "12.5%",
|
||||
tooltipInfo:
|
||||
'Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 940k NYMs, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set.',
|
||||
"Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 940k NYMs, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set.",
|
||||
},
|
||||
{
|
||||
field: 'self_percentage',
|
||||
width: '10%',
|
||||
title: 'Bond %',
|
||||
field: "self_percentage",
|
||||
width: "10%",
|
||||
title: "Bond %",
|
||||
tooltipInfo:
|
||||
"Percentage of the operator's bond to the total stake on the node",
|
||||
},
|
||||
|
||||
{
|
||||
field: 'host',
|
||||
width: '10%',
|
||||
title: 'Host',
|
||||
field: "host",
|
||||
width: "10%",
|
||||
title: "Host",
|
||||
},
|
||||
{
|
||||
field: 'location',
|
||||
title: 'Location',
|
||||
field: "location",
|
||||
title: "Location",
|
||||
},
|
||||
|
||||
{
|
||||
field: 'layer',
|
||||
title: 'Layer',
|
||||
field: "layer",
|
||||
title: "Layer",
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* Shows mix node details
|
||||
@@ -90,10 +97,53 @@ const PageMixnodeDetailWithState = () => {
|
||||
status,
|
||||
uptimeStory,
|
||||
uniqDelegations,
|
||||
} = useMixnodeContext()
|
||||
const isMobile = useIsMobile()
|
||||
} = useMixnodeContext();
|
||||
const isMobile = useIsMobile();
|
||||
const { mode } = useMainContext();
|
||||
|
||||
// useEffect(() => {
|
||||
// if (typeof window !== "undefined") {
|
||||
// // Set Remark42 configuration on the window object
|
||||
// window.remark_config = {
|
||||
// host: "http://localhost:8081",
|
||||
// site_id: "remark42",
|
||||
// components: ["embed", "last-comments"],
|
||||
// max_shown_comments: 100,
|
||||
// theme: mode === "light" ? "light" : "dark",
|
||||
// page_title: "My custom title for a page",
|
||||
// locale: "en",
|
||||
// show_email_subscription: false,
|
||||
// simple_view: true,
|
||||
// no_footer: true,
|
||||
// };
|
||||
|
||||
// // Dynamically load the Remark42 script if it doesn't exist
|
||||
// if (!document.getElementById("remark42-script")) {
|
||||
// const script = document.createElement("script");
|
||||
// script.src = `${window.remark_config.host}/web/embed.js`;
|
||||
// script.async = true;
|
||||
// script.defer = true;
|
||||
// script.id = "remark42-script";
|
||||
// document.body.appendChild(script);
|
||||
// } else if (window.REMARK42) {
|
||||
// // Re-initialize if the script is already loaded
|
||||
// window.REMARK42.createInstance(window.remark_config);
|
||||
// }
|
||||
// }
|
||||
// }, []);
|
||||
|
||||
// // React to mode changes and update Remark42 theme
|
||||
// useEffect(() => {
|
||||
// if (window.REMARK42 && window.REMARK42.changeTheme) {
|
||||
// window.REMARK42.changeTheme(mode === "dark" ? "dark" : "light");
|
||||
// }
|
||||
// }, [mode]);
|
||||
|
||||
return (
|
||||
<Box component="main">
|
||||
<Head>
|
||||
<title>Mixnode Detail</title>
|
||||
</Head>
|
||||
<Title text="Mixnode Detail" />
|
||||
<Grid container spacing={2} mt={1} mb={6}>
|
||||
<Grid item xs={12}>
|
||||
@@ -105,9 +155,9 @@ const PageMixnodeDetailWithState = () => {
|
||||
)}
|
||||
{mixNodeRow?.blacklisted && (
|
||||
<Typography
|
||||
textAlign={isMobile ? 'left' : 'right'}
|
||||
textAlign={isMobile ? "left" : "right"}
|
||||
fontSize="smaller"
|
||||
sx={{ color: 'error.main' }}
|
||||
sx={{ color: "error.main" }}
|
||||
>
|
||||
This node is having a poor performance
|
||||
</Typography>
|
||||
@@ -153,7 +203,7 @@ const PageMixnodeDetailWithState = () => {
|
||||
loading={stats.isLoading}
|
||||
error={stats?.error?.message}
|
||||
title="Since startup"
|
||||
keys={['Received', 'Sent', 'Explicitly dropped']}
|
||||
keys={["Received", "Sent", "Explicitly dropped"]}
|
||||
values={[
|
||||
stats?.data?.packets_received_since_startup || 0,
|
||||
stats?.data?.packets_sent_since_startup || 0,
|
||||
@@ -164,7 +214,7 @@ const PageMixnodeDetailWithState = () => {
|
||||
loading={stats.isLoading}
|
||||
error={stats?.error?.message}
|
||||
title="Since last update"
|
||||
keys={['Received', 'Sent', 'Explicitly dropped']}
|
||||
keys={["Received", "Sent", "Explicitly dropped"]}
|
||||
values={[
|
||||
stats?.data?.packets_received_since_last_update || 0,
|
||||
stats?.data?.packets_sent_since_last_update || 0,
|
||||
@@ -204,7 +254,7 @@ const PageMixnodeDetailWithState = () => {
|
||||
<TwoColSmallTable
|
||||
loading={status.isLoading}
|
||||
error={status?.error?.message}
|
||||
keys={['Mix port', 'Verloc port', 'HTTP port']}
|
||||
keys={["Mix port", "Verloc port", "HTTP port"]}
|
||||
values={[1789, 1790, 8000].map((each) => each)}
|
||||
icons={
|
||||
(status?.data?.ports && Object.values(status.data.ports)) || [
|
||||
@@ -237,29 +287,32 @@ const PageMixnodeDetailWithState = () => {
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<ExplorerCard chat={true} overTitle="Test" />
|
||||
</Grid>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Guard component to handle loading and not found states
|
||||
*/
|
||||
const PageMixnodeDetailGuard = () => {
|
||||
const { mixNode } = useMixnodeContext()
|
||||
const { id } = useParams()
|
||||
const { mixNode } = useMixnodeContext();
|
||||
const { id } = useParams();
|
||||
|
||||
if (mixNode?.isLoading) {
|
||||
return <CircularProgress />
|
||||
return <CircularProgress />;
|
||||
}
|
||||
|
||||
if (mixNode?.error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(mixNode?.error)
|
||||
console.error(mixNode?.error);
|
||||
return (
|
||||
<Alert severity="error">
|
||||
Oh no! Could not load mixnode <code>{id || ''}</code>
|
||||
Oh no! Could not load mixnode <code>{id || ""}</code>
|
||||
</Alert>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// loaded, but not found
|
||||
@@ -267,31 +320,31 @@ const PageMixnodeDetailGuard = () => {
|
||||
return (
|
||||
<Alert severity="warning">
|
||||
<AlertTitle>Mixnode not found</AlertTitle>
|
||||
Sorry, we could not find a mixnode with id <code>{id || ''}</code>
|
||||
Sorry, we could not find a mixnode with id <code>{id || ""}</code>
|
||||
</Alert>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return <PageMixnodeDetailWithState />
|
||||
}
|
||||
return <PageMixnodeDetailWithState />;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper component that adds the mixnode content based on the `id` in the address URL
|
||||
*/
|
||||
const PageMixnodeDetail = () => {
|
||||
const { id } = useParams()
|
||||
const { id } = useParams();
|
||||
|
||||
if (!id || typeof id !== 'string') {
|
||||
if (!id || typeof id !== "string") {
|
||||
return (
|
||||
<Alert severity="error">Oh no! No mixnode identity key specified</Alert>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MixnodeContextProvider mixId={id}>
|
||||
<PageMixnodeDetailGuard />
|
||||
</MixnodeContextProvider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PageMixnodeDetail
|
||||
export default PageMixnodeDetail;
|
||||
|
||||
@@ -1,26 +1,288 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import React, { useEffect } from 'react'
|
||||
import { Box, Grid, Link, Typography } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||
import { PeopleAlt } from '@mui/icons-material'
|
||||
import { Title } from '@/app/components/Title'
|
||||
import { StatsCard } from '@/app/components/StatsCard'
|
||||
import { MixnodesSVG } from '@/app/icons/MixnodesSVG'
|
||||
import { Icons } from '@/app/components/Icons'
|
||||
import { GatewaysSVG } from '@/app/icons/GatewaysSVG'
|
||||
import { ValidatorsSVG } from '@/app/icons/ValidatorsSVG'
|
||||
import { ContentCard } from '@/app/components/ContentCard'
|
||||
import { WorldMap } from '@/app/components/WorldMap'
|
||||
import { BIG_DIPPER } from '@/app/api/constants'
|
||||
import { formatNumber } from '@/app/utils'
|
||||
import { useMainContext } from './context/main'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Box, Grid, Link, Typography } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
|
||||
import { PeopleAlt } from "@mui/icons-material";
|
||||
import { Title } from "@/app/components/Title";
|
||||
import { StatsCard } from "@/app/components/StatsCard";
|
||||
import { MixnodesSVG } from "@/app/icons/MixnodesSVG";
|
||||
import { Icons } from "@/app/components/Icons";
|
||||
import { GatewaysSVG } from "@/app/icons/GatewaysSVG";
|
||||
import { ValidatorsSVG } from "@/app/icons/ValidatorsSVG";
|
||||
import { ContentCard } from "@/app/components/ContentCard";
|
||||
import { WorldMap } from "@/app/components/WorldMap";
|
||||
import { BIG_DIPPER } from "@/app/api/constants";
|
||||
import { formatNumber } from "@/app/utils";
|
||||
import { useMainContext } from "./context/main";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ContentCardProps, ExplorerCard } from "./components/ExplorerCard";
|
||||
import { ExplorerData, getCacheExplorerData } from "./api/explorer";
|
||||
import { IExplorerLineChartData } from "./components/ExplorerLineChart";
|
||||
import {
|
||||
AccountStatsCard,
|
||||
IAccountStatsCardProps,
|
||||
} from "./components/AccountStatsCard";
|
||||
import TwoSidedSwitch from "./components/ExplorerSwitchButton";
|
||||
|
||||
const PageOverview = () => {
|
||||
const theme = useTheme()
|
||||
const router = useRouter()
|
||||
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", numberOfStars: 4 },
|
||||
{ title: "Rating", numberOfStars: 2 },
|
||||
{ title: "Rating", numberOfStars: 3 },
|
||||
],
|
||||
},
|
||||
chat: true,
|
||||
paragraph: "Additional line",
|
||||
button: {
|
||||
onClick: () => {},
|
||||
label: "Label",
|
||||
},
|
||||
};
|
||||
export const DATA_REVALIDATE = 60;
|
||||
|
||||
export default function PageOverview() {
|
||||
const [explorerData, setExplorerData] = useState<ExplorerData | null>(null);
|
||||
const [noiseLineGraphData, setNoiseLineGraphData] = useState<{
|
||||
color: string;
|
||||
label: string;
|
||||
data: IExplorerLineChartData[];
|
||||
}>();
|
||||
const [stakeLineGraphData, setStakeLineGraphData] = useState<{
|
||||
color: string;
|
||||
label: string;
|
||||
data: IExplorerLineChartData[];
|
||||
}>();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const data = await getCacheExplorerData();
|
||||
setExplorerData(data);
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
|
||||
// CURRENT EPOCH
|
||||
const currentEpochStart =
|
||||
explorerData?.currentEpochData.current_epoch_start || "";
|
||||
|
||||
const progressBar = {
|
||||
title: "Current NGM epoch",
|
||||
start: currentEpochStart || "",
|
||||
showEpoch: true,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
// NOISE
|
||||
|
||||
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 parseFloat(change.toFixed(2));
|
||||
};
|
||||
|
||||
const percentage = calculatePercentageChange(noiseLast24H, noisePrevious24H);
|
||||
|
||||
const getPacketsData = () => {
|
||||
const data: Array<IExplorerLineChartData> = [];
|
||||
explorerData?.packetsAndStakingData.map((item: any) => {
|
||||
data.push({
|
||||
date_utc: item.date_utc,
|
||||
numericData: item.total_packets_sent + item.total_packets_received,
|
||||
});
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const noiseLineGraphData = {
|
||||
color: "#8482FD",
|
||||
label: "Total packets sent and received",
|
||||
data: getPacketsData(),
|
||||
};
|
||||
setNoiseLineGraphData(noiseLineGraphData);
|
||||
}, [explorerData]);
|
||||
|
||||
const noiseCard = {
|
||||
overTitle: "Noise generated last 24h",
|
||||
title: formatBigNum(noiseLast24H) || "",
|
||||
upDownLine: {
|
||||
percentage: Math.abs(percentage) || 0,
|
||||
numberWentUp: percentage > 0,
|
||||
},
|
||||
graph: noiseLineGraphData,
|
||||
};
|
||||
|
||||
// STAKE
|
||||
|
||||
const currentStake =
|
||||
Number(explorerData?.currentEpochRewardsData.interval.staking_supply) /
|
||||
1000000 || 0;
|
||||
|
||||
const getStakeData = () => {
|
||||
const data: Array<IExplorerLineChartData> = [];
|
||||
explorerData?.packetsAndStakingData.map((item: any) => {
|
||||
data.push({
|
||||
date_utc: item.date_utc,
|
||||
numericData: item.total_stake / 1000000,
|
||||
});
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
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 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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {
|
||||
summaryOverview,
|
||||
@@ -29,9 +291,14 @@ const PageOverview = () => {
|
||||
block,
|
||||
countryData,
|
||||
serviceProviders,
|
||||
} = useMainContext()
|
||||
} = useMainContext();
|
||||
return (
|
||||
<Box component="main" sx={{ flexGrow: 1 }}>
|
||||
<TwoSidedSwitch
|
||||
leftLabel="Account"
|
||||
rightLabel="Mixnode"
|
||||
onSwitch={() => {}}
|
||||
/>
|
||||
<Grid>
|
||||
<Grid item paddingBottom={3}>
|
||||
<Title text="Overview" />
|
||||
@@ -40,19 +307,34 @@ const PageOverview = () => {
|
||||
<Grid container spacing={3}>
|
||||
{summaryOverview && (
|
||||
<>
|
||||
<Grid item xs={12}>
|
||||
<AccountStatsCard {...accountStatsCard} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<ExplorerCard {...explorerCard} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<ExplorerCard progressBar={progressBar} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<ExplorerCard {...noiseCard} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<ExplorerCard {...stakeCard} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() => router.push('/network-components/mixnodes')}
|
||||
onClick={() => router.push("/network-components/mixnodes")}
|
||||
title="Mixnodes"
|
||||
icon={<MixnodesSVG />}
|
||||
count={summaryOverview.data?.mixnodes.count || ''}
|
||||
count={summaryOverview.data?.mixnodes.count || ""}
|
||||
errorMsg={summaryOverview?.error}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() =>
|
||||
router.push('/network-components/mixnodes?status=active')
|
||||
router.push("/network-components/mixnodes?status=active")
|
||||
}
|
||||
title="Active nodes"
|
||||
icon={<Icons.Mixnodes.Status.Active />}
|
||||
@@ -66,7 +348,7 @@ const PageOverview = () => {
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() =>
|
||||
router.push('/network-components/mixnodes?status=standby')
|
||||
router.push("/network-components/mixnodes?status=standby")
|
||||
}
|
||||
title="Standby nodes"
|
||||
color={
|
||||
@@ -82,9 +364,9 @@ const PageOverview = () => {
|
||||
{gateways && (
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() => router.push('/network-components/gateways')}
|
||||
onClick={() => router.push("/network-components/gateways")}
|
||||
title="Gateways"
|
||||
count={gateways?.data?.length || ''}
|
||||
count={gateways?.data?.length || ""}
|
||||
errorMsg={gateways?.error}
|
||||
icon={<GatewaysSVG />}
|
||||
/>
|
||||
@@ -94,7 +376,7 @@ const PageOverview = () => {
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() =>
|
||||
router.push('/network-components/service-providers')
|
||||
router.push("/network-components/service-providers")
|
||||
}
|
||||
title="Service providers"
|
||||
icon={<PeopleAlt />}
|
||||
@@ -108,7 +390,7 @@ const PageOverview = () => {
|
||||
<StatsCard
|
||||
onClick={() => window.open(`${BIG_DIPPER}/validators`)}
|
||||
title="Validators"
|
||||
count={validators?.data?.count || ''}
|
||||
count={validators?.data?.count || ""}
|
||||
errorMsg={validators?.error}
|
||||
icon={<ValidatorsSVG />}
|
||||
/>
|
||||
@@ -150,7 +432,5 @@ const PageOverview = () => {
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default PageOverview
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
declare module "react-world-flags";
|
||||
@@ -0,0 +1,46 @@
|
||||
version: "2"
|
||||
|
||||
services:
|
||||
remark:
|
||||
# remove the next line in case you want to use this Docker Compose file separately
|
||||
# as otherwise it would complain for absence of Dockerfile
|
||||
build: .
|
||||
image: umputun/remark42:latest
|
||||
container_name: "explorer_remark42"
|
||||
hostname: "remark42"
|
||||
restart: always
|
||||
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "5"
|
||||
|
||||
# uncomment to expose directly (no proxy)
|
||||
ports:
|
||||
- "8081:8080"
|
||||
- "443:8443"
|
||||
|
||||
environment:
|
||||
# - REMARK_URL=http://localhost:8081
|
||||
- REMARK_URL=https://remark.blockfend.com
|
||||
- SECRET=secret-key
|
||||
- AUTH_ANON=true
|
||||
# - SITE=remark42
|
||||
- SITE=nym-explorer-test
|
||||
|
||||
# - DEBUG=true
|
||||
# - AUTH_GOOGLE_CID
|
||||
# - AUTH_GOOGLE_CSEC
|
||||
# - AUTH_GITHUB_CID
|
||||
# - AUTH_GITHUB_CSEC
|
||||
# - AUTH_FACEBOOK_CID
|
||||
# - AUTH_FACEBOOK_CSEC
|
||||
# - AUTH_DISQUS_CID
|
||||
# - AUTH_DISQUS_CSEC
|
||||
# Enable it only for the initial comment import or for manual backups.
|
||||
# Do not leave the server running with the ADMIN_PASSWD set if you don't have an intention
|
||||
# to keep creating backups manually!
|
||||
- ADMIN_PASSWD=password
|
||||
volumes:
|
||||
- ./var:/srv/var
|
||||
@@ -9,15 +9,18 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nymproject/react": "^1.0.0",
|
||||
"@mui/x-data-grid": "7.1.1",
|
||||
"@mui/x-date-pickers": "7.1.1",
|
||||
"@nivo/line": "^0.88.0",
|
||||
"@nymproject/nym-validator-client": "0.18.0",
|
||||
"@nymproject/react": "^1.0.0",
|
||||
"material-react-table": "^2.12.1",
|
||||
"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"
|
||||
"react-world-flags": "^1.6.0",
|
||||
"qrcode.react": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -28,7 +28,8 @@
|
||||
"../assets/*"
|
||||
]
|
||||
},
|
||||
"moduleResolution": "node"
|
||||
"moduleResolution": "node",
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
|
||||
|
After Width: | Height: | Size: 976 B |