WIP
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
import {
|
||||
CIRCULATING_NYM_SUPPLY,
|
||||
CURRENT_EPOCH,
|
||||
CURRENT_EPOCH_REWARDS,
|
||||
HARBOURMASTER_API_MIXNODES_STATS,
|
||||
HARBOURMASTER_API_SUMMARY,
|
||||
} from "./urls";
|
||||
|
||||
type Denom = "unym" | "nym";
|
||||
|
||||
export interface IPacketsAndStakingData {
|
||||
date_utc: string;
|
||||
total_packets_received: number;
|
||||
total_packets_sent: number;
|
||||
total_packets_dropped: number;
|
||||
total_stake: number;
|
||||
}
|
||||
|
||||
export interface ExplorerData {
|
||||
circulatingNymSupplyData: {
|
||||
circulating_supply: { denom: Denom; amount: string };
|
||||
mixmining_reserve: { denom: Denom; amount: string };
|
||||
total_supply: { denom: Denom; amount: string };
|
||||
vesting_tokens: { denom: Denom; amount: string };
|
||||
};
|
||||
nymNodesData: {
|
||||
gateways: {
|
||||
bonded: { count: number; last_updated_utc: string };
|
||||
blacklisted: { count: number; last_updated_utc: string };
|
||||
historical: { count: number; last_updated_utc: string };
|
||||
explorer: { count: number; last_updated_utc: string };
|
||||
};
|
||||
mixnodes: {
|
||||
bonded: {
|
||||
count: number;
|
||||
active: number;
|
||||
inactive: number;
|
||||
reserve: number;
|
||||
last_updated_utc: string;
|
||||
};
|
||||
blacklisted: {
|
||||
count: number;
|
||||
last_updated_utc: string;
|
||||
};
|
||||
historical: { count: number; last_updated_utc: string };
|
||||
};
|
||||
};
|
||||
packetsAndStakingData: IPacketsAndStakingData[];
|
||||
currentEpochData: {
|
||||
id: number;
|
||||
current_epoch_id: number;
|
||||
current_epoch_start: string;
|
||||
epoch_length: { secs: number; nanos: number };
|
||||
epochs_in_interval: number;
|
||||
total_elapsed_epochs: number;
|
||||
};
|
||||
currentEpochRewardsData: {
|
||||
interval: {
|
||||
reward_pool: string;
|
||||
staking_supply: string;
|
||||
staking_supply_scale_factor: string;
|
||||
epoch_reward_budget: string;
|
||||
stake_saturation_point: string;
|
||||
active_set_work_factor: string;
|
||||
interval_pool_emission: string;
|
||||
sybil_resistance: string;
|
||||
};
|
||||
rewarded_set: {
|
||||
entry_gateways: number;
|
||||
exit_gateways: number;
|
||||
mixnodes: number;
|
||||
standby: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ExplorerCache {
|
||||
data?: ExplorerData;
|
||||
lastUpdated?: Date;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// Extend the global object with our custom property
|
||||
let explorerCache: ExplorerCache | undefined;
|
||||
}
|
||||
|
||||
const CACHE_TIME_SECONDS = 60 * 5; // 5 minutes
|
||||
|
||||
const getExplorerData = async () => {
|
||||
// FETCH NYMNODES
|
||||
const fetchNymNodes = await fetch(HARBOURMASTER_API_SUMMARY, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
// FETCH CURRENT EPOCH
|
||||
const fetchCurrentEpoch = await fetch(CURRENT_EPOCH, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
// FETCH CURRENT EPOCH REWARDS
|
||||
const fetchCurrentEpochRewards = await fetch(CURRENT_EPOCH_REWARDS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
// FETCH CIRCULATING NYM SUPPLY
|
||||
const fetchCirculatingNymSupply = await fetch(CIRCULATING_NYM_SUPPLY, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
// FETCH PACKETS AND STAKING
|
||||
const fetchPacketsAndStaking = await fetch(HARBOURMASTER_API_MIXNODES_STATS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
// refresh event list cache at given interval
|
||||
next: { revalidate: Number(process.env.NEXT_PUBLIC_REVALIDATE_CACHE) },
|
||||
});
|
||||
|
||||
const [
|
||||
circulatingNymSupplyRes,
|
||||
nymNodesRes,
|
||||
packetsAndStakingRes,
|
||||
currentEpochRes,
|
||||
currentEpochRewardsRes,
|
||||
] = await Promise.all([
|
||||
fetchCirculatingNymSupply,
|
||||
fetchNymNodes,
|
||||
fetchPacketsAndStaking,
|
||||
fetchCurrentEpoch,
|
||||
fetchCurrentEpochRewards,
|
||||
]);
|
||||
|
||||
const [
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
currentEpochData,
|
||||
currentEpochRewardsData,
|
||||
] = await Promise.all([
|
||||
circulatingNymSupplyRes.json(),
|
||||
nymNodesRes.json(),
|
||||
packetsAndStakingRes.json(),
|
||||
currentEpochRes.json(),
|
||||
currentEpochRewardsRes.json(),
|
||||
]);
|
||||
|
||||
return [
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
currentEpochData,
|
||||
currentEpochRewardsData,
|
||||
];
|
||||
};
|
||||
|
||||
export async function ensureCacheExists() {
|
||||
// makes sure the cache exists in global memory
|
||||
let doUpdate = false;
|
||||
const now = new Date();
|
||||
if (!global.explorerCache) {
|
||||
global.explorerCache = {};
|
||||
doUpdate = true;
|
||||
}
|
||||
if (
|
||||
global.explorerCache.lastUpdated &&
|
||||
now.getDate() - global.explorerCache.lastUpdated.getDate() >
|
||||
CACHE_TIME_SECONDS
|
||||
) {
|
||||
doUpdate = true;
|
||||
}
|
||||
|
||||
// if the cache has expired or never existed, get it from API's
|
||||
if (doUpdate) {
|
||||
const [
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
currentEpochData,
|
||||
currentEpochRewardsData,
|
||||
] = await getExplorerData();
|
||||
|
||||
packetsAndStakingData.pop();
|
||||
|
||||
global.explorerCache.data = {
|
||||
circulatingNymSupplyData,
|
||||
nymNodesData,
|
||||
packetsAndStakingData,
|
||||
currentEpochData,
|
||||
currentEpochRewardsData,
|
||||
};
|
||||
global.explorerCache.lastUpdated = now;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCacheExplorerData() {
|
||||
await ensureCacheExists();
|
||||
|
||||
if (!global.explorerCache?.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log("global.explorerCache.data :>> ", global.explorerCache.data);
|
||||
return global.explorerCache.data || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a custom API route that returns metadata from Strapi about images: height, width, strapi download url.
|
||||
*
|
||||
* The response from Strapi is cached in memory for CACHE_TIME_SECONDS.
|
||||
*/
|
||||
// export default async function handler(
|
||||
// req: NextApiRequest,
|
||||
// res: NextApiResponse
|
||||
// ) {
|
||||
// // return cached data
|
||||
// const data = await getCacheExplorerData();
|
||||
// if (data) {
|
||||
// res.status(200).json(data);
|
||||
// res.end();
|
||||
// }
|
||||
|
||||
// // catch-all
|
||||
// res.status(404).end();
|
||||
// }
|
||||
@@ -0,0 +1,18 @@
|
||||
export const HARBOURMASTER_API_SUMMARY =
|
||||
"https://harbourmaster.nymtech.net/v2/summary";
|
||||
export const EXPLORER_API = "https://explorer.nymtech.net/api/v1/countries";
|
||||
export const VALIDATOR_API_SUPPLY =
|
||||
"https://validator.nymtech.net/api/v1/circulating-supply";
|
||||
export const COSMOS_API =
|
||||
"https://api.nymtech.net/cosmos/bank/v1beta1/balances/n1a53udazy8ayufvy0s434pfwjcedzqv34yg485t";
|
||||
export const VALIDATOR_API_EPOCH =
|
||||
"https://validator.nymtech.net/api/v1/epoch/reward_params";
|
||||
export const HARBOURMASTER_API_MIXNODES_STATS =
|
||||
"https://harbourmaster.nymtech.net/v2/mixnodes/stats";
|
||||
export const HARBOURMASTER_API_BASE = "https://harbourmaster.nymtech.net";
|
||||
export const CURRENT_EPOCH =
|
||||
" https://validator.nymtech.net/api/v1/epoch/current";
|
||||
export const CURRENT_EPOCH_REWARDS =
|
||||
"https://validator.nymtech.net/api/v1/epoch/reward_params";
|
||||
export const CIRCULATING_NYM_SUPPLY =
|
||||
"https://validator.nymtech.net/api/v1/circulating-supply";
|
||||
@@ -0,0 +1 @@
|
||||
export const TABLET_WIDTH = "(min-width:700px)";
|
||||
@@ -5,10 +5,35 @@ import ProgressBar from "@/components/RatingMeter/RatingMeter";
|
||||
import StarRarating from "@/components/StarRating/StarRating";
|
||||
import CopyFile from "@/components/icons/CopyFile";
|
||||
import Gateway from "@/components/icons/Gateway";
|
||||
("use client");
|
||||
import TwoSidedSwitch from "@/components/TwoSidedButtonSwitch";
|
||||
import {
|
||||
AccountStatsCard,
|
||||
type IAccountStatsCardProps,
|
||||
} from "@/components/cards/AccountStatsCard";
|
||||
import { CurrentEpochCard } from "@/components/landingPageComponents/CurrentEpochCard";
|
||||
import { NetworkStakeCard } from "@/components/landingPageComponents/NetworkStakeCard";
|
||||
import { NoiseCard } from "@/components/landingPageComponents/NoiseCard";
|
||||
import { RewardsCard } from "@/components/landingPageComponents/RewardsCard";
|
||||
import { TokenomicsCard } from "@/components/landingPageComponents/TokenomicsCard";
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import { Container, Grid2, IconButton, Stack, Typography } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { type ContentCardProps, MonoCard } from "../components/cards/MonoCard";
|
||||
import { type ExplorerData, getCacheExplorerData } from "./api";
|
||||
|
||||
export default function Home() {
|
||||
const [explorerData, setExplorerData] = useState<ExplorerData>();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const data = await getCacheExplorerData();
|
||||
setExplorerData(data);
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<main>
|
||||
@@ -42,6 +67,46 @@ export default function Home() {
|
||||
value={<ProgressBar value={50} color="secondary" />}
|
||||
/>
|
||||
</ExplorerCard>
|
||||
<Typography variant="h1" textTransform={"uppercase"} mb={5}>
|
||||
Mixnet in your hands
|
||||
</Typography>
|
||||
<Grid container rowSpacing={3} columnSpacing={2} mb={2}>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
{explorerData && <NoiseCard explorerData={explorerData} />}
|
||||
</Grid>
|
||||
<Grid container rowSpacing={3} size={{ xs: 12, md: 3 }}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<RewardsCard />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
{explorerData && (
|
||||
<CurrentEpochCard explorerData={explorerData} />
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
{explorerData && (
|
||||
<NetworkStakeCard explorerData={explorerData} />
|
||||
)}
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<TokenomicsCard />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container gap={2} alignItems={"flex-start"}>
|
||||
<Grid size={{ xs: 12, md: 5 }}>
|
||||
<MonoCard {...explorerCard} />
|
||||
</Grid>
|
||||
<Grid container size={{ xs: 6 }}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TwoSidedSwitch
|
||||
leftLabel="Account"
|
||||
rightLabel="Mixnode"
|
||||
// onSwitch={() => console.log("object :>> ")}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid2 container spacing={4}>
|
||||
<Grid2 size={6}>
|
||||
<ExplorerHeroCard
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export const formatBigNum = (num: number) => {
|
||||
if (typeof num === "number") {
|
||||
if (num >= 1000000000) {
|
||||
return `${(num / 1000000000).toFixed(1).replace(/\.0$/, "")}B`;
|
||||
}
|
||||
if (num >= 1000000) {
|
||||
return `${(num / 1000000).toFixed(1).replace(/\.0$/, "")}M`;
|
||||
}
|
||||
if (num >= 1000) {
|
||||
return `${(num / 1000).toFixed(1).replace(/\.0$/, "")}K`;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,308 @@
|
||||
"use client";
|
||||
|
||||
import { TABLET_WIDTH } from "@/app/constants";
|
||||
import CircleIcon from "@mui/icons-material/Circle";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import { Card, CardContent } from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import * as React from "react";
|
||||
import { MultiSegmentProgressBar } from "../progressBars/MultiSegmentProgressBar";
|
||||
import { StaticProgressBar } from "../progressBars/StaticProgressBar";
|
||||
|
||||
export interface IAccontStatsRowProps {
|
||||
type: string;
|
||||
allocation: number;
|
||||
amount: number;
|
||||
value: number;
|
||||
history?: { type: string; amount: number }[];
|
||||
isLastRow?: boolean;
|
||||
progressBarColor?: string;
|
||||
}
|
||||
|
||||
const progressBarColours = [
|
||||
"#BEF885",
|
||||
"#7FB0FF",
|
||||
"#00D17D",
|
||||
"#004650",
|
||||
"#FEECB3",
|
||||
];
|
||||
|
||||
const Row = (props: IAccontStatsRowProps) => {
|
||||
const tablet = useMediaQuery(TABLET_WIDTH);
|
||||
|
||||
const {
|
||||
type,
|
||||
allocation,
|
||||
amount,
|
||||
value,
|
||||
history,
|
||||
isLastRow,
|
||||
progressBarColor,
|
||||
} = props;
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* Main Row */}
|
||||
|
||||
{tablet ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "25%",
|
||||
}}
|
||||
>
|
||||
<Typography>{type}</Typography>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "25%",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography>{allocation}%</Typography>
|
||||
<StaticProgressBar
|
||||
value={allocation}
|
||||
color={progressBarColor || "green"}
|
||||
/>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "20%",
|
||||
}}
|
||||
>
|
||||
{amount} NYM
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "20%",
|
||||
}}
|
||||
>
|
||||
$ {value}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "10%",
|
||||
}}
|
||||
>
|
||||
{history && (
|
||||
<IconButton
|
||||
aria-label="expand row"
|
||||
size="small"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
// MOBILE VIEW
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "45%",
|
||||
}}
|
||||
>
|
||||
<Box display={"flex"} gap={1} alignItems={"center"}>
|
||||
<CircleIcon sx={{ color: progressBarColor }} fontSize="small" />
|
||||
{type}
|
||||
</Box>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "45%",
|
||||
}}
|
||||
>
|
||||
<Typography>{amount} NYM</Typography>
|
||||
<Typography>$ {value}</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "10%",
|
||||
}}
|
||||
>
|
||||
{history && (
|
||||
<IconButton
|
||||
aria-label="expand row"
|
||||
size="small"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
{/* History Rows */}
|
||||
{history &&
|
||||
open &&
|
||||
history.map((historyRow) => (
|
||||
<TableRow key={historyRow.type}>
|
||||
<TableCell
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
pl: 5,
|
||||
borderBottom: "none", // Explicitly remove border
|
||||
}}
|
||||
>
|
||||
<span style={{ marginRight: 8 }}>•</span>
|
||||
{historyRow.type}
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: "none", // Explicitly remove border
|
||||
}}
|
||||
>
|
||||
{historyRow.amount}
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: "none", // Explicitly remove border
|
||||
}}
|
||||
>
|
||||
{/* Any additional content */}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export interface IAccountStatsCardProps {
|
||||
rows: Array<IAccontStatsRowProps>;
|
||||
overTitle?: string;
|
||||
priceTitle?: number;
|
||||
}
|
||||
|
||||
export const AccountStatsCard = (props: IAccountStatsCardProps) => {
|
||||
const { rows, overTitle, priceTitle } = props;
|
||||
const tablet = useMediaQuery(TABLET_WIDTH);
|
||||
const progressBarPercentages = () => {
|
||||
return rows.map((row) => row.allocation);
|
||||
};
|
||||
const getProgressValues = () => {
|
||||
const percentages = progressBarPercentages();
|
||||
const result: Array<{ percentage: number; color: string }> = [];
|
||||
percentages.map((value, i) => {
|
||||
result.push({
|
||||
percentage: value,
|
||||
color: progressBarColours[i],
|
||||
});
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const progressValues = getProgressValues();
|
||||
|
||||
return (
|
||||
<Card sx={{ height: "100%", borderRadius: "unset" }}>
|
||||
<CardContent>
|
||||
{overTitle && (
|
||||
<Typography fontSize={14} mb={3} textTransform={"uppercase"}>
|
||||
{overTitle}
|
||||
</Typography>
|
||||
)}
|
||||
{priceTitle && (
|
||||
<Typography fontSize={24} mb={3}>
|
||||
${priceTitle}
|
||||
</Typography>
|
||||
)}
|
||||
{!tablet && <MultiSegmentProgressBar values={progressValues} />}
|
||||
<TableContainer>
|
||||
<Table aria-label="collapsible table" sx={{ marginBottom: 3 }}>
|
||||
<TableHead>
|
||||
{tablet ? (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography textTransform={"uppercase"}>Type</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography textTransform={"uppercase"}>
|
||||
Allocation
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography textTransform={"uppercase"}>Amount</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography textTransform={"uppercase"}>Value</Typography>
|
||||
</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography textTransform={"uppercase"}>Type</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography textTransform={"uppercase"}>
|
||||
Amount / Value
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
)}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, i) => (
|
||||
<Row
|
||||
key={row.type}
|
||||
{...row}
|
||||
isLastRow={i === rows.length - 1}
|
||||
progressBarColor={progressBarColours[i]}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,374 @@
|
||||
"use client";
|
||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
||||
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
|
||||
import StarIcon from "@mui/icons-material/Star";
|
||||
import { Box, Button, Card, CardContent, Typography } from "@mui/material";
|
||||
import { CopyToClipboard } from "@nymproject/react/clipboard/CopyToClipboard";
|
||||
import Image from "next/image";
|
||||
import { QRCodeCanvas } from "qrcode.react";
|
||||
import type React from "react";
|
||||
import type { FC, ReactElement } from "react";
|
||||
import Flag from "react-world-flags";
|
||||
import profileImagePlaceholder from "../../../public/profileImagePlaceholder.png";
|
||||
// import { Remark42Comments } from "../comments";
|
||||
import { NymTokenSVG } from "../icons/NymTokenSVG";
|
||||
import { type ILineChartData, LineChart } from "../lineChart";
|
||||
import {
|
||||
DynamicProgressBar,
|
||||
type IDynamicProgressBarProps,
|
||||
} from "../progressBars/DynamicProgressBar";
|
||||
|
||||
interface ICardUpDownPriceLineProps {
|
||||
percentage: number;
|
||||
numberWentUp: boolean;
|
||||
}
|
||||
const CardUpDownPriceLine = (
|
||||
props: ICardUpDownPriceLineProps,
|
||||
): ReactElement => {
|
||||
const { percentage, numberWentUp } = props;
|
||||
return (
|
||||
<Box display={"flex"} alignItems={"center"}>
|
||||
{numberWentUp ? (
|
||||
<ArrowUpwardIcon sx={{ color: "#00CA33", fontSize: 13 }} />
|
||||
) : (
|
||||
<ArrowDownwardIcon sx={{ color: "#DF1400", fontSize: 13 }} />
|
||||
)}
|
||||
<Typography
|
||||
fontSize={13}
|
||||
sx={{ color: numberWentUp ? "#00CA33" : "#DF1400" }}
|
||||
>
|
||||
{percentage}% (24H)
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardTitlePriceProps {
|
||||
price: number;
|
||||
upDownLine: ICardUpDownPriceLineProps;
|
||||
}
|
||||
const CardTitlePrice = (props: ICardTitlePriceProps): React.ReactNode => {
|
||||
const { price, upDownLine } = props;
|
||||
return (
|
||||
<Box display={"flex"} flexDirection={"column"} alignItems={"flex-end"}>
|
||||
<Box display={"flex"} justifyContent={"space-between"} width={"100%"}>
|
||||
<Box display={"flex"} gap={1} alignItems={"center"}>
|
||||
<NymTokenSVG />
|
||||
<Typography>NYM</Typography>
|
||||
</Box>
|
||||
<Typography>${price}</Typography>
|
||||
</Box>
|
||||
<CardUpDownPriceLine {...upDownLine} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export interface ICardDataRowsProps {
|
||||
rows: Array<{ key: string; value: string }>;
|
||||
}
|
||||
export const CardDataRows = (props: ICardDataRowsProps): React.ReactNode => {
|
||||
const { rows } = props;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{rows.map((row, i) => {
|
||||
return (
|
||||
<Box
|
||||
key={row.key}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
display={"flex"}
|
||||
justifyContent={"space-between"}
|
||||
borderBottom={i === 0 ? "1px solid #C3D7D7" : "none"}
|
||||
>
|
||||
<Typography fontSize={14} textTransform={"uppercase"}>
|
||||
{row.key}
|
||||
</Typography>
|
||||
<Typography fontSize={14} textTransform={"uppercase"}>
|
||||
{row.value}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
interface ICardProileImage {
|
||||
url?: string;
|
||||
}
|
||||
const CardProfileImage = (props: ICardProileImage) => {
|
||||
const { url } = props;
|
||||
return (
|
||||
<Box display={"flex"} justifyContent={"flex-start"} mb={3}>
|
||||
{url ? (
|
||||
<Image src={url} alt="linkedIn" width={80} height={80} />
|
||||
) : (
|
||||
<Image
|
||||
src={profileImagePlaceholder}
|
||||
alt="linkedIn"
|
||||
width={80}
|
||||
height={80}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardProfileCountry {
|
||||
countryCode: string;
|
||||
countryName: string;
|
||||
}
|
||||
|
||||
const CardProfileCountry = (props: ICardProfileCountry) => {
|
||||
const { countryCode, countryName } = props;
|
||||
return (
|
||||
<Box display={"flex"} justifyContent={"flex-start"} gap={2}>
|
||||
<Flag code={countryCode} width="20" />
|
||||
<Typography textTransform={"uppercase"}>{countryName}</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardCopyAddressProps {
|
||||
title: string;
|
||||
address: string;
|
||||
}
|
||||
|
||||
const CardCopyAddress = (props: ICardCopyAddressProps) => {
|
||||
const { title, address } = props;
|
||||
return (
|
||||
<Box
|
||||
paddingTop={2}
|
||||
paddingBottom={2}
|
||||
display={"flex"}
|
||||
flexDirection={"column"}
|
||||
gap={2}
|
||||
borderBottom={"1px solid #C3D7D7"}
|
||||
>
|
||||
<Typography textTransform={"uppercase"}>{title}</Typography>
|
||||
<Box display={"flex"} justifyContent={"space-between"}>
|
||||
<Typography maxWidth={"90%"} sx={{ wordWrap: "break-word" }}>
|
||||
{address}
|
||||
</Typography>
|
||||
|
||||
<CopyToClipboard
|
||||
sx={{ mr: 0.5, color: "grey.400" }}
|
||||
smallIcons
|
||||
value={address}
|
||||
tooltip={`Copy identity key ${address} to clipboard`}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardQRCodeProps {
|
||||
url: string;
|
||||
}
|
||||
|
||||
const CardQRCode = (props: ICardQRCodeProps) => {
|
||||
const { url } = props;
|
||||
return (
|
||||
<Box display={"flex"} justifyContent={"flex-start"}>
|
||||
<Box
|
||||
padding={2}
|
||||
border={"1px solid #C3D7D7"}
|
||||
display={"block"}
|
||||
width={"unset"}
|
||||
>
|
||||
<QRCodeCanvas value={url} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardRatingsProps {
|
||||
ratings: Array<{ title: string; numberOfStars: number }>;
|
||||
}
|
||||
|
||||
const CardRatings = (props: ICardRatingsProps) => {
|
||||
const { ratings } = props;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{ratings.map((rating, i) => {
|
||||
const Stars = () => {
|
||||
const stars = [];
|
||||
for (let i = 0; i < rating.numberOfStars; i++) {
|
||||
stars.push(
|
||||
<StarIcon sx={{ color: "#14E76F" }} fontSize="small" key={i} />,
|
||||
);
|
||||
}
|
||||
return stars;
|
||||
};
|
||||
const RatingTitle = () => {
|
||||
switch (rating.numberOfStars) {
|
||||
case 1:
|
||||
case 2:
|
||||
return <Typography>Bad</Typography>;
|
||||
case 3:
|
||||
return <Typography>ok</Typography>;
|
||||
case 4:
|
||||
return <Typography>Good</Typography>;
|
||||
default:
|
||||
return <Typography>Excellent</Typography>;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
key={rating.title}
|
||||
paddingTop={2}
|
||||
paddingBottom={2}
|
||||
display={"flex"}
|
||||
justifyContent={"space-between"}
|
||||
borderBottom={i < ratings.length - 1 ? "1px solid #C3D7D7" : "none"}
|
||||
>
|
||||
<Typography>{rating.title}</Typography>
|
||||
<Box display={"flex"} gap={1} alignItems={"center"}>
|
||||
<Stars />
|
||||
<RatingTitle />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export type ContentCardProps = {
|
||||
overTitle?: string;
|
||||
profileImage?: ICardProileImage;
|
||||
title?: string | number;
|
||||
profileCountry?: ICardProfileCountry;
|
||||
upDownLine?: ICardUpDownPriceLineProps;
|
||||
titlePrice?: ICardTitlePriceProps;
|
||||
dataRows?: ICardDataRowsProps;
|
||||
graph?: { data: Array<ILineChartData>; color: string; label: string };
|
||||
progressBar?: IDynamicProgressBarProps;
|
||||
paragraph?: string;
|
||||
nymAddress?: ICardCopyAddressProps;
|
||||
identityKey?: ICardCopyAddressProps;
|
||||
qrCode?: ICardQRCodeProps;
|
||||
ratings?: ICardRatingsProps;
|
||||
comments?: boolean;
|
||||
stakeButton?: {
|
||||
label: string;
|
||||
identityKey: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const MonoCard: FC<ContentCardProps> = ({
|
||||
title,
|
||||
titlePrice,
|
||||
overTitle,
|
||||
upDownLine,
|
||||
dataRows,
|
||||
graph,
|
||||
progressBar,
|
||||
paragraph,
|
||||
profileImage,
|
||||
profileCountry,
|
||||
nymAddress,
|
||||
identityKey,
|
||||
qrCode,
|
||||
ratings,
|
||||
// comments,
|
||||
stakeButton,
|
||||
}) => (
|
||||
<Card sx={{ height: "100%", borderRadius: "unset", padding: 1 }}>
|
||||
<CardContent
|
||||
sx={{
|
||||
paddingBottom: "0px !important",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
{overTitle && (
|
||||
<Typography fontSize={14} mb={3} textTransform={"uppercase"}>
|
||||
{overTitle}
|
||||
</Typography>
|
||||
)}
|
||||
{profileImage && <CardProfileImage {...profileImage} />}
|
||||
{title && (
|
||||
<Typography fontSize={24} mb={upDownLine ? 0 : 3}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
{profileCountry && (
|
||||
<Box mb={3}>
|
||||
<CardProfileCountry {...profileCountry} />
|
||||
</Box>
|
||||
)}
|
||||
{upDownLine && (
|
||||
<Box mb={3}>
|
||||
<CardUpDownPriceLine {...upDownLine} />
|
||||
</Box>
|
||||
)}
|
||||
{titlePrice && <CardTitlePrice {...titlePrice} />}
|
||||
</Box>
|
||||
{qrCode && (
|
||||
<Box mb={3}>
|
||||
<CardQRCode {...qrCode} />
|
||||
</Box>
|
||||
)}
|
||||
{nymAddress && (
|
||||
<Box mb={3}>
|
||||
<CardCopyAddress {...nymAddress} />
|
||||
</Box>
|
||||
)}
|
||||
{identityKey && (
|
||||
<Box mb={3}>
|
||||
<CardCopyAddress {...identityKey} />
|
||||
</Box>
|
||||
)}
|
||||
{dataRows && (
|
||||
<Box mb={3}>
|
||||
<CardDataRows {...dataRows} />
|
||||
</Box>
|
||||
)}
|
||||
{ratings && (
|
||||
<Box mb={3}>
|
||||
<CardRatings {...ratings} />
|
||||
</Box>
|
||||
)}
|
||||
{graph && (
|
||||
<Box mb={3}>
|
||||
<LineChart
|
||||
data={graph.data}
|
||||
color={graph.color}
|
||||
label={graph.label}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{progressBar && (
|
||||
<Box mb={3}>
|
||||
<DynamicProgressBar {...progressBar} />
|
||||
</Box>
|
||||
)}
|
||||
{paragraph && <Typography mb={3}>{paragraph}</Typography>}
|
||||
{/* {comments && (
|
||||
<Box mb={3}>
|
||||
<Remark42Comments />
|
||||
</Box>
|
||||
)} */}
|
||||
{stakeButton && (
|
||||
<Box mb={3}>
|
||||
<Button
|
||||
onClick={() =>
|
||||
console.log(
|
||||
"stakeButton.identityKey :>> ",
|
||||
stakeButton.identityKey,
|
||||
)
|
||||
}
|
||||
variant="contained"
|
||||
>
|
||||
{stakeButton.label}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Box, Button } from "@mui/material";
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface TwoSidedSwitchProps {
|
||||
leftLabel: string; // Label for the left side
|
||||
rightLabel: string; // Label for the right side
|
||||
onSwitch?: (side: "left" | "right") => void; // Callback when switched
|
||||
}
|
||||
|
||||
const TwoSidedSwitch: React.FC<TwoSidedSwitchProps> = ({
|
||||
leftLabel,
|
||||
rightLabel,
|
||||
onSwitch,
|
||||
}) => {
|
||||
const [selectedSide, setSelectedSide] = useState<"left" | "right">("left");
|
||||
|
||||
const handleSwitch = (side: "left" | "right") => {
|
||||
setSelectedSide(side);
|
||||
if (onSwitch) onSwitch(side);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
borderRadius: "20px",
|
||||
overflow: "hidden",
|
||||
width: "200px",
|
||||
height: "40px",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={() => handleSwitch("left")}
|
||||
sx={{
|
||||
flex: 1,
|
||||
backgroundColor: selectedSide === "left" ? "black" : "transparent",
|
||||
color: selectedSide === "left" ? "white" : "black",
|
||||
border: "1px dashed black",
|
||||
borderRight: "none",
|
||||
borderBottomRightRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
// "&:hover": {
|
||||
// backgroundColor: selectedSide === "left" ? "black" : "lightgray",
|
||||
// },
|
||||
}}
|
||||
>
|
||||
{leftLabel}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleSwitch("right")}
|
||||
sx={{
|
||||
flex: 1,
|
||||
backgroundColor: selectedSide === "right" ? "black" : "transparent",
|
||||
color: selectedSide === "right" ? "white" : "black",
|
||||
border: "1px dashed black",
|
||||
borderLeft: "none",
|
||||
borderBottomLeftRadius: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
// "&:hover": {
|
||||
// backgroundColor: selectedSide === "right" ? "black" : "lightgray",
|
||||
// },
|
||||
}}
|
||||
>
|
||||
{rightLabel}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TwoSidedSwitch;
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { ExplorerData } from "@/app/api";
|
||||
import { MonoCard } from "../cards/MonoCard";
|
||||
|
||||
interface ICurrentEpochCardProps {
|
||||
explorerData: ExplorerData | null;
|
||||
}
|
||||
|
||||
export const CurrentEpochCard = (props: ICurrentEpochCardProps) => {
|
||||
const { explorerData } = props;
|
||||
|
||||
const currentEpochStart =
|
||||
explorerData?.currentEpochData.current_epoch_start || "";
|
||||
|
||||
const progressBar = {
|
||||
start: currentEpochStart || "",
|
||||
showEpoch: true,
|
||||
};
|
||||
return <MonoCard progressBar={progressBar} overTitle="Current NGM epoch" />;
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
import type { ExplorerData, IPacketsAndStakingData } from "@/app/api";
|
||||
import { formatBigNum } from "@/app/utils/formatBigNumbers";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MonoCard } from "../cards/MonoCard";
|
||||
import type { ILineChartData } from "../lineChart";
|
||||
|
||||
interface INetworkStakeCardProps {
|
||||
explorerData: ExplorerData | null;
|
||||
}
|
||||
export const NetworkStakeCard = (props: INetworkStakeCardProps) => {
|
||||
const { explorerData } = props;
|
||||
const [stakeLineGraphData, setStakeLineGraphData] = useState<{
|
||||
color: string;
|
||||
label: string;
|
||||
data: ILineChartData[];
|
||||
}>();
|
||||
const currentStake =
|
||||
Number(explorerData?.currentEpochRewardsData.interval.staking_supply) /
|
||||
1000000 || 0;
|
||||
|
||||
useEffect(() => {
|
||||
const getStakeData = () => {
|
||||
const data: Array<ILineChartData> = [];
|
||||
explorerData?.packetsAndStakingData.map(
|
||||
(item: IPacketsAndStakingData) => {
|
||||
data.push({
|
||||
date_utc: item.date_utc,
|
||||
numericData: item.total_stake / 1000000,
|
||||
});
|
||||
},
|
||||
);
|
||||
return data;
|
||||
};
|
||||
const stakeLineGraphData = {
|
||||
color: "#00CA33",
|
||||
label: "Total stake delegated in NYM",
|
||||
data: getStakeData(),
|
||||
};
|
||||
setStakeLineGraphData(stakeLineGraphData);
|
||||
}, [explorerData]);
|
||||
|
||||
const stakeCard = {
|
||||
overTitle: "Current network stake",
|
||||
title: `${currentStake} NYM` || "",
|
||||
graph: stakeLineGraphData,
|
||||
};
|
||||
return <MonoCard {...stakeCard} />;
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
import type { ExplorerData, IPacketsAndStakingData } from "@/app/api";
|
||||
import { formatBigNum } from "@/app/utils/formatBigNumbers";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MonoCard } from "../cards/MonoCard";
|
||||
import type { ILineChartData } from "../lineChart";
|
||||
|
||||
interface INoiseCardProps {
|
||||
explorerData: ExplorerData;
|
||||
}
|
||||
export const NoiseCard = (props: INoiseCardProps) => {
|
||||
const { explorerData } = props;
|
||||
const [noiseLineGraphData, setNoiseLineGraphData] = useState<{
|
||||
color: string;
|
||||
label: string;
|
||||
data: ILineChartData[];
|
||||
}>();
|
||||
const noiseLast24H =
|
||||
explorerData.packetsAndStakingData[
|
||||
explorerData.packetsAndStakingData.length - 1
|
||||
].total_packets_sent +
|
||||
explorerData.packetsAndStakingData[
|
||||
explorerData.packetsAndStakingData.length - 1
|
||||
].total_packets_received;
|
||||
|
||||
const noisePrevious24H =
|
||||
explorerData.packetsAndStakingData[
|
||||
explorerData.packetsAndStakingData.length - 2
|
||||
].total_packets_sent +
|
||||
explorerData.packetsAndStakingData[
|
||||
explorerData.packetsAndStakingData.length - 2
|
||||
].total_packets_received;
|
||||
|
||||
const calculatePercentageChange = (last24H: number, previous24H: number) => {
|
||||
if (previous24H === 0) {
|
||||
throw new Error(
|
||||
"Cannot calculate percentage change when yesterday's value is zero.",
|
||||
);
|
||||
}
|
||||
|
||||
const change = ((last24H - previous24H) / previous24H) * 100;
|
||||
|
||||
return Number.parseFloat(change.toFixed(2));
|
||||
};
|
||||
|
||||
const percentage = calculatePercentageChange(noiseLast24H, noisePrevious24H);
|
||||
|
||||
useEffect(() => {
|
||||
const getPacketsData = () => {
|
||||
const data: Array<ILineChartData> = [];
|
||||
explorerData?.packetsAndStakingData.map(
|
||||
(item: IPacketsAndStakingData) => {
|
||||
data.push({
|
||||
date_utc: item.date_utc,
|
||||
numericData: item.total_packets_sent + item.total_packets_received,
|
||||
});
|
||||
},
|
||||
);
|
||||
return data;
|
||||
};
|
||||
const noiseLineGraphData = {
|
||||
color: "#8482FD",
|
||||
label: "Total packets sent and received",
|
||||
data: getPacketsData(),
|
||||
};
|
||||
setNoiseLineGraphData(noiseLineGraphData);
|
||||
}, [explorerData]);
|
||||
|
||||
const noiseCard = {
|
||||
overTitle: "Noise generated last 24h",
|
||||
title: formatBigNum(noiseLast24H) || "",
|
||||
upDownLine: {
|
||||
percentage: Math.abs(percentage) || 0,
|
||||
numberWentUp: percentage > 0,
|
||||
},
|
||||
graph: noiseLineGraphData,
|
||||
};
|
||||
return <MonoCard {...noiseCard} />;
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { MonoCard } from "../cards/MonoCard";
|
||||
|
||||
export const RewardsCard = () => {
|
||||
const rewardsCard = {
|
||||
overTitle: "Operator rewards this month",
|
||||
title: "198.841720 NYM",
|
||||
};
|
||||
return <MonoCard {...rewardsCard} />;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { MonoCard } from "../cards/MonoCard";
|
||||
|
||||
export const TokenomicsCard = () => {
|
||||
const tokenomicsCard = {
|
||||
overTitle: "Tokenomics overview",
|
||||
titlePrice: {
|
||||
price: 1.15,
|
||||
upDownLine: {
|
||||
percentage: 10,
|
||||
numberWentUp: true,
|
||||
},
|
||||
},
|
||||
dataRows: {
|
||||
rows: [
|
||||
{ key: "Market cap", value: "$ 1000000" },
|
||||
{ key: "24H VOL", value: "$ 1000000" },
|
||||
],
|
||||
},
|
||||
};
|
||||
return <MonoCard {...tokenomicsCard} />;
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
import { Box, useMediaQuery, useTheme } from "@mui/material";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useState } from "react";
|
||||
import Loading from "../loading";
|
||||
|
||||
const NivoLineChart = dynamic(
|
||||
() => import("@nivo/line").then((m) => m.ResponsiveLine),
|
||||
{
|
||||
loading: () => <Loading />,
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export interface ILineChartData {
|
||||
date_utc: string;
|
||||
numericData?: number;
|
||||
// purpleLineNumericData?: number;
|
||||
}
|
||||
|
||||
interface IAxes {
|
||||
x: Date;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface ILineAxes {
|
||||
id: string;
|
||||
data: Array<IAxes>;
|
||||
}
|
||||
|
||||
export const LineChart = ({
|
||||
data,
|
||||
color,
|
||||
label,
|
||||
}: {
|
||||
data: Array<ILineChartData>;
|
||||
color: string;
|
||||
label: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isDesktop = useMediaQuery(theme.breakpoints.up("lg"));
|
||||
|
||||
const [chartData, setChartData] = useState<Array<ILineAxes>>();
|
||||
|
||||
useEffect(() => {
|
||||
const resultData = transformData(data);
|
||||
if (resultData.length > 0) {
|
||||
setChartData(resultData);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const transformData = (data: Array<ILineChartData>) => {
|
||||
const lineData: ILineAxes = {
|
||||
id: label,
|
||||
data: [],
|
||||
};
|
||||
|
||||
// const purpleLineData: ILineAxes = {
|
||||
// id: "Numeric Data 2",
|
||||
// data: [],
|
||||
// };
|
||||
|
||||
data.map((item: ILineChartData) => {
|
||||
const axesGreenLineData: IAxes = {
|
||||
x: new Date(item.date_utc),
|
||||
y: item.numericData || 0,
|
||||
};
|
||||
|
||||
lineData.data.push(axesGreenLineData);
|
||||
|
||||
// const axesPurpleLineData: IAxes = {
|
||||
// x: new Date(item.date_utc),
|
||||
// y: item.purpleLineNumericData,
|
||||
// };
|
||||
|
||||
// purpleLineData.data.push(axesPurpleLineData);
|
||||
});
|
||||
return [{ ...lineData }];
|
||||
};
|
||||
|
||||
const yformat = (num: number | string | Date) => {
|
||||
if (typeof num === "number") {
|
||||
if (num >= 1000000000) {
|
||||
return `${(num / 1000000000).toFixed(1).replace(/\.0$/, "")}B`;
|
||||
}
|
||||
if (num >= 1000000) {
|
||||
return `${(num / 1000000).toFixed(1).replace(/\.0$/, "")}M`;
|
||||
}
|
||||
if (num >= 1000) {
|
||||
return `${(num / 1000).toFixed(1).replace(/\.0$/, "")}K`;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
throw new Error("Unexpected value");
|
||||
};
|
||||
|
||||
return (
|
||||
<Box width={"100%"} height={isDesktop ? 200 : 150}>
|
||||
{chartData && (
|
||||
<NivoLineChart
|
||||
curve="monotoneX"
|
||||
colors={[color]}
|
||||
data={chartData}
|
||||
animate
|
||||
enableSlices="x"
|
||||
margin={{
|
||||
bottom: 24,
|
||||
left: 30,
|
||||
right: 12,
|
||||
top: 20,
|
||||
}}
|
||||
theme={{
|
||||
grid: { line: { strokeWidth: 0 } },
|
||||
tooltip: { container: { color: "black" } },
|
||||
axis: {
|
||||
domain: {
|
||||
line: { stroke: "#C3D7D7", strokeWidth: 1, strokeOpacity: 1 },
|
||||
},
|
||||
ticks: {
|
||||
text: {
|
||||
fill: "#818386",
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
text: {
|
||||
fill: "#818386",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
xScale={{
|
||||
type: "time",
|
||||
format: "%Y-%m-%d",
|
||||
}}
|
||||
yScale={{ min: 1, type: "linear" }}
|
||||
xFormat="time:%Y-%m-%d"
|
||||
axisLeft={{
|
||||
legendOffset: 12,
|
||||
tickSize: 3,
|
||||
format: yformat,
|
||||
tickValues: 5,
|
||||
}}
|
||||
axisBottom={{
|
||||
format: "%b %d",
|
||||
legendOffset: -12,
|
||||
tickValues:
|
||||
chartData[0].data.length > 7 ? "every 5 days" : "every day",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
import { TABLET_WIDTH } from "@/app/constants";
|
||||
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import ErrorOutline from "@mui/icons-material/ErrorOutline";
|
||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Modal,
|
||||
Stack,
|
||||
type SxProps,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
import type React from "react";
|
||||
|
||||
export const modalStyle = (width: number | string = 600) => ({
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
width,
|
||||
transform: "translate(-50%, -50%)",
|
||||
bgcolor: "background.paper",
|
||||
boxShadow: 24,
|
||||
borderRadius: "16px",
|
||||
p: 4,
|
||||
});
|
||||
|
||||
export const StyledBackButton = ({
|
||||
onBack,
|
||||
label,
|
||||
fullWidth,
|
||||
sx,
|
||||
}: {
|
||||
onBack: () => void;
|
||||
label?: string;
|
||||
fullWidth?: boolean;
|
||||
sx?: SxProps;
|
||||
}) => (
|
||||
<Button
|
||||
disableFocusRipple
|
||||
size="large"
|
||||
fullWidth={fullWidth}
|
||||
variant="outlined"
|
||||
onClick={onBack}
|
||||
sx={sx}
|
||||
>
|
||||
{label || <ArrowBackIosNewIcon fontSize="small" />}
|
||||
</Button>
|
||||
);
|
||||
|
||||
export const SimpleModal: FCWithChildren<{
|
||||
open: boolean;
|
||||
hideCloseIcon?: boolean;
|
||||
displayErrorIcon?: boolean;
|
||||
displayInfoIcon?: boolean;
|
||||
headerStyles?: SxProps;
|
||||
subHeaderStyles?: SxProps;
|
||||
buttonFullWidth?: boolean;
|
||||
onClose?: () => void;
|
||||
onOk?: () => Promise<void>;
|
||||
onBack?: () => void;
|
||||
header: string | React.ReactNode;
|
||||
subHeader?: string;
|
||||
okLabel: string;
|
||||
backLabel?: string;
|
||||
backButtonFullWidth?: boolean;
|
||||
okDisabled?: boolean;
|
||||
sx?: SxProps;
|
||||
children?: React.ReactNode;
|
||||
}> = ({
|
||||
open,
|
||||
hideCloseIcon,
|
||||
displayErrorIcon,
|
||||
displayInfoIcon,
|
||||
headerStyles,
|
||||
buttonFullWidth,
|
||||
onClose,
|
||||
okDisabled,
|
||||
onOk,
|
||||
onBack,
|
||||
header,
|
||||
subHeader,
|
||||
okLabel,
|
||||
backLabel,
|
||||
backButtonFullWidth,
|
||||
sx,
|
||||
children,
|
||||
}) => {
|
||||
const isTablet = useMediaQuery(TABLET_WIDTH);
|
||||
const styles = modalStyle(isTablet ? 600 : "90%");
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={{ styles, ...sx }}>
|
||||
{displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />}
|
||||
{displayInfoIcon && <InfoOutlinedIcon sx={{ mb: 2, color: "blue" }} />}
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
{typeof header === "string" ? (
|
||||
<Typography
|
||||
fontSize={20}
|
||||
fontWeight={600}
|
||||
sx={{ color: "text.primary", ...headerStyles }}
|
||||
>
|
||||
{header}
|
||||
</Typography>
|
||||
) : (
|
||||
header
|
||||
)}
|
||||
{!hideCloseIcon && <CloseIcon onClick={onClose} cursor="pointer" />}
|
||||
</Stack>
|
||||
|
||||
<Typography mt={subHeader ? 0.5 : 0} mb={3} fontSize={12}>
|
||||
{subHeader}
|
||||
</Typography>
|
||||
|
||||
{children}
|
||||
|
||||
{(onOk || onBack) && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 2,
|
||||
mt: 2,
|
||||
width: buttonFullWidth ? "100%" : null,
|
||||
}}
|
||||
>
|
||||
{onBack && (
|
||||
<StyledBackButton
|
||||
onBack={onBack}
|
||||
label={backLabel}
|
||||
fullWidth={backButtonFullWidth}
|
||||
/>
|
||||
)}
|
||||
{onOk && (
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
size="large"
|
||||
onClick={onOk}
|
||||
disabled={okDisabled}
|
||||
>
|
||||
{okLabel}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { Typography } from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import LinearProgress from "@mui/material/LinearProgress";
|
||||
import * as React from "react";
|
||||
|
||||
export interface IDynamicProgressBarProps {
|
||||
overTitle?: string;
|
||||
start: string; // Start timestamp as ISO 8601 string
|
||||
showEpoch: boolean;
|
||||
}
|
||||
|
||||
export const DynamicProgressBar = (props: IDynamicProgressBarProps) => {
|
||||
const { start, showEpoch, overTitle } = props;
|
||||
const [progress, setProgress] = React.useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Parse the start timestamp
|
||||
const startTime = new Date(start).getTime();
|
||||
const endTime = startTime + 60 * 60 * 1000; // Add 1 hour to the start time
|
||||
|
||||
// Validate start timestamp
|
||||
if (Number.isNaN(startTime)) {
|
||||
console.error("Invalid start timestamp:", { start });
|
||||
return;
|
||||
}
|
||||
|
||||
// Function to calculate progress
|
||||
const calculateProgress = () => {
|
||||
const currentTime = Date.now();
|
||||
if (currentTime < startTime) {
|
||||
return 0;
|
||||
}
|
||||
if (currentTime >= endTime) {
|
||||
return 100;
|
||||
}
|
||||
const elapsed = currentTime - startTime;
|
||||
const total = endTime - startTime;
|
||||
return (elapsed / total) * 100;
|
||||
};
|
||||
|
||||
// Set initial progress and start timer
|
||||
setProgress(calculateProgress());
|
||||
const timer = setInterval(() => {
|
||||
setProgress(calculateProgress());
|
||||
}, 60000); // Update every minute (60000 milliseconds)
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [start]);
|
||||
|
||||
// Helper function to format date
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are 0-based
|
||||
const year = date.getFullYear();
|
||||
return `${hours}:${minutes}, ${day}/${month}/${year}`;
|
||||
};
|
||||
|
||||
const startTime = new Date(start).getTime();
|
||||
const endTime = startTime + 60 * 60 * 1000;
|
||||
|
||||
return (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
{overTitle && (
|
||||
<Typography fontSize={14} mb={2} textTransform={"uppercase"}>
|
||||
{overTitle}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
sx={{
|
||||
backgroundColor: "#CAD6D7",
|
||||
"& .MuiLinearProgress-bar": {
|
||||
backgroundColor: "#14E76F",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{showEpoch && (
|
||||
<Box mt={2}>
|
||||
<Box display={"flex"} justifyContent={"space-between"}>
|
||||
<Typography fontSize={14} textTransform={"uppercase"}>
|
||||
START:
|
||||
</Typography>
|
||||
<Typography fontSize={14}>
|
||||
{startTime ? formatDate(startTime) : ""}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display={"flex"} justifyContent={"space-between"}>
|
||||
<Typography fontSize={14} textTransform={"uppercase"}>
|
||||
END:
|
||||
</Typography>
|
||||
<Typography fontSize={14}>
|
||||
{endTime ? formatDate(endTime) : ""}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
declare type FCWithChildren<P> = React.FC<React.PropsWithChildren<P>>;
|
||||
Reference in New Issue
Block a user