diff --git a/src/components/dashboard-page/ArrowButton/index.js b/src/components/dashboard-page/ArrowButton/index.js
new file mode 100644
index 000000000..9694e9167
--- /dev/null
+++ b/src/components/dashboard-page/ArrowButton/index.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import cn from 'classnames';
+import { string, func } from 'prop-types';
+
+import { ReactComponent as AccentPointer } from '../../../assets/svg/dashboard/accent-pointer.svg';
+
+import './style.scss';
+
+const propTypes = {
+ text: string.isRequired,
+ onClick: func,
+ buttonCn: string,
+};
+
+const ArrowButton = ({ text, onClick, buttonCn }) => {
+ return (
+
+ {text}
+
+
+ );
+};
+
+ArrowButton.propTypes = propTypes;
+
+export default ArrowButton;
diff --git a/src/components/dashboard-page/ArrowButton/style.scss b/src/components/dashboard-page/ArrowButton/style.scss
new file mode 100644
index 000000000..38949f1c0
--- /dev/null
+++ b/src/components/dashboard-page/ArrowButton/style.scss
@@ -0,0 +1,12 @@
+@import '../../../styles/main';
+
+.arrow-button {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ @include t300;
+ font-weight: 600;
+ color: $dashboard-history-stages-buttons-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ cursor: pointer;
+}
diff --git a/src/components/dashboard-page/Backers/data.js b/src/components/dashboard-page/Backers/data.js
new file mode 100644
index 000000000..f6429b9eb
--- /dev/null
+++ b/src/components/dashboard-page/Backers/data.js
@@ -0,0 +1,5 @@
+import companyIcons from '../../../data/investors';
+
+const backersKeys = ['DCG', 'Hypersphere', 'DefiAlliance', 'D1Ventures', 'OKX', 'GSR'];
+
+export const backers = companyIcons.filter(companyIcon => backersKeys.includes(companyIcon.key));
diff --git a/src/components/dashboard-page/Backers/index.js b/src/components/dashboard-page/Backers/index.js
new file mode 100644
index 000000000..d342a1a9a
--- /dev/null
+++ b/src/components/dashboard-page/Backers/index.js
@@ -0,0 +1,54 @@
+import React from 'react';
+import { func } from 'prop-types';
+
+import SectionHeader from '../SectionHeader';
+import WidgetHeading from '../WidgetHeading';
+import StatsWidget from '../StatsWidget';
+import { PressStory } from '../../about-page/Press';
+
+import { backers } from './data';
+
+import './style.scss';
+
+const propTypes = {
+ t: func.isRequired,
+};
+
+const DashboardBackers = ({ t }) => {
+ return (
+
+
+
+
+
+ {backers.map(({ key, Icon }) => {
+ return (
+
+
+
+
+
+ );
+ })}
+
+
+
+
+ );
+};
+
+DashboardBackers.propTypes = propTypes;
+
+export default DashboardBackers;
diff --git a/src/components/dashboard-page/Backers/style.scss b/src/components/dashboard-page/Backers/style.scss
new file mode 100644
index 000000000..b35d99cf4
--- /dev/null
+++ b/src/components/dashboard-page/Backers/style.scss
@@ -0,0 +1,226 @@
+@import '../../../styles/main';
+
+.dashboard-backers {
+ @include dashboard-section;
+
+ &__container {
+ @include dashboard-container;
+ }
+
+ &__heading {
+ margin-bottom: 8px;
+ }
+
+ &__backers-list {
+ margin-bottom: 16px;
+ display: grid;
+ gap: 16px;
+ padding: 0;
+ list-style: none;
+ }
+
+ &__backers-list-item {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ }
+
+ &__backer {
+ height: 96px;
+ padding: 8px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: $dashboard-backer-widget-background-color;
+ border-radius: 8px;
+ }
+
+ &__backer-icon--inactive {
+ display: none;
+ }
+
+ &__coindesk-widgets-wrapper {
+ display: grid;
+ gap: 16px;
+ }
+
+ &__coindesk-story-link {
+ margin: 0;
+ border-radius: 12px;
+ transition: outline 0.25s ease-out;
+
+ &:hover {
+ outline: 1px solid $dashboard-press-story-hover-border-color;
+ }
+
+ & .AboutPage__press__story {
+ width: 100%;
+ height: unset;
+ max-width: none;
+ padding: 16px 0 0 16px;
+ gap: 16px;
+ }
+
+ & .AboutPage__press__story__about__section-title {
+ @include h100;
+ font-feature-settings: $dashboard-font-feature-settings;
+ letter-spacing: 0.84px;
+ }
+
+ & .AboutPage__press__story__about {
+ max-width: none;
+ padding-right: 16px;
+ }
+
+ & .AboutPage__press__story__about__title {
+ margin-top: 24px;
+ @include h500;
+ font-weight: 600;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ & .AboutPage__press__story__about__platform-description {
+ @include t300;
+ }
+
+ & .AboutPage__press__story__about__link {
+ @include t300;
+ }
+
+ & .AboutPage__press__story__visual {
+ max-width: none;
+ height: 260px;
+ }
+
+ & .AboutPage__press__story__visual__image {
+ width: 720px;
+ max-width: 720px;
+ border-radius: 12px;
+ }
+
+ & .AboutPage__press__story__visual__bottom-gradient {
+ transition-timing-function: ease-out;
+ }
+ }
+
+ @media #{$screen-min-dashboard-xs} {
+ &__backers-list {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ &__coindesk-story-link {
+ & .AboutPage__press__story__about__title {
+ margin-top: 40px;
+ }
+ }
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ & .dashboard-stats-widget__text-wrapper {
+ display: flex;
+ flex-direction: column;
+ }
+
+ & .dashboard-stats-widget__text {
+ @include h700;
+ }
+
+ &__backers-list {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ &__coindesk-story-link {
+ & {
+ width: 100%;
+ height: 424px;
+ overflow: hidden;
+ }
+
+ & .AboutPage__press__story {
+ padding: 0;
+ flex-direction: row;
+ gap: 0;
+ }
+
+ & .AboutPage__press__story__about {
+ padding: 32px;
+ width: 50%;
+ flex-shrink: 0;
+ }
+
+ & .AboutPage__press__story__about__title {
+ margin-top: 48px;
+ }
+
+ & .AboutPage__press__story__visual {
+ height: 424px;
+ padding-left: 48px;
+ transform: translateY(48px);
+ }
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & .dashboard-stats-widget {
+ display: flex;
+ flex-direction: column;
+ }
+
+ & .dashboard-stats-widget__text-wrapper {
+ height: 100%;
+ justify-content: center;
+ }
+
+ & .dashboard-stats-widget__text {
+ @include h800;
+ }
+
+ &__backers-list {
+ margin-bottom: 24px;
+ gap: 24px;
+ }
+
+ &__coindesk-widgets-wrapper {
+ gap: 24px;
+ grid-template-columns: repeat(3, 1fr);
+
+ & .dashboard-stats-widget {
+ order: 1;
+ grid-column: 3 / span 1;
+ }
+ }
+
+ &__coindesk-story-link {
+ height: 448px;
+ order: -1;
+ grid-column: 1 / span 2;
+
+ & .AboutPage__press__story__visual {
+ height: 448px;
+ }
+ }
+ }
+
+ @media #{$screen-min-dashboard-lg} {
+ &__backers-list {
+ grid-template-columns: repeat(6, 1fr);
+ }
+
+ &__coindesk-widgets-wrapper {
+ grid-template-columns: repeat(6, 1fr);
+
+ & .dashboard-stats-widget {
+ grid-column: 5 / span 2;
+ }
+ }
+
+ &__coindesk-story-link {
+ height: 392px;
+ grid-column: 1 / span 4;
+
+ & .AboutPage__press__story__visual {
+ height: 392px;
+ }
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Carousel/index.js b/src/components/dashboard-page/Carousel/index.js
new file mode 100644
index 000000000..ad1ca86a4
--- /dev/null
+++ b/src/components/dashboard-page/Carousel/index.js
@@ -0,0 +1,118 @@
+import React, { useState, useMemo } from 'react';
+import cn from 'classnames';
+import { CarouselProvider, Slider, ButtonBack, ButtonNext } from 'pure-react-carousel';
+import 'pure-react-carousel/dist/react-carousel.es.css';
+import { useMediaQuery } from 'react-responsive';
+import { arrayOf, node, bool, string } from 'prop-types';
+
+import useDashboardMedia from '../../../utils/useDashboardMedia';
+
+import { ReactComponent as ChevronRight } from '../../../assets/svg/dashboard/chevron-right.svg';
+
+import './style.scss';
+
+const propTypes = {
+ children: arrayOf(node),
+ withLgSlides: bool,
+ withExtraItem: bool,
+ carouselCn: string,
+};
+
+const defaultProps = {
+ withLgSlides: false,
+};
+
+const DashboardCarousel = ({ children, withLgSlides, withExtraItem, carouselCn }) => {
+ const isMd = useMediaQuery({ minWidth: 1024, maxWidth: 1279 });
+ const extraItems = useMemo(() => {
+ if (isMd) {
+ return 2;
+ }
+
+ return 1;
+ }, [isMd]);
+
+ const totalSlides = withExtraItem ? children.length + extraItems : children.length;
+ const [currentSlide, setCurrentSlide] = useState(0);
+ const { currentBreakpoints } = useDashboardMedia();
+
+ const visibleSlides = useMemo(
+ () =>
+ currentBreakpoints === 'xxs'
+ ? withLgSlides
+ ? 1
+ : 1.05
+ : currentBreakpoints === 'xs'
+ ? 1.45
+ : currentBreakpoints === 'sm'
+ ? 2
+ : currentBreakpoints === 'md'
+ ? withLgSlides
+ ? 2
+ : 3
+ : currentBreakpoints === 'lg'
+ ? withLgSlides
+ ? 2.8
+ : 4
+ : currentBreakpoints === 'xl'
+ ? withLgSlides
+ ? 2.8
+ : 4
+ : 2,
+ [currentBreakpoints, withLgSlides]
+ );
+
+ const largeScreens =
+ currentBreakpoints === 'sm' ||
+ currentBreakpoints === 'md' ||
+ currentBreakpoints === 'lg' ||
+ currentBreakpoints === 'xl';
+
+ const isButtonPrevSlideVisible = currentSlide > 0 && largeScreens;
+ const isButtonNextSlideVisible =
+ visibleSlides < totalSlides && totalSlides - visibleSlides > currentSlide && largeScreens;
+
+ return (
+
+ {children}
+ setCurrentSlide(prevSlide => prevSlide - 1)}
+ >
+
+
+ setCurrentSlide(prevSlide => prevSlide + 1)}
+ >
+
+
+
+
+
+ );
+};
+
+DashboardCarousel.propTypes = propTypes;
+DashboardCarousel.defaultProps = defaultProps;
+
+export default DashboardCarousel;
diff --git a/src/components/dashboard-page/Carousel/style.scss b/src/components/dashboard-page/Carousel/style.scss
new file mode 100644
index 000000000..5f91328f0
--- /dev/null
+++ b/src/components/dashboard-page/Carousel/style.scss
@@ -0,0 +1,94 @@
+@import '../../../styles/main';
+
+.dashboard-carousel {
+ position: relative;
+
+ &__button {
+ padding: 16px;
+ background-color: $dashboard-carousel-buttons-background-color;
+ border: 1px solid $dashboard-base-borders-color;
+ border-radius: 2px;
+ backdrop-filter: blur(2px);
+ transition: all $dashboard-transition-duration $dashboard-transition-timing;
+ z-index: 1;
+
+ visibility: hidden;
+ opacity: 0;
+
+ &.is-button-visible {
+ visibility: visible;
+ opacity: 1;
+ }
+
+ &:hover,
+ &:focus {
+ background-color: $dashboard-carousel-buttons-hover-background-color;
+ }
+
+ &:active {
+ background-color: $dashboard-carousel-buttons-pressed-background-color;
+ transition-duration: 0ms;
+ }
+ }
+
+ &__button-prev-slide {
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+
+ & > svg {
+ transform: rotate(180deg);
+ }
+ }
+
+ &__button-next-slide {
+ position: absolute;
+ right: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ }
+
+ & li {
+ padding-left: 0;
+ }
+
+ &__overlay {
+ position: absolute;
+ top: 0;
+ width: 130px;
+ height: 100%;
+ z-index: 0;
+ transition: all 0.25s ease-out;
+
+ visibility: hidden;
+ opacity: 0;
+
+ &.visible {
+ opacity: 1;
+ visibility: visible;
+ }
+ }
+
+ &__next-overlay {
+ right: 0;
+ background: linear-gradient(270deg, #000 26.56%, rgba(0, 0, 0, 0) 100%);
+ }
+
+ &__prev-overlay {
+ left: 0;
+ background: linear-gradient(90deg, #000 26.56%, rgba(0, 0, 0, 0) 100%);
+ }
+}
+
+.focusRing___1airF.carousel__slide-focus-ring {
+ outline: none !important;
+}
+
+.dashboard-joy-carousel__slide {
+ padding-right: 6px;
+
+ @media #{$screen-min-dashboard-sm} {
+ padding-right: 16px;
+ }
+}
diff --git a/src/components/dashboard-page/ChartWrapper/index.js b/src/components/dashboard-page/ChartWrapper/index.js
new file mode 100644
index 000000000..1a70bc7bb
--- /dev/null
+++ b/src/components/dashboard-page/ChartWrapper/index.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import { node, number } from 'prop-types';
+
+import './style.scss';
+
+const propTypes = {
+ children: node.isRequired,
+ chartHeight: number.isRequired,
+};
+
+/*
+ * This component addresses the slowness of ResponsiveContainer (recharts) when resizing the page.
+ * Workaround for https://github.com/recharts/recharts/issues/1767
+ */
+
+const ChartWrapper = ({ children, chartHeight }) => {
+ return (
+
+ );
+};
+
+ChartWrapper.propTypes = propTypes;
+
+export default ChartWrapper;
diff --git a/src/components/dashboard-page/ChartWrapper/style.scss b/src/components/dashboard-page/ChartWrapper/style.scss
new file mode 100644
index 000000000..55c071ece
--- /dev/null
+++ b/src/components/dashboard-page/ChartWrapper/style.scss
@@ -0,0 +1,12 @@
+.chart-wrapper {
+ position: relative;
+ width: 100%;
+
+ &__container {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ }
+}
diff --git a/src/components/dashboard-page/Community/Followers/index.js b/src/components/dashboard-page/Community/Followers/index.js
new file mode 100644
index 000000000..6dc631cc5
--- /dev/null
+++ b/src/components/dashboard-page/Community/Followers/index.js
@@ -0,0 +1,89 @@
+import React, { useState } from 'react';
+import cn from 'classnames';
+import { string, func, arrayOf, shape, bool } from 'prop-types';
+
+import WidgetHeading from '../../WidgetHeading';
+import { FollowersBlockSkeleton } from '../Skeletons';
+
+import './style.scss';
+
+const followerPropTypes = {
+ avatar: string.isRequired,
+ name: string.isRequired,
+ username: string.isRequired,
+ followersQuantity: string.isRequired,
+ setIsCarouselRunning: func.isRequired,
+};
+
+const Follower = ({ avatar, name, username, followersQuantity, setIsCarouselRunning }) => {
+ return (
+
+ setIsCarouselRunning(false)}
+ onMouseLeave={() => setIsCarouselRunning(true)}
+ >
+
+
{name}
+
{`@${username}`}
+
+ {followersQuantity}
+ Followers
+
+
+
+ );
+};
+
+Follower.propTypes = followerPropTypes;
+
+const { setIsCarouselRunning, ...followersRelatedPropTypes } = followerPropTypes;
+
+const followersPropTypes = {
+ followers: arrayOf(shape(followersRelatedPropTypes)),
+ loading: bool,
+};
+
+const Followers = ({ followers, loading }) => {
+ const shouldRenderAsCarousel = followers.length > 4;
+ const [isCarouselRunning, setIsCarouselRunning] = useState(true);
+
+ const renderFollowersList = ({ setIsCarouselRunning }) => {
+ return (
+
+ {followers.map((follower, index) => (
+
+
+
+ ))}
+
+ );
+ };
+
+ return (
+
+
+ {loading || !followers.length ? (
+
+ ) : (
+
+ {renderFollowersList({ setIsCarouselRunning })}
+ {shouldRenderAsCarousel && renderFollowersList({ setIsCarouselRunning })}
+
+ )}
+
+ );
+};
+
+Followers.propTypes = followersPropTypes;
+
+export default Followers;
diff --git a/src/components/dashboard-page/Community/Followers/style.scss b/src/components/dashboard-page/Community/Followers/style.scss
new file mode 100644
index 000000000..bdd1dc9bf
--- /dev/null
+++ b/src/components/dashboard-page/Community/Followers/style.scss
@@ -0,0 +1,114 @@
+@import '../../../../styles/main';
+@import '../../../index-page/shared-styles';
+
+.dashboard-community-followers {
+ margin-top: 16px;
+
+ &__heading {
+ margin-bottom: 8px;
+ }
+
+ &__grid {
+ display: flex;
+
+ overflow: auto;
+ scrollbar-width: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+
+ &.as-carousel {
+ overflow: hidden;
+ gap: 16px;
+ }
+ }
+
+ &__list {
+ padding: 0;
+ list-style: none;
+
+ display: flex;
+ gap: 16px;
+
+ &.in-carousel {
+ @include animate-carousel(16px, 50s);
+
+ &.carousel-paused {
+ animation-play-state: paused;
+ }
+ }
+ }
+
+ &__list-item {
+ margin: 0;
+ padding: 0;
+ }
+
+ &__follower {
+ width: 272px;
+ padding: 32px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ background-color: $dashboard-follower-widget-background-color;
+ border: 1px solid $dashboard-follower-widget-background-color;
+ border-radius: 8px;
+
+ &:hover {
+ border-color: $dashboard-follower-widget-hover-border-color;
+ }
+ }
+
+ &__follower-avatar {
+ width: 120px;
+ border-radius: 50%;
+ }
+
+ &__follower-name {
+ @include line-clamp(1);
+ @include t400;
+ font-weight: 600;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__follower-username {
+ @include t300;
+ color: $dashboard-followers-gray-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__follower-subscribers {
+ @extend .dashboard-community-followers__follower-username;
+ & > span {
+ color: $dashboard-content-base-text-color;
+ }
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__follower {
+ width: 326px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ margin-top: 24px;
+ }
+
+ &__grid {
+ &.as-carousel {
+ gap: 24px;
+ }
+ }
+
+ &__list {
+ gap: 24px;
+
+ &.in-carousel {
+ @include animate-carousel(24px, 50s);
+ }
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Community/OpenEvents/index.js b/src/components/dashboard-page/Community/OpenEvents/index.js
new file mode 100644
index 000000000..86adb2254
--- /dev/null
+++ b/src/components/dashboard-page/Community/OpenEvents/index.js
@@ -0,0 +1,122 @@
+import React, { useMemo } from 'react';
+import { string, instanceOf, arrayOf, shape, bool, number } from 'prop-types';
+
+import WidgetHeading from '../../WidgetHeading';
+import Carousel from '../../Carousel';
+import { OpenEventsBlockSkeleton } from '../Skeletons';
+import useDashboardMedia from '../../../../utils/useDashboardMedia';
+
+import { ReactComponent as SoundIcon } from '../../../../assets/svg/dashboard/sound.svg';
+
+import { eventsDateTimeFormat, eventsShortDateTimeFormat, isToday, isTomorrow, isSameDate } from '../utils';
+
+import './style.scss';
+
+const eventPropTypes = {
+ link: string.isRequired,
+ picture: string.isRequired,
+ name: string.isRequired,
+ date: instanceOf(Date).isRequired,
+ description: string.isRequired,
+ discordVoice: string.isRequired,
+ withDateLabel: bool,
+ eventsOnDateCount: number,
+};
+
+const Event = ({ link, picture, name, date, description, discordVoice, withDateLabel, eventsOnDateCount }) => {
+ const { currentBreakpoints } = useDashboardMedia();
+ const gap = useMemo(() => {
+ switch (currentBreakpoints) {
+ case 'xxs':
+ case 'xs':
+ case 'sm':
+ return 16;
+ default:
+ return 24;
+ }
+ }, [currentBreakpoints]);
+
+ const formattedDate = eventsDateTimeFormat.format(date);
+ const formattedDateLabel = `${formattedDate.split(',').join(' at')} CEST`;
+
+ const formattedShortDate = eventsShortDateTimeFormat.format(date);
+
+ return (
+
+
+
+
+
+
{name}
+
{formattedDateLabel}
+
{description}
+
+
+
+ {withDateLabel && (
+
1
+ ? { width: `calc(${eventsOnDateCount * 100}% + ${(eventsOnDateCount - 1) * gap}px)` }
+ : {}
+ }
+ >
+
{isToday(date) ? 'Today' : isTomorrow(date) ? 'Tomorrow' : formattedShortDate}
+
+ )}
+
+
+ );
+};
+
+Event.propTypes = eventPropTypes;
+
+const { withDateLabel, eventsOnDateCount, ...eventRequiredPropTypes } = eventPropTypes;
+
+const openEventsPropTypes = {
+ events: arrayOf(shape(eventRequiredPropTypes)),
+ loading: bool,
+};
+
+const OpenEvents = ({ events, loading }) => {
+ return (
+
+
+
+ {loading || !events.length ? (
+
+ ) : (
+
+ {events.map((e, index) => {
+ const firstEventOnDateIdx = events.findIndex(openEvent => isSameDate(openEvent.date, e.date));
+ const eventsOnDateCount = events.filter(openEvent => isSameDate(openEvent.date, e.date)).length;
+
+ return (
+
+ );
+ })}
+
+ )}
+
+ );
+};
+
+OpenEvents.propTypes = openEventsPropTypes;
+
+export default OpenEvents;
diff --git a/src/components/dashboard-page/Community/OpenEvents/style.scss b/src/components/dashboard-page/Community/OpenEvents/style.scss
new file mode 100644
index 000000000..e0c3cfe0a
--- /dev/null
+++ b/src/components/dashboard-page/Community/OpenEvents/style.scss
@@ -0,0 +1,137 @@
+@import '../../../../styles/main';
+@import '../../../index-page/shared-styles';
+
+.dashboard-community-open-events {
+ margin-top: 16px;
+
+ &__heading {
+ margin-bottom: 0;
+ }
+
+ &__carousel {
+ padding-top: 88px;
+ overflow-x: hidden;
+
+ & .horizontalSlider___281Ls {
+ overflow: visible;
+ }
+ }
+
+ &__event-link {
+ &:not(:last-child) {
+ margin-right: 16px;
+ }
+ }
+
+ &__event {
+ width: 272px;
+ flex-shrink: 0;
+ background-color: $dashboard-widget-base-background-color;
+ border: 1px solid $dashboard-widget-base-background-color;
+ border-radius: 8px;
+ position: relative;
+
+ &:hover {
+ border-color: $dashboard-base-border-color;
+ }
+ }
+
+ &__event-date-label {
+ height: 64px;
+ background-color: $dashboard-open-event-widget-label-background-color;
+ border: 1px solid $dashboard-base-borders-color;
+ border-radius: 8px;
+
+ position: absolute;
+ bottom: calc(100% + 16px);
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ & > p {
+ @include t400;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+ }
+
+ &__event-picture {
+ height: 177px;
+ background-position: left;
+ background-size: cover;
+ background-repeat: no-repeat;
+ border-top-left-radius: 8px;
+ border-top-right-radius: 8px;
+ }
+
+ &__event-descr-container {
+ padding: 24px;
+ }
+
+ &__event-descr-wrapper {
+ margin-bottom: 24px;
+ height: 128px;
+ }
+
+ &__event-name {
+ margin-bottom: 4px;
+ @include h400;
+ color: $dashboard-base-white-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ @include line-clamp(1);
+ }
+
+ &__event-date {
+ margin-bottom: 8px;
+ @include t200;
+ color: $dashboard-open-events-gray-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__event-descr {
+ @include t200;
+ color: $dashboard-base-gray-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ @include line-clamp(3);
+ }
+
+ &__event-discord-descr {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ &__event-discord-voice {
+ @include t200;
+ font-weight: 600;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__event {
+ width: 360px;
+ }
+
+ &__event-name {
+ @include h500;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ margin-top: 24px;
+ }
+
+ &__event-link {
+ &:not(:last-child) {
+ margin-right: 24px;
+ }
+ }
+
+ &__event {
+ width: 443px;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Community/Skeletons/index.js b/src/components/dashboard-page/Community/Skeletons/index.js
new file mode 100644
index 000000000..242a462cd
--- /dev/null
+++ b/src/components/dashboard-page/Community/Skeletons/index.js
@@ -0,0 +1,38 @@
+import React from 'react';
+import cn from 'classnames';
+
+import Skeleton from '../../Skeleton';
+
+import './style.scss';
+
+export const SocialMediaBlockSkeleton = () => {
+ return (
+
+ {Array.from({ length: 4 }, (_, i) => {
+ return (
+
+ );
+ })}
+
+ );
+};
+
+export const FollowersBlockSkeleton = () => {
+ return
;
+};
+
+export const OpenEventsBlockSkeleton = () => {
+ return (
+
+ {Array.from({ length: 4 }, (_, i) => {
+ return ;
+ })}
+
+ );
+};
diff --git a/src/components/dashboard-page/Community/Skeletons/style.scss b/src/components/dashboard-page/Community/Skeletons/style.scss
new file mode 100644
index 000000000..3b9549768
--- /dev/null
+++ b/src/components/dashboard-page/Community/Skeletons/style.scss
@@ -0,0 +1,75 @@
+@import '../../../../styles/main';
+
+.social-media-block-skeleton {
+ display: grid;
+ gap: 16px;
+
+ &__lg-stats-widget {
+ width: 100%;
+ height: 360px;
+ }
+
+ &__sm-stats-widget {
+ width: 100%;
+ height: 180px;
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ grid-template-rows: repeat(2, min-content);
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ gap: 24px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-lg} {
+ & {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ &__lg-stats-widget {
+ height: auto;
+ grid-row: 1 / span 2;
+ }
+
+ &__sm-stats-widget:last-child {
+ grid-column: 3;
+ }
+ }
+}
+
+.followers-block-skeleton {
+ width: 100%;
+ height: 276px;
+}
+
+.open-events-block-skeleton {
+ margin-top: 8px;
+ display: flex;
+ flex-wrap: nowrap;
+ gap: 16px;
+ overflow-x: auto;
+ scrollbar-width: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+
+ &__event {
+ width: 272px;
+ height: 390px;
+ flex-shrink: 0;
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__event {
+ width: 360px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ gap: 24px;
+ }
+}
diff --git a/src/components/dashboard-page/Community/SocialMedia/index.js b/src/components/dashboard-page/Community/SocialMedia/index.js
new file mode 100644
index 000000000..d0a1c75d7
--- /dev/null
+++ b/src/components/dashboard-page/Community/SocialMedia/index.js
@@ -0,0 +1,119 @@
+import React from 'react';
+import cn from 'classnames';
+import { func, string, oneOf, object } from 'prop-types';
+
+import WidgetHeading from '../../WidgetHeading';
+import ArrowButton from '../../ArrowButton';
+import Feature from '../../../Feature';
+
+import { ReactComponent as TwitterLogo } from '../../../../assets/svg/dashboard/twitter-logo.svg';
+import { ReactComponent as DiscordLogo } from '../../../../assets/svg/dashboard/discord-logo.svg';
+import { ReactComponent as TelegramLogo } from '../../../../assets/svg/dashboard/telegram-logo.svg';
+import tweetScoutLogo from '../../../../assets/images/dashboard/tweetscout-logo.png';
+
+import {
+ parseSocialMediaMemberCount,
+ parseSocialMediaMemberCountMonthlyChange,
+ parseTweetscoutScore,
+ parseTweetscoutLevel,
+} from '../utils';
+
+import './style.scss';
+
+const primaryStatsPropTypes = {
+ SocialMediaLogo: func.isRequired,
+ socialMediaName: string.isRequired,
+ mainStats: string.isRequired,
+ supplementalStats: string,
+ statsBlockBgColor: oneOf(['blue-bg', 'purple-bg']),
+};
+
+const PrimaryStats = ({ SocialMediaLogo, socialMediaName, mainStats, supplementalStats, statsBlockBgColor }) => {
+ return (
+
+
+
+
{socialMediaName}
+
{mainStats}
+
+ {supplementalStats || ' Last month'}
+
+
+
+ );
+};
+
+PrimaryStats.propTypes = primaryStatsPropTypes;
+
+const tweetScoutLink = 'https://tweetscout.io/search?q=joystreamdao';
+
+const socialMediaPropTypes = {
+ data: object,
+};
+
+const SocialMedia = ({ data }) => {
+ return (
+
+ );
+};
+
+SocialMedia.propTypes = socialMediaPropTypes;
+
+export default SocialMedia;
diff --git a/src/components/dashboard-page/Community/SocialMedia/style.scss b/src/components/dashboard-page/Community/SocialMedia/style.scss
new file mode 100644
index 000000000..0c8bce02e
--- /dev/null
+++ b/src/components/dashboard-page/Community/SocialMedia/style.scss
@@ -0,0 +1,177 @@
+@import '../../../../styles/main';
+
+.dashboard-community-social-media {
+ display: grid;
+ gap: 16px;
+
+ &__primary-stats-block {
+ height: 360px;
+ padding: 32px;
+ display: flex;
+ flex-direction: column;
+ border: 1px solid $dashboard-base-border-color;
+ border-radius: 8px;
+
+ &.blue-bg {
+ background: radial-gradient(299.34% 181.84% at 50% 50%, rgba(29, 161, 242, 0) 0%, #1da1f2 100%);
+ }
+
+ &.purple-bg {
+ background: radial-gradient(299.34% 181.84% at 50% 50%, rgba(114, 137, 218, 0) 0%, #7289da 100%);
+ }
+ }
+
+ &__stats-container {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ }
+
+ & > svg {
+ width: 40px;
+ height: 40px;
+ }
+
+ &__name {
+ @include dashboard-widget-heading;
+ }
+
+ &__main-stats {
+ @include h600;
+ color: $dashboard-base-white-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__supplemental-stats {
+ @include dashboard-widget-helper-text;
+
+ &.hidden {
+ opacity: 0;
+ visibility: hidden;
+ }
+ }
+
+ &__secondary-stats-block {
+ padding: 32px;
+ display: flex;
+ flex-direction: column;
+ background-color: $dashboard-widget-base-background-color;
+ border: 1px solid $dashboard-widget-base-background-color;
+ border-radius: 8px;
+ }
+
+ &__secondary-stats-container {
+ @extend .dashboard-community-social-media__stats-container;
+ gap: 8px;
+ }
+
+ &__extra-stats-block {
+ @extend .dashboard-community-social-media__secondary-stats-block;
+ padding-right: 21px;
+ display: flex;
+ flex-direction: column;
+ cursor: pointer;
+
+ &:hover {
+ border: 1px solid #dce1e56b;
+ }
+ }
+
+ &__extra-stats-container {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ &__stats {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+
+ &__extra-stats-block-header-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ }
+
+ &__extra-stats-heading {
+ & .dashboard-widget-heading__info-wrapper {
+ right: 0;
+
+ @media #{$screen-min-dashboard-xs} {
+ right: -53px;
+ }
+ }
+ }
+
+ &__extra-stats-social-media-logo {
+ width: 33px;
+ border-radius: 50%;
+ }
+
+ @media #{$screen-min-dashboard-xs} {
+ &__primary-stats-block {
+ height: 400px;
+ }
+
+ &__main-stats {
+ &.font-size-increased {
+ @include h800;
+ }
+ }
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ & {
+ grid-template-columns: repeat(2, 1fr);
+ grid-template-rows: repeat(2, min-content);
+ }
+
+ &__main-stats {
+ @include h700;
+
+ &.font-size-increased {
+ @include h1000;
+ }
+ }
+
+ &__secondary-stats-block {
+ height: 260px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ gap: 24px;
+ }
+
+ &__main-stats {
+ @include h900;
+ }
+ }
+
+ @media #{$screen-min-dashboard-lg} {
+ & {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ &__primary-stats-block {
+ height: auto;
+ grid-row: 1 / span 2;
+ }
+
+ &__secondary-stats-block {
+ height: 376px;
+ }
+
+ &__extra-stats-block {
+ height: 260px;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Community/index.js b/src/components/dashboard-page/Community/index.js
new file mode 100644
index 000000000..6876d4362
--- /dev/null
+++ b/src/components/dashboard-page/Community/index.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import { object, bool } from 'prop-types';
+
+import SectionHeader from '../SectionHeader';
+import SocialMedia from './SocialMedia';
+import Followers from './Followers';
+import OpenEvents from './OpenEvents';
+import { SocialMediaBlockSkeleton } from './Skeletons';
+
+import { parseFollowers, parseDiscrordEvents } from './utils';
+
+import './style.scss';
+
+const propTypes = {
+ data: object,
+ loading: bool,
+};
+
+const Community = ({ data, loading }) => {
+ const { featuredFollowers, discordEvents, ...socialMediaData } = data ?? {};
+
+ const parsedFollowers = parseFollowers(featuredFollowers);
+ const parsedEvents = parseDiscrordEvents(discordEvents);
+
+ return (
+
+
+
+
+ {loading ? : }
+
+
+
+
+
+ );
+};
+
+Community.propTypes = propTypes;
+
+export default Community;
diff --git a/src/components/dashboard-page/Community/style.scss b/src/components/dashboard-page/Community/style.scss
new file mode 100644
index 000000000..49293e1ed
--- /dev/null
+++ b/src/components/dashboard-page/Community/style.scss
@@ -0,0 +1,9 @@
+@import '../../../styles/main';
+
+.dashboard-community {
+ @include dashboard-section;
+
+ &__container {
+ @include dashboard-container;
+ }
+}
diff --git a/src/components/dashboard-page/Community/utils.js b/src/components/dashboard-page/Community/utils.js
new file mode 100644
index 000000000..36c7fe425
--- /dev/null
+++ b/src/components/dashboard-page/Community/utils.js
@@ -0,0 +1,80 @@
+/* eslint-disable max-len */
+
+import genericEventPicture from '../../../assets/images/dashboard/generic-event-picture.png';
+
+export const msInDay = 24 * 60 * 60 * 1000;
+export const eventsDateTimeFormat = new Intl.DateTimeFormat('en-GB', {
+ timeZone: 'Europe/Berlin', // IANA time zone identifier for CEST
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+});
+
+export const eventsShortDateTimeFormat = new Intl.DateTimeFormat('en-GB', {
+ timeZone: 'Europe/Berlin', // IANA time zone identifier for CEST
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+});
+
+export const isToday = date => new Date().toDateString() === new Date(date).toDateString();
+
+export const isTomorrow = date =>
+ new Date(new Date().getTime() + msInDay).toDateString() === new Date(date).toDateString();
+
+export const isSameDate = (date, anotherDate) => new Date(date).toDateString() === new Date(anotherDate).toDateString();
+
+export const parseSocialMediaMemberCount = (data, key) => {
+ const socialMediaMemberCount = data[key];
+ if (!socialMediaMemberCount) {
+ return '0K';
+ }
+
+ const socialMediaMemberCountInThousands = socialMediaMemberCount / 1000;
+
+ return `${socialMediaMemberCountInThousands.toFixed(1)}K`;
+};
+
+export const parseSocialMediaMemberCountMonthlyChange = (data, key) => {
+ const socialMediaMemberCountMonthlyChange = data[key];
+ if (!socialMediaMemberCountMonthlyChange) {
+ return '0% Last month';
+ }
+
+ const roundedSocialMediaMemberCountMonthlyChange = Math.round(socialMediaMemberCountMonthlyChange);
+ const socialMediaMemberCountMonthlyChangeWithSign =
+ roundedSocialMediaMemberCountMonthlyChange > 0
+ ? `+${roundedSocialMediaMemberCountMonthlyChange}%`
+ : `${roundedSocialMediaMemberCountMonthlyChange}%`;
+
+ return `${socialMediaMemberCountMonthlyChangeWithSign} Last month`;
+};
+
+export const parseTweetscoutScore = data => Math.round(data.tweetscoutScore) || 0;
+
+export const parseTweetscoutLevel = data => `Level ${data.tweetscoutLevel || 0}`;
+
+export const parseFollowers = (followers = []) =>
+ followers.map(follower => ({
+ avatar: follower.avatar,
+ name: follower.name,
+ username: follower.screenName,
+ followersQuantity: `${Math.round(follower.followersCount / 1000)} K`,
+ }));
+
+const defaultEventLink = 'https://discord.gg/NaNzysB5YZ';
+
+export const parseDiscrordEvents = (events = []) =>
+ events
+ .filter(e => new Date(e.scheduledStartTime).getTime() >= new Date().getTime())
+ .map(e => ({
+ link: defaultEventLink,
+ date: new Date(e.scheduledStartTime),
+ name: e.name,
+ description: e.description,
+ picture: e.image || genericEventPicture,
+ discordVoice: !!e.location ? `Discord - ${e.location}` : 'Discord',
+ }))
+ .sort((eventA, eventB) => eventA.date - eventB.date);
diff --git a/src/components/dashboard-page/Comparison/Positioning/data.js b/src/components/dashboard-page/Comparison/Positioning/data.js
new file mode 100644
index 000000000..8bf3ad610
--- /dev/null
+++ b/src/components/dashboard-page/Comparison/Positioning/data.js
@@ -0,0 +1,79 @@
+export const columns = [
+ {
+ header: '',
+ accessorKey: 'indicator',
+ },
+ {
+ header: 'Joystream',
+ accessorKey: 'joystream',
+ },
+ {
+ header: 'Lbry',
+ accessorKey: 'lbry',
+ },
+ {
+ header: 'Rumble',
+ accessorKey: 'rumble',
+ },
+ {
+ header: 'Deso',
+ accessorKey: 'deso',
+ },
+ {
+ header: 'Theta',
+ accessorKey: 'theta',
+ },
+];
+
+const getFDV = (val, fallbackVal = 0) => Number((val / 1000000).toFixed(1)) || fallbackVal;
+
+export const getData = (dynamicData = {}) => [
+ {
+ indicator: 'FDV',
+ joystream: getFDV(dynamicData?.token?.fullyDilutedValue),
+ lbry: getFDV(dynamicData?.token?.fdvs?.lbc),
+ rumble: getFDV(dynamicData?.token?.fdvs?.rum),
+ deso: getFDV(dynamicData?.token?.fdvs?.deso),
+ theta: getFDV(dynamicData?.token?.fdvs?.theta),
+ },
+ {
+ indicator: 'Open social graph',
+ joystream: true,
+ lbry: true,
+ rumble: false,
+ deso: true,
+ theta: false,
+ },
+ {
+ indicator: 'Video NFTs',
+ joystream: true,
+ lbry: false,
+ rumble: false,
+ deso: true,
+ theta: false,
+ },
+ {
+ indicator: 'Creator tokens',
+ joystream: true,
+ lbry: false,
+ rumble: false,
+ deso: true,
+ theta: false,
+ },
+ {
+ indicator: 'Storage & Delivery',
+ joystream: true,
+ lbry: true,
+ rumble: true,
+ deso: false,
+ theta: true,
+ },
+ {
+ indicator: 'Dao',
+ joystream: true,
+ lbry: false,
+ rumble: false,
+ deso: false,
+ theta: false,
+ },
+];
diff --git a/src/components/dashboard-page/Comparison/Positioning/index.js b/src/components/dashboard-page/Comparison/Positioning/index.js
new file mode 100644
index 000000000..f1a1f3b6c
--- /dev/null
+++ b/src/components/dashboard-page/Comparison/Positioning/index.js
@@ -0,0 +1,117 @@
+/* eslint-disable max-len */
+
+import React from 'react';
+import { object } from 'prop-types';
+
+import WidgetHeading from '../../WidgetHeading';
+import Feature from '../../../Feature';
+
+import { ReactComponent as SuccessIcon } from '../../../../assets/svg/dashboard/success.svg';
+import { ReactComponent as ErrorIcon } from '../../../../assets/svg/dashboard/error.svg';
+
+import { columns, getData } from './data';
+
+import './style.scss';
+
+const propTypes = {
+ dynamicData: object,
+};
+
+const Positioning = ({ dynamicData }) => {
+ const data = getData(dynamicData);
+
+ const fdvsRowData = data.find(val => val.indicator === 'FDV');
+ const fdvs = Object.values(fdvsRowData || {}).filter(val => typeof val === 'number');
+
+ const getFdvBarHeight = fdv => {
+ const range = fdv >= 1000 ? 'over1B' : fdv >= 100 ? 'between100MAnd1B' : 'under100M';
+ const fdvsInRange = fdvs.filter(fdv => {
+ switch (range) {
+ case 'over1B':
+ return fdv >= 1000;
+ case 'between100MAnd1B':
+ return fdv >= 100 && fdv < 1000;
+ default:
+ return fdv < 100;
+ }
+ });
+ const maxFdvInRange = Math.max(...fdvsInRange);
+ // Assume vals > 1B max-height: 100%; 100M <= vals < 1B max-height: 50% and vals < 100M max-height: 25%
+ const rangeMaxPercentage = range === 'over1B' ? 100 : range === 'between100MAnd1B' ? 50 : 25;
+
+ return (fdv * rangeMaxPercentage) / maxFdvInRange;
+ };
+
+ const renderCell = cellData => {
+ switch (typeof cellData) {
+ case 'string':
+ return cellData;
+ case 'number':
+ const height = `${getFdvBarHeight(cellData)}%`;
+ return (
+
+ {cellData > 1000 ? `$${Math.round(cellData / 1000)}B` : `$${cellData}M`}
+
+ );
+ case 'boolean':
+ return cellData ?
:
;
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {columns.map((column, index) => (
+
+ {column.header}
+
+ ))}
+
+
+
+ {data.map((rowData, index) => (
+
+ {columns.map((column, index) => {
+ return (
+
+ {renderCell(rowData[column.accessorKey])}
+
+ );
+ })}
+
+ ))}
+
+
+
+
+ );
+};
+
+Positioning.propTypes = propTypes;
+
+export default Positioning;
diff --git a/src/components/dashboard-page/Comparison/Positioning/style.scss b/src/components/dashboard-page/Comparison/Positioning/style.scss
new file mode 100644
index 000000000..17df984c2
--- /dev/null
+++ b/src/components/dashboard-page/Comparison/Positioning/style.scss
@@ -0,0 +1,82 @@
+@import '../../../../styles/main';
+
+.dashboard-comparison-positioning {
+ @include dashboard-widget;
+
+ &__table-wrapper {
+ overflow-x: auto;
+ scrollbar-width: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ }
+
+ &__table {
+ width: 100%;
+ min-width: 495px;
+ table-layout: fixed;
+ }
+
+ &__table-row {
+ border-bottom: 1px solid $dashboard-base-borders-color;
+ }
+
+ &__table-cell {
+ min-height: 1px;
+
+ font-family: $font-secondary;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+
+ &:first-of-type {
+ padding-left: 12px;
+ }
+
+ &:last-of-type {
+ padding-right: 12px;
+ }
+ }
+
+ &__table-head-cell {
+ padding: 12px 4px;
+ @include t200;
+ text-align: start;
+ }
+
+ &__table-body {
+ & > .dashboard-comparison-positioning__table-row:first-of-type {
+ height: 136px;
+
+ & > td {
+ height: 1px;
+ }
+
+ & > td > div {
+ font-size: 10px;
+ line-height: 1.6;
+ letter-spacing: 0.1px;
+ }
+
+ @-moz-document url-prefix() {
+ & > td {
+ height: 100%;
+ }
+ }
+ }
+ }
+
+ &__table-body-cell {
+ padding: 16px 4px;
+ vertical-align: bottom;
+ @include t200;
+ }
+
+ &__bar {
+ min-height: 20px;
+ padding: 4px;
+ display: flex;
+ align-items: end;
+ background-color: #bbd9f621;
+ border-radius: 4px;
+ }
+}
diff --git a/src/components/dashboard-page/Comparison/Skeletons/index.js b/src/components/dashboard-page/Comparison/Skeletons/index.js
new file mode 100644
index 000000000..d7c4136da
--- /dev/null
+++ b/src/components/dashboard-page/Comparison/Skeletons/index.js
@@ -0,0 +1,9 @@
+import React from 'react';
+
+import Skeleton from '../../Skeleton';
+
+import './style.scss';
+
+export const PositioningSkeleton = () => {
+ return
;
+};
diff --git a/src/components/dashboard-page/Comparison/Skeletons/style.scss b/src/components/dashboard-page/Comparison/Skeletons/style.scss
new file mode 100644
index 000000000..4bd0046af
--- /dev/null
+++ b/src/components/dashboard-page/Comparison/Skeletons/style.scss
@@ -0,0 +1,4 @@
+.positioning-skeleton {
+ width: 100%;
+ height: 524px;
+}
diff --git a/src/components/dashboard-page/Comparison/index.js b/src/components/dashboard-page/Comparison/index.js
new file mode 100644
index 000000000..6713f9a01
--- /dev/null
+++ b/src/components/dashboard-page/Comparison/index.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import { object, bool } from 'prop-types';
+
+import SectionHeader from '../SectionHeader';
+import Positioning from './Positioning';
+import { PositioningSkeleton } from './Skeletons';
+
+import './style.scss';
+
+const propTypes = {
+ data: object,
+ loading: bool,
+};
+
+const Comparison = ({ data, loading }) => {
+ return (
+
+ );
+};
+
+Comparison.propTypes = propTypes;
+
+export default Comparison;
diff --git a/src/components/dashboard-page/Comparison/style.scss b/src/components/dashboard-page/Comparison/style.scss
new file mode 100644
index 000000000..f752e9f15
--- /dev/null
+++ b/src/components/dashboard-page/Comparison/style.scss
@@ -0,0 +1,9 @@
+@import '../../../styles/main';
+
+.dashboard-comparison {
+ @include dashboard-section;
+
+ &__container {
+ @include dashboard-container;
+ }
+}
diff --git a/src/components/dashboard-page/Engineering/Chart/index.js b/src/components/dashboard-page/Engineering/Chart/index.js
new file mode 100644
index 000000000..853e0019c
--- /dev/null
+++ b/src/components/dashboard-page/Engineering/Chart/index.js
@@ -0,0 +1,134 @@
+import React, { useRef, useState } from 'react';
+import { ResponsiveContainer, AreaChart, CartesianGrid, XAxis, Text, YAxis, Area, Tooltip } from 'recharts';
+import { arrayOf, shape, instanceOf, number } from 'prop-types';
+
+import { formatDateToShowInTooltip } from '../../Token/PriceChart/utils';
+import { formatXAxisTick, renderCustomActiveDot } from './utils';
+
+const propTypes = {
+ chartData: arrayOf(
+ shape({
+ date: instanceOf(Date).isRequired,
+ contributions: number.isRequired,
+ })
+ ).isRequired,
+};
+
+const Chart = ({ chartData }) => {
+ const cartesianGridRef = useRef(null);
+ const chartWidth = cartesianGridRef.current?.props.offset.width || 0;
+ const chartOffsetLeft = cartesianGridRef.current?.props.offset.left || 0;
+
+ const maxCommitsCount = Math.max(...chartData.map(val => val.contributions));
+
+ /**
+ * Triggering re-render to obtain CartesianGrid ref value which is null on the first render.
+ */
+ const [key, setKey] = useState('0');
+
+ return (
+
+
+
+ {
+ return (
+
+ {formatXAxisTick(tickProps.payload.value)}
+
+ );
+ }}
+ tickLine={false}
+ tickMargin={16}
+ axisLine={false}
+ />
+ = 20
+ ticks={[0, 6, 12, 18, maxCommitsCount]}
+ tick={tickProps => (
+
+ {tickProps.payload.value}
+
+ )}
+ tickLine={false}
+ tickMargin={40}
+ axisLine={{ stroke: '#BBD9F621' }}
+ />
+ {
+ return renderCustomActiveDot(areaChartActiveDotProps, chartWidth, chartOffsetLeft);
+ }}
+ // isAnimationActive={false}
+ animationDuration={0}
+ onAnimationEnd={() => setKey('1')}
+ />
+ }
+ content={tooltipContentProps => }
+ />
+
+
+ );
+};
+
+function CustomCursor(tooltipCursorProps) {
+ const [points1] = tooltipCursorProps.points;
+ const axisIndent = 8;
+ const lineHeight = tooltipCursorProps.height + tooltipCursorProps.top + axisIndent;
+
+ return (
+
+
+
+
+ {formatDateToShowInTooltip((tooltipCursorProps.payload || [])[0]?.payload.date)}
+
+
+ );
+}
+
+function CustomTooltip(tooltipContentProps) {
+ const { active, payload } = tooltipContentProps;
+
+ if (active && payload && payload.length) {
+ return (
+
+
+
{formatDateToShowInTooltip(payload[0].payload.date)}
+
+
+
Contributions:
+
{payload[0].payload.contributions}
+
+
+ );
+ }
+ return null;
+}
+
+Chart.propTypes = propTypes;
+
+export default Chart;
diff --git a/src/components/dashboard-page/Engineering/Chart/utils.js b/src/components/dashboard-page/Engineering/Chart/utils.js
new file mode 100644
index 000000000..d97d9c696
--- /dev/null
+++ b/src/components/dashboard-page/Engineering/Chart/utils.js
@@ -0,0 +1,57 @@
+import React from 'react';
+
+export const formatXAxisTick = (datestr, locale = 'en-US') => {
+ const date = new Date(datestr);
+ const day = date.getDate().toString();
+ const daysToShow = ['3', '5', '8', '12', '15', '19', '22', '25', '27', '30'];
+
+ if (day === '1') {
+ return date.toLocaleString(locale, { month: 'short' });
+ }
+
+ if (daysToShow.includes(day)) {
+ return day;
+ }
+
+ return '';
+};
+
+export const renderCustomActiveDot = (areaChartActiveDotProps, chartWidth, chartOffsetLeft) => {
+ const contributions = areaChartActiveDotProps.payload.contributions;
+
+ return (
+
+
+
+
+
+
+
+ {contributions}
+
+
+ );
+};
diff --git a/src/components/dashboard-page/Engineering/ChartWidget/index.js b/src/components/dashboard-page/Engineering/ChartWidget/index.js
new file mode 100644
index 000000000..6071805cd
--- /dev/null
+++ b/src/components/dashboard-page/Engineering/ChartWidget/index.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import { arrayOf, shape, instanceOf, number } from 'prop-types';
+
+import WidgetHeading from '../../WidgetHeading';
+import Chart from '../Chart';
+
+import './style.scss';
+
+const propTypes = {
+ chartData: arrayOf(
+ shape({
+ date: instanceOf(Date).isRequired,
+ contributions: number.isRequired,
+ })
+ ).isRequired,
+};
+
+const ChartWidget = ({ chartData }) => {
+ return (
+
+
+
+
+ );
+};
+
+ChartWidget.propTypes = propTypes;
+
+export default ChartWidget;
diff --git a/src/components/dashboard-page/Engineering/ChartWidget/style.scss b/src/components/dashboard-page/Engineering/ChartWidget/style.scss
new file mode 100644
index 000000000..e138b7cd4
--- /dev/null
+++ b/src/components/dashboard-page/Engineering/ChartWidget/style.scss
@@ -0,0 +1,12 @@
+@import '../../../../styles/main';
+
+.dashboard-engineering-chart-widget {
+ @include dashboard-widget;
+ margin-top: 16px;
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ margin-top: 24px;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Engineering/Contributors/index.js b/src/components/dashboard-page/Engineering/Contributors/index.js
new file mode 100644
index 000000000..5be184e9b
--- /dev/null
+++ b/src/components/dashboard-page/Engineering/Contributors/index.js
@@ -0,0 +1,86 @@
+import React, { useMemo, useEffect, useState } from 'react';
+import { arrayOf, shape, string } from 'prop-types';
+
+import useDashboardMedia from '../../../../utils/useDashboardMedia';
+
+import './style.scss';
+
+const propTypes = {
+ topContributors: arrayOf(
+ shape({
+ avatar: string,
+ name: string.isRequired,
+ username: string,
+ })
+ ).isRequired,
+};
+
+const Contributors = ({ topContributors }) => {
+ const { currentBreakpoints } = useDashboardMedia();
+
+ const totalCount = topContributors.length;
+ const initShownCount = useMemo(() => {
+ switch (currentBreakpoints) {
+ case 'xxs':
+ case 'xs':
+ return 3;
+ case 'sm':
+ return 6;
+ case 'md':
+ return 8;
+ default:
+ return 12;
+ }
+ }, [currentBreakpoints]);
+
+ useEffect(() => {
+ setShownCount(initShownCount);
+ }, [initShownCount]);
+
+ const initHiddenCount = useMemo(() => totalCount - initShownCount, [totalCount, initShownCount]);
+
+ const [shownCount, setShownCount] = useState(initShownCount);
+ const toggleShownCount = () =>
+ setShownCount(prevShownCount => (prevShownCount === initShownCount ? totalCount : initShownCount));
+
+ const shownContributors = useMemo(() => topContributors.slice(0, shownCount), [topContributors, shownCount]);
+ const toggleButtonText = useMemo(() => {
+ if (shownCount === initShownCount) {
+ return `Show ${totalCount} top contributors`;
+ }
+ return `Hide ${initHiddenCount} contributors`;
+ }, [initHiddenCount, initShownCount, totalCount, shownCount]);
+
+ const getUnifiedUsername = user => user.username || user.name;
+
+ return (
+
+
+
+ {toggleButtonText}
+
+
+ );
+};
+
+Contributors.propTypes = propTypes;
+
+export default Contributors;
diff --git a/src/components/dashboard-page/Engineering/Contributors/style.scss b/src/components/dashboard-page/Engineering/Contributors/style.scss
new file mode 100644
index 000000000..e4bb95bd6
--- /dev/null
+++ b/src/components/dashboard-page/Engineering/Contributors/style.scss
@@ -0,0 +1,112 @@
+@import '../../../../styles/main';
+
+.dashboard-engineering-contributors {
+ &__list {
+ padding: 0;
+ list-style: none;
+
+ display: grid;
+ gap: 16px;
+ }
+
+ &__list-item {
+ margin: 0;
+ padding: 0;
+ }
+
+ &__contributor {
+ height: 140px;
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ background-color: $dashboard-contibutor-widget-background-color;
+ border: 1px solid $dashboard-contibutor-widget-background-color;
+ border-radius: 8px;
+
+ &:hover {
+ border-color: $dashboard-contibutor-widget-border-color;
+ cursor: pointer;
+ }
+ }
+
+ &__contributor-avatar {
+ width: 56px;
+ border-radius: 50%;
+ }
+
+ &__contributor-name {
+ @include t300;
+ font-weight: 600;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+
+ max-width: 145px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &__contributor-username {
+ @include t200;
+ color: $dashboard-base-gray-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ text-overflow: ellipsis;
+ }
+
+ &__button-toggle-shown {
+ display: block;
+ margin: 16px auto 0;
+ // width: 226px;
+ height: 48px;
+ padding: 12px 20px;
+ @include t300;
+ font-weight: 600;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ background-color: $dashboard-widget-base-background-color;
+ border: 1px solid $dashboard-base-borders-color;
+ border-radius: 2px;
+ cursor: pointer;
+ transition: background-color $dashboard-transition-duration $dashboard-transition-timing;
+
+ &:hover,
+ &:focus {
+ background-color: $dashboard-buttons-base-hover-background-color;
+ }
+
+ &:active {
+ background-color: $dashboard-buttons-base-pressed-background-color;
+ transition-duration: 0ms;
+ }
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__list {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ &__contributor {
+ height: 172px;
+ padding: 32px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ &__list {
+ gap: 24px;
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ &__button-toggle-shown {
+ margin-top: 24px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-lg} {
+ &__list {
+ grid-template-columns: repeat(6, 1fr);
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Engineering/GithubStats/index.js b/src/components/dashboard-page/Engineering/GithubStats/index.js
new file mode 100644
index 000000000..f9d26d747
--- /dev/null
+++ b/src/components/dashboard-page/Engineering/GithubStats/index.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import { string, oneOfType, number } from 'prop-types';
+
+import WidgetHeading from '../../WidgetHeading';
+
+import './style.scss';
+
+const propTypes = {
+ metrics: string.isRequired,
+ value: oneOfType([string, number]),
+ termDefinitionKey: string,
+};
+
+export const GithubStats = ({ metrics, value, termDefinitionKey }) => {
+ return (
+
+ );
+};
+
+GithubStats.propTypes = propTypes;
+
+export default GithubStats;
diff --git a/src/components/dashboard-page/Engineering/GithubStats/style.scss b/src/components/dashboard-page/Engineering/GithubStats/style.scss
new file mode 100644
index 000000000..834996d0b
--- /dev/null
+++ b/src/components/dashboard-page/Engineering/GithubStats/style.scss
@@ -0,0 +1,12 @@
+@import '../../../../styles/main';
+
+.dashboard-engineering-github-stats {
+ & .dashboard-widget-heading__heading.dim-heading {
+ @include t400;
+ }
+
+ &__value {
+ @include dashboard-widget-text;
+ @include h600;
+ }
+}
diff --git a/src/components/dashboard-page/Engineering/Skeletons/index.js b/src/components/dashboard-page/Engineering/Skeletons/index.js
new file mode 100644
index 000000000..7945f32ba
--- /dev/null
+++ b/src/components/dashboard-page/Engineering/Skeletons/index.js
@@ -0,0 +1,22 @@
+import React from 'react';
+
+import Skeleton from '../../Skeleton';
+
+import './style.scss';
+
+export const StatsBlockSkeleton = () => {
+ return (
+
+
+
+
+ );
+};
+
+export const ChartBlockSkeleton = () => {
+ return
;
+};
+
+export const ContributorsBlockSkeleton = () => {
+ return
;
+};
diff --git a/src/components/dashboard-page/Engineering/Skeletons/style.scss b/src/components/dashboard-page/Engineering/Skeletons/style.scss
new file mode 100644
index 000000000..5dfe0eb89
--- /dev/null
+++ b/src/components/dashboard-page/Engineering/Skeletons/style.scss
@@ -0,0 +1,65 @@
+@import '../../../../styles/main';
+
+.stats-block-skeleton {
+ display: grid;
+ gap: 16px;
+
+ &__github-stats {
+ width: 100%;
+ height: 600px;
+ }
+
+ &__followers {
+ width: 100%;
+ height: 120px;
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__github-stats {
+ height: 360px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ gap: 24px;
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ &__github-stats {
+ grid-column: 1 / span 3;
+ }
+
+ &__followers {
+ height: auto;
+ }
+ }
+}
+
+.chart-block-skeleton {
+ margin-top: 16px;
+ width: 100%;
+ height: 288px;
+
+ @media #{$screen-min-dashboard-sm} {
+ & {
+ height: 320px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ margin-top: 24px;
+ }
+ }
+}
+
+.contributors-block-skeleton {
+ margin-top: 16px;
+ width: 100%;
+ height: 400px;
+
+ @media #{$screen-min-dashboard-md} {
+ margin-top: 24px;
+ }
+}
diff --git a/src/components/dashboard-page/Engineering/data.js b/src/components/dashboard-page/Engineering/data.js
new file mode 100644
index 000000000..c3396834c
--- /dev/null
+++ b/src/components/dashboard-page/Engineering/data.js
@@ -0,0 +1,80 @@
+import { withFallbackNumVal } from '../../../utils/withFallbackVal';
+
+export const parseGithubStats = (data = {}) => [
+ {
+ metrics: 'Stars',
+ value: withFallbackNumVal(data.numberOfStars),
+ termDefinitionKey: 'stars',
+ },
+ {
+ metrics: 'Commits',
+ value: `${Math.round(withFallbackNumVal(data.numberOfCommits) / 1000)}K`,
+ termDefinitionKey: 'commits',
+ },
+ {
+ metrics: 'Commits this week',
+ value: withFallbackNumVal(data.totalNumberOfCommitsThisWeek),
+ termDefinitionKey: 'commitsThisWeek',
+ },
+ {
+ metrics: 'Open PRs',
+ value: withFallbackNumVal(data.numberOfOpenPRs),
+ termDefinitionKey: 'openPrs',
+ },
+ {
+ metrics: 'Open issues',
+ value: withFallbackNumVal(data.numberOfOpenIssues),
+ termDefinitionKey: 'openIssues',
+ },
+ {
+ metrics: 'Repositories',
+ value: withFallbackNumVal(data.numberOfRepositories),
+ termDefinitionKey: 'repositories',
+ },
+];
+
+export const parseFollowersCount = (data = {}) => data.numberOfFollowers || 0;
+
+export const desiredContributionsMonthsOrder = {
+ '12': 0,
+ '01': 1,
+ '02': 2,
+ '03': 3,
+ '04': 4,
+ '05': 5,
+ '06': 6,
+ '07': 7,
+ '08': 8,
+ '09': 9,
+ '10': 10,
+ '11': 11,
+};
+
+export const parseContributions = (commits = {}) => {
+ const data = [];
+ const months = Object.keys(commits).sort(
+ (a, b) => desiredContributionsMonthsOrder[a] - desiredContributionsMonthsOrder[b]
+ );
+
+ for (const month of months) {
+ const commitsForMonth = commits[month];
+ const days = Object.keys(commitsForMonth).sort((a, b) => a - b);
+
+ for (const day of days) {
+ const commitsForDay = commitsForMonth[day];
+ data.push({
+ date: new Date(month === '12' ? 2023 : 2024, Number(month) - 1, Number(day)),
+ contributions: commitsForDay,
+ });
+ }
+ }
+
+ return data;
+};
+
+export const parseContributors = (contributors = []) =>
+ contributors.map(c => ({
+ avatar: c.avatar,
+ name: c.name || c.id,
+ username: !!c.name ? c.id : null,
+ }));
diff --git a/src/components/dashboard-page/Engineering/index.js b/src/components/dashboard-page/Engineering/index.js
new file mode 100644
index 000000000..988822998
--- /dev/null
+++ b/src/components/dashboard-page/Engineering/index.js
@@ -0,0 +1,75 @@
+import React from 'react';
+import { object, bool } from 'prop-types';
+
+import SectionHeader from '../SectionHeader';
+import GithubStats from './GithubStats';
+import StatsWidget from '../StatsWidget';
+import ChartWidget from './ChartWidget';
+import WidgetHeading from '../WidgetHeading';
+import Contributors from './Contributors';
+import { StatsBlockSkeleton, ChartBlockSkeleton, ContributorsBlockSkeleton } from './Skeletons';
+
+import { parseGithubStats, parseFollowersCount, parseContributions, parseContributors } from './data';
+
+import './style.scss';
+
+const propTypes = {
+ data: object,
+ loading: bool,
+};
+
+const Engineering = ({ data, loading }) => {
+ const parsedGithubStats = parseGithubStats(data);
+
+ const parsedContributions = parseContributions(data?.commits);
+
+ const parsedContributors = parseContributors(data?.contributors);
+
+ return (
+
+
+
+
+ {loading ? (
+
+ ) : (
+
+
+
+
+ {parsedGithubStats.map((stats, index) => (
+
+ ))}
+
+
+
+
+ )}
+
+ {loading || !parsedContributions.length ? (
+
+ ) : (
+
+ )}
+
+ {loading || !parsedContributors.length ? (
+
+ ) : (
+
+
+
+
+ )}
+
+
+ );
+};
+
+Engineering.propTypes = propTypes;
+
+export default Engineering;
diff --git a/src/components/dashboard-page/Engineering/style.scss b/src/components/dashboard-page/Engineering/style.scss
new file mode 100644
index 000000000..2dd5e3a15
--- /dev/null
+++ b/src/components/dashboard-page/Engineering/style.scss
@@ -0,0 +1,91 @@
+@import '../../../styles/main';
+
+.dashboard-engineering {
+ @include dashboard-section;
+
+ &__container {
+ @include dashboard-container;
+ }
+
+ &__stats-wrapper {
+ display: grid;
+ gap: 16px;
+ }
+
+ &__github-stats-widget {
+ @include dashboard-widget;
+ }
+
+ &__github-stats {
+ display: grid;
+ gap: 40px;
+ }
+
+ & .dashboard-stats-widget__text {
+ @include h600;
+ }
+
+ & .dashboard-stats-widget__helper-text {
+ @include t400;
+ }
+
+ &__contributors {
+ margin-top: 16px;
+ }
+
+ &__contributors-heading {
+ margin-bottom: 8px;
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__github-stats {
+ gap: 16px;
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ & .dashboard-stats-widget__text {
+ @include h700;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ &__stats-wrapper {
+ gap: 24px;
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ &__github-stats-widget {
+ grid-column: span 3;
+ }
+
+ & .dashboard-stats-widget {
+ display: flex;
+ flex-direction: column;
+ }
+
+ & .dashboard-stats-widget__text-wrapper {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ justify-content: center;
+ }
+
+ & .dashboard-stats-widget__helper-text {
+ margin-left: -6px;
+ }
+
+ & .dashboard-stats-widget__text {
+ @include h800;
+ }
+
+ &__contributors {
+ margin-top: 24px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-lg} {
+ &__github-stats {
+ grid-template-columns: repeat(3, 1fr);
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Header/data.js b/src/components/dashboard-page/Header/data.js
new file mode 100644
index 000000000..3e31dec72
--- /dev/null
+++ b/src/components/dashboard-page/Header/data.js
@@ -0,0 +1,12 @@
+export const anchors = [
+ 'Introduction',
+ 'Token',
+ 'Backers',
+ 'History',
+ 'Traction',
+ 'Engineering',
+ 'Community',
+ 'Team',
+ 'Comparison',
+ 'Roadmap',
+];
diff --git a/src/components/dashboard-page/Header/index.js b/src/components/dashboard-page/Header/index.js
new file mode 100644
index 000000000..4e6e9d8e4
--- /dev/null
+++ b/src/components/dashboard-page/Header/index.js
@@ -0,0 +1,88 @@
+import React, { useContext } from 'react';
+import { Link } from 'gatsby';
+import { useTransition, animated } from 'react-spring';
+import cn from 'classnames';
+import { string, func, bool } from 'prop-types';
+
+import { ScrollContext } from '../../_enhancers/ScrollContext';
+import Feature from '../../Feature';
+
+import { ReactComponent as ArrowBack } from '../../../assets/svg/dashboard/arrow-back.svg';
+import { ReactComponent as GleevLogo } from '../../../assets/svg/dashboard/gleev-logo.svg';
+import { ReactComponent as ChatButtonIcon } from '../../../assets/svg/dashboard/chat-button-icon.svg';
+
+import { anchors } from './data';
+import './style.scss';
+
+const propTypes = {
+ activeAnchor: string.isRequired,
+ onAnchorClick: func.isRequired,
+ historyHidden: bool,
+};
+
+const DashboardHeader = ({ activeAnchor, onAnchorClick, historyHidden }) => {
+ const scrollContext = useContext(ScrollContext);
+ const { isScrollUp } = scrollContext;
+
+ const transitions = useTransition(isScrollUp, null, {
+ from: { opacity: 0 },
+ enter: { opacity: 1 },
+ leave: { opacity: 0 },
+ });
+
+ const visibleAnchors = anchors.filter(anchor => (historyHidden ? anchor !== 'History' : true));
+
+ const onButtonChatClick = () => {};
+
+ return (
+
+
+
+
+
+
+ Back
+ Back to Joystream.org
+
+
+
+
+
+
+
+ Chat
+ Chat with Joystream team
+
+
+
+
+
+
+ {transitions.map(
+ ({ item, key, props }) =>
+ item && (
+
+
+
+ {visibleAnchors.map(anchor => (
+
+ onAnchorClick(anchor)}
+ >
+ {anchor}
+
+
+ ))}
+
+
+
+ )
+ )}
+
+ );
+};
+
+DashboardHeader.propTypes = propTypes;
+
+export default DashboardHeader;
diff --git a/src/components/dashboard-page/Header/style.scss b/src/components/dashboard-page/Header/style.scss
new file mode 100644
index 000000000..2bf465b7e
--- /dev/null
+++ b/src/components/dashboard-page/Header/style.scss
@@ -0,0 +1,183 @@
+@import '../../../styles/_main.scss';
+
+.dashboard-header {
+ position: sticky;
+ top: 0;
+ z-index: $dashboard-header-z-index;
+
+ &__wrapper {
+ height: $dashboard-header-height;
+ background-color: $dashboard-header-background-color;
+ border-bottom: 1px solid $dashboard-header-border-bottom-color;
+ }
+
+ &__container {
+ padding: 12px 16px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ &__button {
+ display: flex;
+ align-items: center;
+
+ @include t200;
+
+ color: $dashboard-header-buttons-color;
+ cursor: pointer;
+ @include dashboard-buttons-states;
+ }
+
+ &__button-back {
+ padding: 12px;
+ opacity: 0.5;
+ border-radius: 20px;
+
+ &_short-text {
+ margin-left: 8px;
+ display: none;
+ }
+
+ &_text {
+ margin-left: 8px;
+ display: none;
+ }
+ }
+
+ &__button-chat {
+ padding: 8px 16px;
+ background-color: $dashboard-header-chat-button-background-color;
+
+ border: 1px solid $dashboard-header-buttons-border-color;
+ border-radius: 2px;
+
+ &_text {
+ display: none;
+ }
+
+ &_icon {
+ margin-left: 8px;
+ display: none;
+ }
+ }
+
+ &__nav-wrapper {
+ background-color: $dashboard-header-background-color;
+ }
+
+ &__nav {
+ height: $dashboard-header-nav-height;
+ padding: 8px 16px;
+ background-color: $dashboard-navbar-background-color;
+ border-top: 1px solid $dashboard-header-border-bottom-color;
+ border-bottom: 1px solid $dashboard-base-border-color;
+ overflow-x: auto;
+
+ scrollbar-width: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ }
+
+ &__nav-list {
+ padding: 0;
+ list-style: none;
+
+ display: flex;
+ }
+
+ &__nav-list-item {
+ margin: 0;
+ padding: 0;
+
+ &:not(:last-of-type) {
+ margin-right: 16px;
+ }
+ }
+
+ &__nav-button {
+ height: 32px;
+ padding: 8px 10px;
+
+ @include t100;
+
+ background-color: transparent;
+ color: $dashboard-header-navbar-buttons-color;
+ border: 1px solid $dashboard-header-navbar-buttons-border-color;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: all $dashboard-transition-duration $dashboard-transition-timing;
+
+ &:hover,
+ &:focus {
+ color: $dashboard-header-navbar-buttons-states-color;
+ border-color: $dashboard-header-navbar-buttons-states-border-color;
+ }
+
+ &.active {
+ background-color: $dahboard-header-navbar-buttons-states-background-color;
+ color: $dashboard-header-navbar-buttons-states-color;
+ border-color: $dashboard-header-navbar-buttons-states-border-color;
+ }
+ }
+
+ @media #{$screen-min-dashboard-xs} {
+ &__button-back {
+ padding: 10px 16px;
+ border-radius: 2px;
+ &_short-text {
+ display: block;
+ }
+ }
+
+ &__button-chat {
+ &_icon {
+ display: block;
+ }
+ }
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__button-back {
+ opacity: 1;
+
+ &_short-text {
+ display: none;
+ }
+
+ &_text {
+ display: block;
+ }
+ }
+
+ &__button-chat {
+ &_short-text {
+ display: none;
+ }
+
+ &_text {
+ display: block;
+ }
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ &__container {
+ padding-inline: 32px;
+ }
+
+ &__nav {
+ padding-inline: 32px;
+ display: flex;
+ justify-content: center;
+ }
+ }
+
+ @media #{$screen-min-dashboard-xl} {
+ &__container {
+ margin: 0 auto;
+ width: 1920px;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Hero/index.js b/src/components/dashboard-page/Hero/index.js
new file mode 100644
index 000000000..82209224f
--- /dev/null
+++ b/src/components/dashboard-page/Hero/index.js
@@ -0,0 +1,103 @@
+import React, { useRef, useState, useEffect } from 'react';
+import Player from '@vimeo/player';
+import ReactModal from 'react-modal';
+import cn from 'classnames';
+import { string, bool } from 'prop-types';
+
+import DashboardHeroVideoOverlay from '../../../assets/images/dashboard/dashboard-hero-video-overlay.png';
+import { ReactComponent as DashboardPlayVideoIcon } from '../../../assets/svg/dashboard/dashboard-play-video-icon.svg';
+
+import './style.scss';
+
+ReactModal.setAppElement('#___gatsby');
+
+const propTypes = {
+ introVideoSrc: string,
+ embedded: bool,
+};
+
+const defaultProps = {
+ introVideoSrc: 'https://player.vimeo.com/video/888678724?h=1e85bf9838',
+};
+
+const DashboardHero = ({ introVideoSrc, embedded }) => {
+ const vimeoVideoIframeRef = useRef(null);
+
+ const [videoPlayerOpen, setVideoPlayerOpen] = useState(false);
+ const onVideoPlayerClose = () => setVideoPlayerOpen(false);
+ const onVideoWrapperClick = () => setVideoPlayerOpen(true);
+ const onVideoWrapperKeyDown = event => {
+ if (event.key !== 'Enter') {
+ return;
+ }
+ onVideoWrapperClick();
+ };
+
+ useEffect(() => {
+ if (videoPlayerOpen && vimeoVideoIframeRef.current) {
+ const player = new Player(vimeoVideoIframeRef.current);
+ player.play();
+ player.disableTextTrack();
+ }
+ }, [videoPlayerOpen]);
+
+ return (
+ <>
+
+
+
+ {embedded &&
DASHBOARD }
+
+ Everything you ever wanted to know in one place
+
+
+ A dynamic and comprehensive dashboard with an up to date view on Joystream.
+
+
+ {!embedded && (
+
+
+
+
+
+
+ )}
+
+
+
+
+
+ >
+ );
+};
+
+DashboardHero.propTypes = propTypes;
+DashboardHero.defaultProps = defaultProps;
+
+export default DashboardHero;
diff --git a/src/components/dashboard-page/Hero/style.scss b/src/components/dashboard-page/Hero/style.scss
new file mode 100644
index 000000000..53a378e10
--- /dev/null
+++ b/src/components/dashboard-page/Hero/style.scss
@@ -0,0 +1,184 @@
+@import '../../../styles/main';
+
+.dashboard-hero {
+ padding-block: 56px 40px;
+
+ &.scroll-offset {
+ margin-top: -($dashboard-header-sum-of-heights);
+ padding-top: calc(56px + $dashboard-header-sum-of-heights);
+ }
+
+ &__container {
+ @include dashboard-container;
+ display: grid;
+ gap: 24px;
+ }
+
+ &__embedded-section-title {
+ margin-bottom: 4px;
+ @include h400;
+ color: #6c6cff;
+ font-feature-settings: $dashboard-font-feature-settings;
+ text-align: center;
+ text-transform: uppercase;
+ }
+
+ &__title {
+ @include h600;
+ font-feature-settings: $dashboard-font-feature-settings;
+ margin-bottom: 8px;
+ color: $dashboard-base-white-text-color;
+ text-align: center;
+ }
+
+ &__description {
+ @include t300;
+ font-feature-settings: $dashboard-font-feature-settings;
+ color: $dashboard-base-gray-text-color;
+ text-align: center;
+ }
+
+ &__video-wrapper {
+ position: relative;
+ cursor: pointer;
+ filter: blur(0.19854368269443512px);
+
+ &:hover > .dashboard-hero__button-play-video {
+ background-color: $dashboard-hero-play-video-button-hover-background-color;
+ }
+ }
+
+ &__video-overlay {
+ width: 100%;
+ border-radius: 12px;
+ }
+
+ &__button-play-video {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 64px;
+ height: 40px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: $dashboard-hero-play-video-button-background-color;
+ border-radius: 4px;
+ cursor: pointer;
+ // backdrop-filter: blur(4px);
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ filter: blur(4px);
+ }
+ }
+
+ &__video-player-modal {
+ position: absolute;
+ inset: 40px;
+ border: 1px dashed $dashboard-hero-video-player-modal-border-color;
+ background-color: $dashboard-hero-video-player-modal-background-color;
+ border-radius: 4px;
+ outline: none;
+ padding: 20px;
+ }
+
+ &__video-player-modal-overlay {
+ position: fixed;
+ inset: 0;
+ background-color: $dashboard-modals-overlay-color;
+ z-index: calc($dashboard-header-z-index + 1);
+ }
+
+ @media #{$screen-min-dashboard-xs} {
+ & {
+ padding-bottom: 48px;
+ }
+
+ &__title {
+ @include h700;
+ margin-bottom: 16px;
+ }
+
+ &__description {
+ @include t400;
+ }
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ & {
+ padding-block: 64px;
+ &.scroll-offset {
+ margin-top: -($dashboard-header-sum-of-heights);
+ padding-top: calc(64px + $dashboard-header-sum-of-heights);
+ }
+ }
+
+ &__container {
+ padding-inline: 80px;
+ }
+
+ &__title {
+ margin-bottom: 24px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ padding-block: 96px 80px;
+ &.scroll-offset {
+ margin-top: -($dashboard-header-sum-of-heights);
+ padding-top: calc(96px + $dashboard-header-sum-of-heights);
+ }
+ }
+
+ &__container {
+ padding-inline: 32px;
+ grid-template-columns: repeat(2, 1fr);
+
+ &.embedded {
+ grid-template-columns: 1fr;
+ }
+ }
+
+ &__text-wrapper.embedded {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ &__title {
+ @include h800;
+ text-align: start;
+
+ &.embedded {
+ max-width: 909px;
+ text-align: center;
+ }
+ }
+
+ &__description {
+ text-align: start;
+
+ &.embedded {
+ text-align: center;
+ }
+ }
+ }
+
+ @media #{$screen-min-dashboard-lg} {
+ &__container {
+ gap: 140px;
+ grid-template-columns: 40.5% 49%;
+ }
+ &__title {
+ @include h900;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/History/Markdown/index.js b/src/components/dashboard-page/History/Markdown/index.js
new file mode 100644
index 000000000..1f41ce2ed
--- /dev/null
+++ b/src/components/dashboard-page/History/Markdown/index.js
@@ -0,0 +1,34 @@
+/* eslint-disable jsx-a11y/heading-has-content */
+/* eslint-disable jsx-a11y/anchor-has-content */
+
+import React from 'react';
+import { useRemarkSync } from 'react-remark';
+import { string } from 'prop-types';
+
+import './style.scss';
+
+const propTypes = {
+ content: string.isRequired,
+};
+
+const Markdown = ({ content }) => {
+ const reactContent = useRemarkSync(content, {
+ rehypeReactOptions: {
+ components: {
+ p: props =>
,
+ h1: props =>
,
+ h2: props =>
,
+ h3: props =>
,
+ a: props =>
,
+ ol: props =>
,
+ li: props =>
,
+ },
+ },
+ });
+
+ return <>{reactContent}>;
+};
+
+Markdown.propTypes = propTypes;
+
+export default Markdown;
diff --git a/src/components/dashboard-page/History/Markdown/style.scss b/src/components/dashboard-page/History/Markdown/style.scss
new file mode 100644
index 000000000..ef2cf1519
--- /dev/null
+++ b/src/components/dashboard-page/History/Markdown/style.scss
@@ -0,0 +1,54 @@
+@import '../../../../styles/main';
+
+.paragraph {
+ margin-bottom: 16px;
+ @include t300;
+ color: $dashboard-base-gray-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+
+ & > strong {
+ font-weight: 700;
+ }
+
+ & > em > strong {
+ font-weight: 400;
+ font-style: normal;
+ text-decoration: line-through;
+ }
+}
+
+.heading {
+ margin-block: 16px 8px;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+}
+
+.heading1 {
+ margin-top: 0px;
+ @include h400;
+}
+
+.heading2 {
+ @include h300;
+}
+
+.heading3 {
+ @include h100;
+ text-transform: uppercase;
+}
+
+.anchor {
+ @include t300;
+ font-feature-settings: $dashboard-font-feature-settings;
+ color: $dashboard-markdown-anchor-color;
+ text-decoration: underline;
+}
+
+.ordered-list {
+ list-style: auto;
+}
+
+.list-item {
+ @extend .paragraph;
+ margin-bottom: 4px;
+}
diff --git a/src/components/dashboard-page/History/data.js b/src/components/dashboard-page/History/data.js
new file mode 100644
index 000000000..606d2e588
--- /dev/null
+++ b/src/components/dashboard-page/History/data.js
@@ -0,0 +1,51 @@
+/* eslint-disable max-len */
+import historyStageJun2019Img from '../../../assets/images/dashboard/history-stage-jun-2019.jpg';
+import historyStageNov2019Img from '../../../assets/images/dashboard/history-stage-nov-2019.webp';
+import historyStageFeb2023Img from '../../../assets/images/dashboard/history-stage-feb-2023.jpeg';
+import historyStageJul2023Img from '../../../assets/images/dashboard/history-stage-jul-2023.jpg';
+
+// React Remark does not support line-through decoration out of the box. To override the limitation use ***text***
+
+const longDescrSample = `
+While editing the text user should have a lot of options which basic markdown syntax allows for. *Text can be italic*, **or it can be bolded**, or if something is completed ***it can be striked through***.
+
+## There are H2 headings as well 🫡
+
+User can use links by enclosing the link text in brackets (e.g., [Duck Duck Go]) and then follow it immediately with the URL in parentheses (e.g., (https://duckduckgo.com)).
+
+The link should be styled as following: [Hey I'm a link click me](https://www.joystream.org/)
+
+### There are more headings (this one is H3)
+Lets look at the **ordered list** of all headings that user can use:
+
+1. H1 by typing *‘#’* or clicking H1 action button.
+2. H2 by typing *‘##’* or clicking H2 action button.
+3. H3 by typing *‘###’* or clicking H3 action button.
+`;
+
+export const historyStages = [
+ {
+ img: historyStageJun2019Img,
+ date: 'Jun 2019',
+ shortDescr: 'The idea for the Joystream product',
+ longDescr: longDescrSample,
+ },
+ {
+ img: historyStageNov2019Img,
+ date: 'Nov 2019',
+ shortDescr: 'First POC of the product is created',
+ longDescr: '',
+ },
+ {
+ img: historyStageFeb2023Img,
+ date: 'Feb 2023',
+ shortDescr: 'JSGenesis is formed',
+ longDescr: 'While editing the text user should have a lot of options which basic markdown syntax allows for.',
+ },
+ {
+ img: historyStageJul2023Img,
+ date: 'Jul 2023',
+ shortDescr: 'Joystream is officialy in Testnet and released to the public for first test rounds',
+ longDescr: 'While editing the text user should have a lot of options which basic markdown syntax allows for.',
+ },
+];
diff --git a/src/components/dashboard-page/History/index.js b/src/components/dashboard-page/History/index.js
new file mode 100644
index 000000000..d0f1317b7
--- /dev/null
+++ b/src/components/dashboard-page/History/index.js
@@ -0,0 +1,176 @@
+import React, { useState } from 'react';
+import ReactModal from 'react-modal';
+import cn from 'classnames';
+import { string, func, number, arrayOf, shape } from 'prop-types';
+
+import SectionHeader from '../SectionHeader';
+import Carousel from '../Carousel';
+import Markdown from './Markdown';
+import ArrowButton from '../ArrowButton';
+
+import { ReactComponent as WhiteCrossIcon } from '../../../assets/svg/dashboard/white-cross.svg';
+import { ReactComponent as PrevStoryPointer } from '../../../assets/svg/dashboard/prev-story-pointer.svg';
+import { ReactComponent as NextStoryPointer } from '../../../assets/svg/dashboard/next-story-pointer.svg';
+
+import { historyStages } from './data';
+
+import './style.scss';
+
+ReactModal.setAppElement('#___gatsby');
+
+const dashboardHistoryStagePropTypes = {
+ img: string.isRequired,
+ date: string.isRequired,
+ shortDescr: string.isRequired,
+ longDescr: string,
+ onClick: func,
+};
+
+const DashboardHistoryStage = ({ img, date, shortDescr, longDescr, onClick }) => {
+ return (
+
{}}
+ onClick={onClick}
+ >
+
+
+
+
+
+
+
{date}
+
{shortDescr}
+
+ {!!longDescr &&
}
+
+
+ );
+};
+
+DashboardHistoryStage.propTypes = dashboardHistoryStagePropTypes;
+
+const dashboardHistoryModalContentPropTypes = {
+ onModalClose: func.isRequired,
+ stages: arrayOf(
+ shape({
+ img: string.isRequired,
+ date: string.isRequired,
+ shortDescr: string.isRequired,
+ longDescr: string,
+ })
+ ).isRequired,
+ stageIdx: number.isRequired,
+};
+
+const DashboardHistoryModalContent = ({ onModalClose, stages, stageIdx }) => {
+ const [currentStageIdx, setCurrentStageIndex] = useState(stageIdx);
+ const showPrevStage = () => setCurrentStageIndex(prevStageIdx => prevStageIdx - 1);
+ const showNextStage = () => setCurrentStageIndex(prevStageIdx => prevStageIdx + 1);
+
+ const currentStage = stages[currentStageIdx];
+ const stagesQuantity = stages.length;
+ const isShowPrevStageBtnDisabled = currentStageIdx === 0;
+ const isShowNextStageBtnDisabled = currentStageIdx === stagesQuantity - 1;
+
+ return (
+
+
+
+
+
+
+
+ {currentStage.date}
+
+ •
+
+ {currentStage.shortDescr}
+
+
+
+
+
+
+ Read previous
+
+
+ {currentStageIdx + 1}
+ {` / ${stagesQuantity}`}
+
+
+ Read next
+
+
+
+
+ );
+};
+
+DashboardHistoryModalContent.propTypes = dashboardHistoryModalContentPropTypes;
+
+const DashboardHistory = () => {
+ const [modalOpen, setModalOpen] = useState(false);
+ const [interactiveStageIdx, setInteractiveStageIdx] = useState(0);
+
+ const interactiveStages = historyStages.filter(historyStage => !!historyStage.longDescr);
+
+ const onModalOpen = historyStage => {
+ if (!historyStage.longDescr) {
+ return;
+ }
+
+ const stageIdx = interactiveStages.findIndex(stage => historyStage.date === stage.date);
+ setInteractiveStageIdx(stageIdx);
+
+ return setModalOpen(true);
+ };
+
+ const onModalClose = () => setModalOpen(false);
+
+ return (
+ <>
+
+
+
+
+ {historyStages.map((historyStage, index) => {
+ return onModalOpen(historyStage)} />;
+ })}
+
+
+
+
+
+
+ >
+ );
+};
+
+export default DashboardHistory;
diff --git a/src/components/dashboard-page/History/style.scss b/src/components/dashboard-page/History/style.scss
new file mode 100644
index 000000000..578ed9c91
--- /dev/null
+++ b/src/components/dashboard-page/History/style.scss
@@ -0,0 +1,238 @@
+@import '../../../styles/main';
+
+.dashboard-history {
+ @include dashboard-section;
+
+ &__container {
+ @include dashboard-container;
+ }
+
+ &__stage {
+ width: 272px;
+ height: 391px;
+ display: flex;
+ flex-direction: column;
+ background-color: $dashboard-widget-base-background-color;
+ border: 1px solid $dashboard-widget-base-background-color;
+ border-radius: 8px;
+
+ &:not(:last-of-type) {
+ margin-right: 16px;
+ }
+
+ &.card-interactive {
+ cursor: pointer;
+
+ &:hover {
+ border-color: $dashboard-base-border-color;
+
+ & .dashboard-history__stage-img-overlay {
+ opacity: 0;
+ visibility: hidden;
+ }
+ }
+ }
+ }
+
+ &__stage-img-wrapper {
+ position: relative;
+ }
+
+ &__stage-img-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 99%;
+ background-color: #6363fb9c;
+ border-top-left-radius: 8px;
+ border-top-right-radius: 8px;
+ opacity: 1;
+ visibility: visible;
+ transition: all $dashboard-transition-duration $dashboard-transition-timing;
+ }
+
+ &__stage-img {
+ border-top-left-radius: 8px;
+ border-top-right-radius: 8px;
+ }
+
+ &__stage-descr-wrapper {
+ flex-grow: 1;
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ }
+
+ &__stage-date {
+ margin-bottom: 8px;
+ @include h500;
+ color: $dashboard-base-white-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ text-overflow: ellipsis;
+ }
+
+ &__stage-descr {
+ @include t300;
+ color: $dashboard-base-gray-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__button-read-more {
+ align-self: flex-start;
+ }
+
+ &__modal {
+ position: absolute;
+ inset: 80px 16px;
+ max-width: 800px;
+ margin-inline: auto;
+ background-color: $dashboard-history-modal-background-color;
+ outline: none;
+ border-radius: 8px;
+ }
+
+ &__modal-overlay {
+ position: fixed;
+ inset: 0;
+ background-color: $dashboard-modals-overlay-color;
+ z-index: calc($dashboard-header-z-index + 1);
+ }
+
+ &__modal-content {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__modal-img-wrapper {
+ height: 200px;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ }
+
+ &__modal-long-descr-wrapper {
+ padding: 24px;
+ flex: 1 1 auto;
+ overflow-y: auto;
+ scrollbar-color: $dashboard-custom-scrollbar-track-background $dashboard-custom-scrollbar-thumb-background;
+
+ &::-webkit-scrollbar {
+ width: 8px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: $dashboard-custom-scrollbar-track-background;
+ border-radius: 20px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: $dashboard-custom-scrollbar-thumb-background;
+ border-radius: 20px;
+ }
+
+ &::-moz-scrollbar {
+ width: 8px;
+ border-radius: 20px;
+ }
+ }
+
+ &__modal-long-descr-title {
+ margin-bottom: 16px;
+ @include h400;
+ font-weight: 600;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+
+ &_regular {
+ font-weight: 400;
+ }
+ }
+
+ &__button {
+ background-color: $dashboard-history-modal-buttons-background-color;
+ border: 1px solid $dashboard-base-borders-color;
+ border-radius: 2px;
+ cursor: pointer;
+ transition: all $dashboard-transition-timing $dashboard-transition-duration;
+ &:hover:enabled,
+ &:focus:enabled {
+ background-color: $dashboard-buttons-base-hover-background-color;
+ }
+
+ &:active:enabled {
+ background-color: $dashboard-buttons-base-pressed-background-color;
+ transition-duration: 0ms;
+ }
+ }
+
+ &__button-close-modal {
+ position: absolute;
+ top: 24px;
+ right: 24px;
+ width: 48px;
+ height: 48px;
+ backdrop-filter: blur(8px);
+ }
+
+ &__modal-actions {
+ padding: 24px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ box-shadow: 0px 1px 0px 0px rgba(187, 217, 246, 0.13) inset;
+ }
+
+ &__button-modal-action {
+ width: 157px;
+ height: 40px;
+ padding: 10px 16px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+ @include t200;
+ font-weight: 600;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+
+ &:disabled {
+ border-color: $dashboard-history-modal-buttons-background-color;
+ opacity: 0.5;
+ cursor: auto;
+ }
+ }
+
+ &__stages-count {
+ @include t300;
+ color: $dashboard-base-gray-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ &_current {
+ font-weight: 600;
+ color: $dashboard-content-base-text-color;
+ }
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__stage {
+ width: 304px;
+ height: 407px;
+
+ &:not(:last-of-type) {
+ margin-right: 24px;
+ }
+ }
+
+ &__stage-descr-wrapper {
+ padding: 24px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-lg} {
+ &__stage {
+ width: 326px;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/JoyCarousel/data.js b/src/components/dashboard-page/JoyCarousel/data.js
new file mode 100644
index 000000000..1571a7957
--- /dev/null
+++ b/src/components/dashboard-page/JoyCarousel/data.js
@@ -0,0 +1,61 @@
+import mexcLogo from '../../../assets/images/dashboard/mexc-logo.png';
+import bitgetLogo from '../../../assets/images/dashboard/bitget-logo.png';
+
+export const joyCarouselItems = [
+ {
+ platformLogo: mexcLogo,
+ platformName: 'MEXC',
+ requiresKyc: true,
+ acceptsUsersFromPoland: true,
+ exchangePair: ['JOY', 'USDT'],
+ },
+ {
+ platformLogo: bitgetLogo,
+ platformName: 'Bitget',
+ requiresKyc: false,
+ acceptsUsersFromPoland: false,
+ exchangePair: ['JOYSTREAM', 'USDT'],
+ },
+ {
+ platformLogo: mexcLogo,
+ platformName: 'MEXC',
+ requiresKyc: false,
+ acceptsUsersFromPoland: true,
+ exchangePair: ['JOY', 'ETH'],
+ },
+ {
+ platformLogo: bitgetLogo,
+ platformName: 'Bitget',
+ requiresKyc: true,
+ acceptsUsersFromPoland: false,
+ exchangePair: ['JOY', 'BTC'],
+ },
+ {
+ platformLogo: mexcLogo,
+ platformName: 'MEXC',
+ requiresKyc: true,
+ acceptsUsersFromPoland: true,
+ exchangePair: ['JOY', 'USDT'],
+ },
+ {
+ platformLogo: bitgetLogo,
+ platformName: 'Bitget',
+ requiresKyc: false,
+ acceptsUsersFromPoland: false,
+ exchangePair: ['JOYSTREAM', 'USDT'],
+ },
+ {
+ platformLogo: mexcLogo,
+ platformName: 'MEXC',
+ requiresKyc: false,
+ acceptsUsersFromPoland: true,
+ exchangePair: ['JOY', 'ETH'],
+ },
+ {
+ platformLogo: bitgetLogo,
+ platformName: 'Bitget',
+ requiresKyc: true,
+ acceptsUsersFromPoland: false,
+ exchangePair: ['JOY', 'BTC'],
+ },
+];
diff --git a/src/components/dashboard-page/JoyCarousel/index.js b/src/components/dashboard-page/JoyCarousel/index.js
new file mode 100644
index 000000000..0929b2d1b
--- /dev/null
+++ b/src/components/dashboard-page/JoyCarousel/index.js
@@ -0,0 +1,76 @@
+import React from 'react';
+import { Slide } from 'pure-react-carousel';
+import { string, bool, arrayOf } from 'prop-types';
+
+import WidgetHeading from '../WidgetHeading';
+import Carousel from '../Carousel';
+
+import { ReactComponent as ExclamationMarkIcon } from '../../../assets/svg/dashboard/exclamation-mark-icon.svg';
+import { ReactComponent as CheckAcceptedIcon } from '../../../assets/svg/dashboard/check-accepted-icon.svg';
+import { ReactComponent as CancelRejectedIcon } from '../../../assets/svg/dashboard/cancel-rejected-icon.svg';
+import { ReactComponent as ExchangerIcon } from '../../../assets/svg/dashboard/exchanger-icon.svg';
+
+import { joyCarouselItems } from './data';
+
+import './style.scss';
+
+const joyCarouselItemPropTypes = {
+ platformLogo: string.isRequired,
+ platformName: string.isRequired,
+ requiresKyc: bool.isRequired,
+ acceptsUsersFromPoland: bool.isRequired,
+ exchangePair: arrayOf(string),
+};
+
+const JoyCarouselItem = ({ platformLogo, platformName, requiresKyc, acceptsUsersFromPoland, exchangePair }) => {
+ return (
+
+
+
{platformName}
+
+
+
+
+
{requiresKyc ? 'Requires KYC' : 'Doesn’t require KYC'}
+
+
+
+
+ {acceptsUsersFromPoland ?
:
}
+
+ {acceptsUsersFromPoland ? 'Accepting users from: Poland' : 'Not accepting users from: Poland'}
+
+
+
+
+
+
+
{`Pair: ${exchangePair[0]} <-> ${exchangePair[1]}`}
+
+
+
+
How to buy?
+
+ );
+};
+
+const DashboardJoyCarousel = () => {
+ return (
+
+
+
+ {joyCarouselItems.map((joyCarouselItem, index) => {
+ return (
+
+
+
+ );
+ })}
+
+
+ );
+};
+
+JoyCarouselItem.propTypes = joyCarouselItemPropTypes;
+
+export default DashboardJoyCarousel;
diff --git a/src/components/dashboard-page/JoyCarousel/style.scss b/src/components/dashboard-page/JoyCarousel/style.scss
new file mode 100644
index 000000000..f7ffacac0
--- /dev/null
+++ b/src/components/dashboard-page/JoyCarousel/style.scss
@@ -0,0 +1,116 @@
+@import '../../../styles/main';
+
+.dashboard-joy-carousel {
+ margin-top: 16px;
+
+ &__heading {
+ margin-bottom: 8px;
+ }
+
+ &__item {
+ @include dashboard-widget;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ border: 1px solid $dashboard-base-border-color;
+ }
+
+ &__item-logo {
+ display: block;
+ margin-bottom: 16px;
+ width: 88px;
+ border: 1px solid $dashboard-base-borders-color;
+ border-radius: 8px;
+ }
+
+ &__item-heading {
+ margin-bottom: 16px;
+ @include h500;
+ font-weight: 600;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__tags {
+ margin-bottom: 32px;
+ padding: 0;
+ list-style: none;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ &__tags-item {
+ margin: 0;
+ padding: 0;
+
+ &:not(:last-of-type) {
+ margin-bottom: 8px;
+ }
+ }
+
+ &__tag {
+ padding: 8px;
+ width: fit-content;
+ display: flex;
+ align-items: center;
+ background-color: $dashboard-widget-tags-background-color;
+ border-radius: 4px;
+ }
+
+ &__tag-text {
+ margin-left: 6px;
+ @include t100;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__button-how-to-buy {
+ padding: 12px 20px;
+ @include t300;
+ font-weight: 600;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ background-color: $dashboard-widget-accent-background-color;
+ border: 1px solid $dashboard-base-borders-color;
+ border-radius: 2px;
+ cursor: pointer;
+ transition: all $dashboard-transition-duration $dashboard-transition-timing;
+
+ &:hover,
+ &:focus {
+ background-color: $dashboard-widget-accent-hover-background-color;
+ border-color: $dashboard-base-border-color;
+ }
+
+ &:active {
+ background-color: $dashboard-widget-accent-active-background-color;
+ transition-duration: 0ms;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ margin-top: 24px;
+ }
+
+ &__item-heading {
+ @include h600;
+ }
+ }
+
+ @media #{$screen-min-dashboard-xl} {
+ &__tags {
+ flex-direction: row;
+ }
+
+ &__tags-item {
+ &:not(:last-of-type) {
+ margin-bottom: 0;
+ margin-right: 8px;
+ }
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Roadmap/index.js b/src/components/dashboard-page/Roadmap/index.js
new file mode 100644
index 000000000..ce3a0c81b
--- /dev/null
+++ b/src/components/dashboard-page/Roadmap/index.js
@@ -0,0 +1,99 @@
+import React, { useMemo } from 'react';
+import { Link } from 'gatsby';
+import { string, arrayOf, shape, number } from 'prop-types';
+
+import SectionHeader from '../SectionHeader';
+
+import { ReactComponent as NewTabIcon } from '../../../assets/svg/dashboard/new-tab.svg';
+
+import './style.scss';
+
+import roadmapData, { iconMap } from '../../../data/quarters';
+import { parseQuarters } from '../../roadmap-page/Quarters';
+
+const quarterPropTypes = {
+ year: string.isRequired,
+ id: string.isRequired,
+ deliveryMilestones: arrayOf(
+ shape({
+ icon: string.isRequired,
+ title: string.isRequired,
+ Content: string.isRequired,
+ generalIndex: number.isRequired,
+ })
+ ).isRequired,
+ roadmapDataFilename: string,
+};
+
+const Quarter = ({ year, id, deliveryMilestones, roadmapDataFilename }) => {
+ return (
+
+
+
+
{`${year} ${id}`}
+
+
+ Open roadmap
+
+
+
+
+
+ {deliveryMilestones.map((deliveryMilestone, index) => {
+ return (
+
+
+
+
+
+
{`${index + 1}. ${
+ deliveryMilestone.title
+ }`}
+
{deliveryMilestone.Content}
+
+
+ );
+ })}
+
+ );
+};
+
+Quarter.propTypes = quarterPropTypes;
+
+const Roadmap = () => {
+ const roadmapDataFilename = useMemo(() => {
+ const newestRoadmapData = roadmapData.find(({ isNewest }) => isNewest);
+ return newestRoadmapData?.name;
+ }, []);
+
+ const quarters = useMemo(() => {
+ const newestRoadmapData = roadmapData.find(({ isNewest }) => isNewest);
+ const parsedQuarters = parseQuarters(newestRoadmapData?.value ?? []);
+ const quartersInEnglish = parsedQuarters.find(({ language }) => language === 'English');
+ return quartersInEnglish?.quarters;
+ }, []);
+
+ if (!quarters) {
+ return null;
+ }
+
+ return (
+
+
+
+ {quarters.map((quarter, index) => {
+ return ;
+ })}
+
+
+ );
+};
+
+export default Roadmap;
diff --git a/src/components/dashboard-page/Roadmap/style.scss b/src/components/dashboard-page/Roadmap/style.scss
new file mode 100644
index 000000000..355ca0b53
--- /dev/null
+++ b/src/components/dashboard-page/Roadmap/style.scss
@@ -0,0 +1,144 @@
+@import '../../../styles/main';
+@import '../../index-page/shared-styles';
+
+.dashboard-roadmap {
+ @include dashboard-section;
+
+ &__container {
+ @include dashboard-container;
+ }
+
+ &__quarter {
+ display: grid;
+ gap: 16px;
+
+ &:not(:last-child) {
+ margin-bottom: 16px;
+ }
+ }
+
+ &__quarter-header-wrapper {
+ padding: 1px;
+ background: linear-gradient(180deg, #52616b 0%, rgba(82, 97, 107, 0) 100%);
+ border-radius: 8px;
+ }
+
+ &__quarter-header {
+ @include dashboard-widget;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: get-color-from-opaque-and-background(rgb(188, 213, 250), 0.08, $c_black);
+ }
+
+ &__quarter-heading {
+ @include h500;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__button-open-roadmap {
+ padding: 16px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ @include t300;
+ font-weight: 600;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ border-radius: 50%;
+ cursor: pointer;
+ @include dashboard-buttons-states;
+
+ & > span {
+ display: none;
+ }
+ }
+
+ &__delivery-milestone {
+ @include dashboard-widget;
+ height: 184px;
+ background-color: $dashboard-secondary-widget-background-color;
+ border: 1px solid $dashboard-secondary-widget-background-color;
+
+ &:hover {
+ border: 1px solid #dce1e56b;
+ }
+ }
+
+ &__delivery-milestone-icon-wrapper {
+ position: relative;
+ margin-bottom: 12px;
+ width: 48px;
+ height: 48px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: #716bff26;
+ border: 1px solid #a3c3f230;
+ border-radius: 50%;
+ }
+
+ &__delivery-milestone-icon {
+ width: 24px;
+ height: 24px;
+ filter: brightness(0) saturate(100%) invert(32%) sepia(47%) saturate(5519%) hue-rotate(235deg) brightness(99%)
+ contrast(104%);
+ }
+
+ &__delivery-milestone-title {
+ margin-bottom: 8px;
+ @include line-clamp(1);
+ @include h400;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__delivery-milestone-description {
+ @include line-clamp(3);
+ font-family: $font-secondary;
+ font-size: 14px;
+ line-height: 20px;
+ color: $dashboard-base-gray-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ @media #{$screen-min-dashboard-xs} {
+ &__button-open-roadmap {
+ padding: 12px 20px;
+ border-radius: 2px;
+
+ & > span {
+ display: block;
+ }
+ }
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__quarter {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ &__quarter-header-wrapper {
+ grid-column: 1 / span 2;
+ }
+
+ &__quarter-heading {
+ @include h600;
+ }
+
+ &__delivery-milestone {
+ height: 216px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ &__quarter {
+ gap: 24px;
+
+ &:not(:last-child) {
+ margin-bottom: 24px;
+ }
+ }
+ }
+}
diff --git a/src/components/dashboard-page/SectionHeader/index.js b/src/components/dashboard-page/SectionHeader/index.js
new file mode 100644
index 000000000..c12ee18b2
--- /dev/null
+++ b/src/components/dashboard-page/SectionHeader/index.js
@@ -0,0 +1,64 @@
+import React, { useState, useEffect } from 'react';
+import cn from 'classnames';
+import { string } from 'prop-types';
+
+import { ReactComponent as CopyLinkIcon } from '../../../assets/svg/dashboard/copy-link-icon.svg';
+
+import './style.scss';
+
+const propTypes = {
+ sectionId: string.isRequired,
+ sectionHeading: string.isRequired,
+};
+
+const timeLinkCopiedTooltipVisibleFor = 3000; // in ms
+
+const SectionHeader = ({ sectionId, sectionHeading }) => {
+ const [isLinkCopiedTooltipVisible, setIsLinkCopiedTooltipVisible] = useState(false);
+
+ const onButtonCopyLinkClick = async () => {
+ const location = window.location.href;
+ const noHash = location.replace(/#.*/gm, '');
+ await navigator.clipboard.writeText(`${noHash}#${sectionId}`);
+
+ if (isLinkCopiedTooltipVisible) {
+ return;
+ }
+ setIsLinkCopiedTooltipVisible(true);
+ };
+
+ useEffect(() => {
+ if (!isLinkCopiedTooltipVisible) {
+ return;
+ }
+ const linkCopiedTooltipVisibleTimeoutId = setTimeout(() => {
+ setIsLinkCopiedTooltipVisible(false);
+ }, timeLinkCopiedTooltipVisibleFor);
+ return () => {
+ clearTimeout(linkCopiedTooltipVisibleTimeoutId);
+ };
+ }, [isLinkCopiedTooltipVisible]);
+
+ return (
+
+
{sectionHeading}
+
+ Copy link
+ Copy link to this section
+
+
+
+
Link copied to the clipboard!
+
+
+ );
+};
+
+SectionHeader.propTypes = propTypes;
+
+export default SectionHeader;
diff --git a/src/components/dashboard-page/SectionHeader/style.scss b/src/components/dashboard-page/SectionHeader/style.scss
new file mode 100644
index 000000000..b13efe174
--- /dev/null
+++ b/src/components/dashboard-page/SectionHeader/style.scss
@@ -0,0 +1,92 @@
+@import '../../../styles/main';
+
+.dashboard-section-header {
+ scroll-margin-top: calc(32px + $dashboard-header-height);
+
+ margin-bottom: 16px;
+ padding-bottom: 16px;
+ position: relative;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid $dashboard-base-border-color;
+
+ &__heading {
+ @include h500;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__button-copy-link {
+ padding: 12px 20px;
+ display: flex;
+ align-items: center;
+ @include t300;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ border-radius: 2px;
+ cursor: pointer;
+ @include dashboard-buttons-states;
+
+ &_text {
+ display: none;
+ }
+
+ &_icon {
+ margin-left: 8px;
+ }
+ }
+
+ &__tooltip {
+ padding: 8px;
+ position: absolute;
+ bottom: calc(100% + 8px);
+ right: 0;
+ background-color: $dashboard-section-header-tooltip-background-color;
+ border-radius: 2px;
+ box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.12), 0px 4px 4px 0px rgba(0, 0, 0, 0.1);
+ opacity: 0;
+ visibility: hidden;
+ transition: visibility 0.3s linear, opacity 0.3s linear;
+ transition-delay: 0.1s;
+
+ &.is-link-copied-tooltip-visible {
+ opacity: 1;
+ visibility: visible;
+ }
+ }
+
+ &__tooltip-text {
+ @include t100;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ @media #{$screen-min-dashboard-xs} {
+ &__heading {
+ @include h600;
+ }
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__button-copy-link {
+ &_short-text {
+ display: none;
+ }
+ &_text {
+ display: block;
+ }
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ margin-bottom: 24px;
+ padding-bottom: 24px;
+ }
+
+ &__heading {
+ @include h700;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Skeleton/index.js b/src/components/dashboard-page/Skeleton/index.js
new file mode 100644
index 000000000..1bb964718
--- /dev/null
+++ b/src/components/dashboard-page/Skeleton/index.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import cn from 'classnames';
+import { string } from 'prop-types';
+
+import './style.scss';
+
+const propTypes = {
+ skeletonCn: string,
+};
+
+const Skeleton = ({ skeletonCn }) => {
+ return (
+
+ );
+};
+
+Skeleton.propTypes = propTypes;
+
+export default Skeleton;
diff --git a/src/components/dashboard-page/Skeleton/style.scss b/src/components/dashboard-page/Skeleton/style.scss
new file mode 100644
index 000000000..347f29b6e
--- /dev/null
+++ b/src/components/dashboard-page/Skeleton/style.scss
@@ -0,0 +1,33 @@
+@keyframes wave {
+ 0% {
+ transform: translateX(-100%);
+ }
+ 100% {
+ transform: translateX(100%);
+ }
+}
+
+.dashboard-skeleton {
+ position: relative;
+ overflow: hidden;
+ background-color: #181c1f;
+
+ &__overlay {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background: linear-gradient(
+ 104deg,
+ rgba(183, 200, 250, 0) 15%,
+ rgba(183, 200, 250, 0.06) 30%,
+ rgba(187, 217, 246, 0.13) 48%,
+ rgba(187, 217, 246, 0.13) 52%,
+ rgba(183, 200, 250, 0.06) 67%,
+ rgba(183, 200, 250, 0) 70%,
+ rgba(183, 200, 250, 0) 85%
+ );
+ animation: wave 2s cubic-bezier(0, 0.1, 0.2, 1) infinite;
+ }
+}
diff --git a/src/components/dashboard-page/StatsWidget/index.js b/src/components/dashboard-page/StatsWidget/index.js
new file mode 100644
index 000000000..079ee6573
--- /dev/null
+++ b/src/components/dashboard-page/StatsWidget/index.js
@@ -0,0 +1,49 @@
+import React from 'react';
+import cn from 'classnames';
+import { string, bool, oneOfType, number } from 'prop-types';
+
+import WidgetHeading from '../WidgetHeading';
+
+import './style.scss';
+
+const propTypes = {
+ heading: string.isRequired,
+ text: oneOfType([string, number]).isRequired,
+ helperText: string,
+ withTextSizeIncreasedFromMd: bool,
+ termDefinitionKey: string,
+ headingWrapperCn: string,
+};
+
+const DashboardStatsWidget = ({
+ heading,
+ text,
+ helperText,
+ withTextSizeIncreasedFromMd,
+ termDefinitionKey,
+ headingWrapperCn,
+}) => {
+ return (
+
+
+
+
+ {text}
+
+ {!!helperText &&
{helperText}
}
+
+
+ );
+};
+
+DashboardStatsWidget.propTypes = propTypes;
+
+export default DashboardStatsWidget;
diff --git a/src/components/dashboard-page/StatsWidget/style.scss b/src/components/dashboard-page/StatsWidget/style.scss
new file mode 100644
index 000000000..e0eab933a
--- /dev/null
+++ b/src/components/dashboard-page/StatsWidget/style.scss
@@ -0,0 +1,29 @@
+@import '../../../styles/main';
+
+.dashboard-stats-widget {
+ @include dashboard-widget;
+
+ &__heading {
+ margin-bottom: 16px;
+ }
+
+ &__text {
+ @include dashboard-widget-text;
+ }
+
+ &__helper-text {
+ @include dashboard-widget-helper-text;
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__text.with-text-size-increased-from-md {
+ @include h700;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ &__text.with-text-size-increased-from-md {
+ @include h800;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Table/data.js b/src/components/dashboard-page/Table/data.js
new file mode 100644
index 000000000..382cae613
--- /dev/null
+++ b/src/components/dashboard-page/Table/data.js
@@ -0,0 +1,3 @@
+const endAlignedCols = ['rateOfTotalSupply', 'tokenAmount', 'rateOfTgeUnlock'];
+
+export const shouldEndAlign = accessorKey => endAlignedCols.includes(accessorKey);
diff --git a/src/components/dashboard-page/Table/index.js b/src/components/dashboard-page/Table/index.js
new file mode 100644
index 000000000..802f087ff
--- /dev/null
+++ b/src/components/dashboard-page/Table/index.js
@@ -0,0 +1,59 @@
+import React from 'react';
+import cn from 'classnames';
+import { arrayOf, shape, string, object } from 'prop-types';
+
+import { shouldEndAlign } from './data';
+
+import './style.scss';
+
+const propTypes = {
+ columns: arrayOf(
+ shape({
+ header: string.isRequired,
+ accessorKey: string.isRequired,
+ })
+ ),
+ data: arrayOf(object),
+ tableCn: string,
+};
+
+const DashboardTable = ({ columns, data, tableCn }) => {
+ return (
+
+
+
+ {columns.map((column, index) => (
+
+ {column.header}
+
+ ))}
+
+
+
+ {data.map((rowData, index) => (
+
+ {columns.map((column, index) => (
+
+ {rowData[column.accessorKey]}
+
+ ))}
+
+ ))}
+
+
+ );
+};
+
+DashboardTable.propTypes = propTypes;
+
+export default DashboardTable;
diff --git a/src/components/dashboard-page/Table/style.scss b/src/components/dashboard-page/Table/style.scss
new file mode 100644
index 000000000..329962088
--- /dev/null
+++ b/src/components/dashboard-page/Table/style.scss
@@ -0,0 +1,64 @@
+@import '../../../styles/main';
+
+.dashboard-table {
+ width: 100%;
+
+ &__head {
+ background-color: $dashboard-widget-base-background-color;
+ }
+
+ &__row {
+ box-shadow: 0px -1px 0px 0px rgba(187, 217, 246, 0.13) inset;
+ }
+
+ & th {
+ vertical-align: middle;
+ }
+
+ &__cell {
+ padding: 16px 8px;
+
+ &:first-of-type {
+ padding-left: 24px;
+ }
+
+ &:last-of-type {
+ padding-right: 24px;
+ }
+
+ &:nth-of-type(2n),
+ &:nth-of-type(3n) {
+ padding-left: 0;
+ }
+
+ &.end-align {
+ text-align: end;
+ }
+ }
+
+ &__head-cell {
+ @include h100;
+ color: $dashboard-base-gray-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ text-align: start;
+ text-transform: uppercase;
+ }
+
+ &__body-cell {
+ @include t100;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__cell {
+ &:first-of-type {
+ padding-left: 40px;
+ }
+
+ &:last-of-type {
+ padding-right: 40px;
+ }
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Team/ActionButton/index.js b/src/components/dashboard-page/Team/ActionButton/index.js
new file mode 100644
index 000000000..491ff8828
--- /dev/null
+++ b/src/components/dashboard-page/Team/ActionButton/index.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import cn from 'classnames';
+import { string, func } from 'prop-types';
+
+import { ReactComponent as NewTabIcon } from '../../../../assets/svg/dashboard/new-tab.svg';
+
+import './style.scss';
+
+const propTypes = {
+ text: string.isRequired,
+ onClick: func,
+ buttonCn: string,
+};
+
+const ActionButton = ({ text, onClick, buttonCn }) => {
+ return (
+
+ {text}
+
+
+ );
+};
+
+ActionButton.propTypes = propTypes;
+
+export default ActionButton;
diff --git a/src/components/dashboard-page/Team/ActionButton/style.scss b/src/components/dashboard-page/Team/ActionButton/style.scss
new file mode 100644
index 000000000..180ce5690
--- /dev/null
+++ b/src/components/dashboard-page/Team/ActionButton/style.scss
@@ -0,0 +1,35 @@
+@import '../../../../styles/main';
+
+.dashboard-team-action-button {
+ width: 100%;
+ height: 48px;
+ padding: 12px 18px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+ @include t300;
+ font-weight: 600;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ background-color: $dashboard-widget-base-background-color;
+ border: 1px solid $dashboard-base-borders-color;
+ border-radius: 2px;
+ cursor: pointer;
+ transition: all $dashboard-transition-duration $dashboard-transition-timing;
+
+ &:hover,
+ &:focus {
+ border-color: $dashboard-base-border-color;
+ background-color: $dashboard-widget-base-info-states-background-color;
+ }
+
+ &:active {
+ background-color: $dashboard-widget-base-background-color;
+ transition-duration: 0ms;
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ width: 210px;
+ }
+}
diff --git a/src/components/dashboard-page/Team/Council/index.js b/src/components/dashboard-page/Team/Council/index.js
new file mode 100644
index 000000000..6840efa75
--- /dev/null
+++ b/src/components/dashboard-page/Team/Council/index.js
@@ -0,0 +1,56 @@
+import React from 'react';
+import { object } from 'prop-types';
+
+import WidgetHeading from '../../WidgetHeading';
+import ActionButton from '../ActionButton';
+
+import { parseCouncilTermLength } from '../utils';
+import { termDefinitions } from '../../../../data/pages/dashboard/termDefinitions';
+import { withFallbackNumVal } from '../../../../utils/withFallbackVal';
+
+import './style.scss';
+
+const propTypes = {
+ data: object,
+};
+
+const viewPastCouncilsLink = 'https://pioneerapp.xyz/#/council/past-councils';
+
+const Council = ({ data }) => {
+ return (
+
+
+
+
+
Council
+
{termDefinitions.council}
+
+
+
+
+
+ );
+};
+
+Council.propTypes = propTypes;
+
+export default Council;
diff --git a/src/components/dashboard-page/Team/Council/style.scss b/src/components/dashboard-page/Team/Council/style.scss
new file mode 100644
index 000000000..7e429fa78
--- /dev/null
+++ b/src/components/dashboard-page/Team/Council/style.scss
@@ -0,0 +1,123 @@
+@import '../../../../styles/main';
+
+.dashboard-team-council-wrapper {
+ padding: 1px;
+ background: linear-gradient(180deg, #52616b 0%, rgba(82, 97, 107, 0) 100%);
+ border-radius: 8px;
+}
+
+.dashboard-team-council {
+ @include dashboard-widget;
+ background-color: get-color-from-opaque-and-background(rgb(188, 213, 250), 0.08, $c_black);
+
+ &__container {
+ display: grid;
+ gap: 40px;
+ }
+
+ &__heading {
+ margin-bottom: 16px;
+ @include h500;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__role-description {
+ @include t200;
+ color: $dashboard-base-gray-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__terms-actions-wrapper {
+ display: grid;
+ gap: 40px;
+ }
+
+ &__terms-list {
+ padding: 0;
+ list-style: none;
+
+ display: grid;
+ gap: 24px;
+ }
+
+ &__terms-list-item {
+ margin: 0;
+ padding: 0;
+ }
+
+ &__term {
+ @include dashboard-widget-text;
+ }
+
+ @media #{$screen-min-dashboard-xs} {
+ &__heading {
+ @include h600;
+ }
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__container {
+ grid-template-columns: repeat(3, 1fr);
+ grid-template-rows: repeat(2, min-content);
+ }
+
+ &__description-wrapper {
+ grid-column: 1 / span 2;
+ }
+
+ &__heading {
+ margin-bottom: 24px;
+ @include h700;
+ }
+
+ &__terms-actions-wrapper {
+ grid-row: 2;
+ grid-column: 1 / span 3;
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ &__terms-list {
+ grid-column: 1 / span 2;
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ &__action-button-link > .dashboard-team-action-button {
+ width: fit-content;
+ min-width: 210px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ &__heading {
+ @include h800;
+ }
+
+ &__container {
+ gap: unset;
+ grid-template-rows: unset;
+ grid-template-columns: 450px 424px;
+ justify-content: space-between;
+ }
+
+ &__description-wrapper {
+ grid-column: unset;
+ }
+
+ &__terms-actions-wrapper {
+ grid-row: unset;
+ grid-column: unset;
+ grid-template-columns: unset;
+ gap: unset;
+ }
+
+ &__terms-list {
+ grid-column: unset;
+ }
+
+ &__action-button-link {
+ align-self: self-end;
+ justify-self: flex-end;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Team/CurrentCouncil/index.js b/src/components/dashboard-page/Team/CurrentCouncil/index.js
new file mode 100644
index 000000000..11f7ffc4d
--- /dev/null
+++ b/src/components/dashboard-page/Team/CurrentCouncil/index.js
@@ -0,0 +1,62 @@
+import React from 'react';
+import { object } from 'prop-types';
+
+import WidgetHeading from '../../WidgetHeading';
+import ActionButton from '../ActionButton';
+
+import { parseWeeklySalaryInJOY, getDaysLeftToNextElection } from './utils';
+import { formatDateToShowInTooltip as parseElectedOnDate } from '../../Token/PriceChart/utils';
+
+import './style.scss';
+
+const propTypes = {
+ data: object,
+};
+
+const readCouncilPlanLink = 'https://pioneerapp.xyz/#/forum/category/7';
+
+const CurrentCouncil = ({ data }) => {
+ const parsedElectionDate = parseElectedOnDate(data?.electedOnDate || new Date());
+
+ const parsedWeeklySalaryInJoy = parseWeeklySalaryInJOY(data?.weeklySalaryInJOY);
+
+ const daysInService = data?.termLength;
+ const nextElectionDate = data?.nextElectionDate || new Date();
+
+ const daysLeftToNextElection = getDaysLeftToNextElection(nextElectionDate);
+ const remainingDaysInServicePercentage = Math.round((daysLeftToNextElection / daysInService) * 100);
+
+ return (
+
+
+
+
+
Elected on
+
{parsedElectionDate}
+
+
+
+
+ {daysLeftToNextElection} Days until next election
+
+
+
+
Weekly councilor salary
+
{parsedWeeklySalaryInJoy}
+
+
+
+
+
+
+ );
+};
+
+CurrentCouncil.propTypes = propTypes;
+
+export default CurrentCouncil;
diff --git a/src/components/dashboard-page/Team/CurrentCouncil/style.scss b/src/components/dashboard-page/Team/CurrentCouncil/style.scss
new file mode 100644
index 000000000..6470019ff
--- /dev/null
+++ b/src/components/dashboard-page/Team/CurrentCouncil/style.scss
@@ -0,0 +1,79 @@
+@import '../../../../styles/main';
+
+.dashboard-team-current-council {
+ @include dashboard-widget;
+
+ &__container {
+ display: grid;
+ gap: 40px;
+ }
+
+ &__info-label {
+ @include t400;
+ color: $dashboard-base-gray-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+
+ & > span {
+ font-weight: 600;
+ color: $dashboard-content-base-text-color;
+ }
+ }
+
+ &__info {
+ @include h500;
+ color: $dashboard-base-white-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__progress-bar {
+ position: relative;
+ margin-bottom: 8px;
+ width: 100%;
+ height: 20px;
+ background-color: $dashboard-progress-bar-color;
+ border-radius: 4px;
+ }
+
+ &__progress-bar-content {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 20px;
+ background-color: #8890ff;
+ border-radius: 4px;
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ grid-column: 1 / span 3;
+ }
+
+ &__container {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 16px;
+ }
+
+ &__info {
+ @include h600;
+ }
+
+ &__progress-bar {
+ max-width: 192px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-lg} {
+ & {
+ grid-column: 1;
+ }
+
+ &__container {
+ grid-template-columns: 1fr;
+ gap: 40px;
+ }
+
+ &__progress-bar {
+ max-width: none;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Team/CurrentCouncil/utils.js b/src/components/dashboard-page/Team/CurrentCouncil/utils.js
new file mode 100644
index 000000000..330539618
--- /dev/null
+++ b/src/components/dashboard-page/Team/CurrentCouncil/utils.js
@@ -0,0 +1,18 @@
+import { isNaN } from '../../../../utils/withFallbackVal';
+
+const msInDay = 1000 * 60 * 60 * 24;
+
+export const getDaysLeftToNextElection = nextElectionDate => {
+ return Math.round((new Date(nextElectionDate).getTime() - new Date().getTime()) / msInDay);
+};
+
+export const parseWeeklySalaryInJOY = weeklySalaryInJOY => {
+ if (isNaN(weeklySalaryInJOY)) {
+ return '0 JOY';
+ }
+
+ const roundedWeeklySalaryInJoy = Math.round(weeklySalaryInJOY);
+ // French locale uses space as a separator
+ const weeklySalaryInJoyWithSeparators = roundedWeeklySalaryInJoy.toLocaleString('fr-FR');
+ return `${weeklySalaryInJoyWithSeparators} JOY`;
+};
diff --git a/src/components/dashboard-page/Team/Jsgenesis/index.js b/src/components/dashboard-page/Team/Jsgenesis/index.js
new file mode 100644
index 000000000..c0affb5c4
--- /dev/null
+++ b/src/components/dashboard-page/Team/Jsgenesis/index.js
@@ -0,0 +1,75 @@
+import React from 'react';
+import { string, arrayOf, shape, oneOf } from 'prop-types';
+
+import ArrowButton from '../../ArrowButton';
+
+import { renderSocialMediaLogo, jsgenesisLink } from '../utils';
+import { termDefinitions } from '../../../../data/pages/dashboard/termDefinitions';
+
+import './style.scss';
+
+const founderPropTypes = {
+ name: string.isRequired,
+ avatar: string.isRequired,
+ socialMediaUsernames: arrayOf(
+ shape({
+ socialMedia: oneOf(['email', 'twitter', 'telegram', 'discord']).isRequired,
+ username: string.isRequired,
+ })
+ ).isRequired,
+};
+
+const Founder = ({ name, avatar, socialMediaUsernames }) => {
+ return (
+
+
+
+
+
+
+ {socialMediaUsernames.map((sm, idx) => {
+ return (
+
+ {renderSocialMediaLogo(sm.socialMedia)}
+
{sm.username}
+
+ );
+ })}
+
+
+ );
+};
+
+Founder.propTypes = founderPropTypes;
+
+const jsgenesisPropTypes = {
+ founders: arrayOf(shape(founderPropTypes)).isRequired,
+};
+
+const Jsgenesis = ({ founders }) => {
+ return (
+
+
+
+
+
Jsgenesis
+
+
{termDefinitions.jsgenesis}
+
+
+
+
+ {founders.map((founder, idx) => {
+ return
;
+ })}
+
+
+ );
+};
+
+Jsgenesis.propTypes = jsgenesisPropTypes;
+
+export default Jsgenesis;
diff --git a/src/components/dashboard-page/Team/Jsgenesis/style.scss b/src/components/dashboard-page/Team/Jsgenesis/style.scss
new file mode 100644
index 000000000..ebe6b6840
--- /dev/null
+++ b/src/components/dashboard-page/Team/Jsgenesis/style.scss
@@ -0,0 +1,142 @@
+@import '../../../../styles/main';
+
+.dashboard-team-jsgenesis {
+ display: grid;
+ gap: 16px;
+
+ &__description-widget-wrapper {
+ padding: 1px;
+ background: linear-gradient(180deg, #52616b 0%, rgba(82, 97, 107, 0) 100%);
+ border-radius: 8px;
+ }
+
+ &__description-widget {
+ @include dashboard-widget;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ background-color: get-color-from-opaque-and-background(rgb(188, 213, 250), 0.08, $c_black);
+ }
+
+ &__description-widget-heading {
+ @include h600;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__description {
+ @include t200;
+ color: $dashboard-base-gray-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__button-read-more {
+ margin-top: 8px;
+ }
+
+ &__founder {
+ @include dashboard-widget;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ overflow: hidden;
+ }
+
+ &__founder-avatar {
+ width: 123px;
+ height: 123px;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: cover;
+ border-radius: 50%;
+ }
+
+ &__founder-role-name-wrapper {
+ text-align: center;
+ }
+
+ &__founder-role {
+ @include t100;
+ font-weight: 700;
+ color: $dashboard-base-gray-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__founder-name {
+ @include t400;
+ font-weight: 600;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__founder-social-media-usernames-wrapper {
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 8px;
+ width: 326px;
+ padding-inline: 33px;
+ }
+
+ &__founder-social-media-tag {
+ padding: 4px 6px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ background-color: #272d33;
+ border-radius: 4px;
+
+ & > svg {
+ width: 16px;
+ height: 16px;
+ }
+ }
+
+ &__founder-social-media-username {
+ @include t100;
+ font-weight: 500;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ & {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ &__description-widget-wrapper {
+ grid-column: 1 / span 2;
+ }
+
+ &__description-widget-heading {
+ @include h700;
+ }
+
+ &__description {
+ max-width: 450px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ gap: 24px;
+ }
+
+ &__description-widget-heading {
+ @include h800;
+ }
+ }
+
+ @media #{$screen-min-dashboard-lg} {
+ & {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ &__description-widget {
+ gap: 0;
+ justify-content: space-between;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Team/PastCouncil/index.js b/src/components/dashboard-page/Team/PastCouncil/index.js
new file mode 100644
index 000000000..52cf998e0
--- /dev/null
+++ b/src/components/dashboard-page/Team/PastCouncil/index.js
@@ -0,0 +1,56 @@
+import React from 'react';
+import { string, arrayOf, shape, oneOf, number } from 'prop-types';
+
+import WidgetHeading from '../../WidgetHeading';
+
+import { renderSocialMediaLogo } from '../utils';
+
+import './style.scss';
+
+const propTypes = {
+ linkToPioneerProfile: string.isRequired,
+ username: string.isRequired,
+ avatar: string.isRequired,
+ socialMediaUsernames: arrayOf(
+ shape({
+ socialMedia: oneOf(['email', 'twitter', 'telegram', 'discord']).isRequired,
+ username: string.isRequired,
+ })
+ ).isRequired,
+ timesServed: number.isRequired,
+};
+
+const PastCouncil = ({ linkToPioneerProfile, username, avatar, socialMediaUsernames, timesServed }) => {
+ return (
+
+
+
+
+
+
+
+
{username}
+
+ {socialMediaUsernames.map((sm, idx) => {
+ return (
+
+ {renderSocialMediaLogo(sm.socialMedia)}
+
{sm.username}
+
+ );
+ })}
+
+
+
+
+
+
+ );
+};
+
+PastCouncil.propTypes = propTypes;
+
+export default PastCouncil;
diff --git a/src/components/dashboard-page/Team/PastCouncil/style.scss b/src/components/dashboard-page/Team/PastCouncil/style.scss
new file mode 100644
index 000000000..44128ed8e
--- /dev/null
+++ b/src/components/dashboard-page/Team/PastCouncil/style.scss
@@ -0,0 +1,116 @@
+@import '../../../../styles/main';
+
+.dashboard-team-past-council {
+ position: relative;
+ height: 484px;
+ padding-block: 112px 32px;
+ background-color: $dashboard-secondary-widget-background-color;
+ border: 1px solid $dashboard-secondary-widget-background-color;
+ border-radius: 8px;
+ overflow: hidden;
+ z-index: 1;
+
+ &:hover {
+ border: 1px solid #dce1e56b;
+ }
+
+ &__inner-bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 181px;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 115%;
+ filter: blur(10px);
+ clip-path: border-box;
+ border-top-right-radius: 8px;
+ border-top-left-radius: 8px;
+ z-index: -1;
+ }
+
+ &__inner-bg-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 181px;
+ background-color: #101214bf;
+ z-index: -1;
+ }
+
+ &__container {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ &__avatar {
+ margin-bottom: 4px;
+ display: block;
+ width: 123px;
+ height: 123px;
+ background-color: #0f1114;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: cover;
+ border-radius: 50%;
+ }
+
+ &__username {
+ margin-bottom: 8px;
+ @include t400;
+ font-weight: 600;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__social-media-usernames {
+ padding-inline: 32px;
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ &__social-media-tag {
+ padding: 4px 6px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 4px;
+ background-color: $dashboard-widget-tags-background-color;
+ border-radius: 4px;
+
+ & > svg {
+ width: 16px;
+ height: 16px;
+ }
+ }
+
+ &__social-media-username {
+ @include t100;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__times-served-box {
+ margin-top: auto;
+ width: 200px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ & .dashboard-widget-heading__heading {
+ @include t300;
+ }
+ }
+
+ &__times-served {
+ @include h500;
+ color: $dashboard-base-white-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+}
diff --git a/src/components/dashboard-page/Team/Skeletons/index.js b/src/components/dashboard-page/Team/Skeletons/index.js
new file mode 100644
index 000000000..6abdf453c
--- /dev/null
+++ b/src/components/dashboard-page/Team/Skeletons/index.js
@@ -0,0 +1,13 @@
+import React from 'react';
+
+import Skeleton from '../../Skeleton';
+
+import './style.scss';
+
+export const CouncilsBlockSkeleton = () => {
+ return
;
+};
+
+export const WorkingGroupsBlockSkeleton = () => {
+ return
;
+};
diff --git a/src/components/dashboard-page/Team/Skeletons/style.scss b/src/components/dashboard-page/Team/Skeletons/style.scss
new file mode 100644
index 000000000..8a0c02fe7
--- /dev/null
+++ b/src/components/dashboard-page/Team/Skeletons/style.scss
@@ -0,0 +1,16 @@
+@import '../../../../styles/main';
+
+.councils-block-skeleton {
+ margin-bottom: 64px;
+ width: 100%;
+ height: 740px;
+
+ @media #{$screen-min-dashboard-md} {
+ margin-bottom: 80px;
+ }
+}
+
+.working-groups-block-skeleton {
+ width: 100%;
+ height: 425px;
+}
diff --git a/src/components/dashboard-page/Team/WorkingGroups/index.js b/src/components/dashboard-page/Team/WorkingGroups/index.js
new file mode 100644
index 000000000..d9e185335
--- /dev/null
+++ b/src/components/dashboard-page/Team/WorkingGroups/index.js
@@ -0,0 +1,130 @@
+import React from 'react';
+import { string, shape, arrayOf, bool } from 'prop-types';
+
+import Carousel from '../../Carousel';
+import { WorkingGroupsBlockSkeleton } from '../Skeletons';
+
+import { ReactComponent as EmptyAvatar } from '../../../../assets/svg/dashboard/empty-avatar.svg';
+
+import { termDefinitions } from '../../../../data/pages/dashboard/termDefinitions';
+
+import './style.scss';
+
+const workingGroupPropTypes = {
+ name: string.isRequired,
+ logo: string.isRequired,
+ link: string.isRequired,
+ lead: shape({
+ avatar: string,
+ username: string,
+ }),
+ currentBudget: string.isRequired,
+ workers: arrayOf(
+ shape({
+ avatar: string,
+ username: string.isRequired,
+ })
+ ).isRequired,
+};
+
+const WorkingGroup = ({ name, logo, link, lead, currentBudget, workers }) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
{name}
+
+
+
+
+
+
+
Current budget:
+
{currentBudget}
+
+
+
+
+ {/* eslint-disable-next-line max-len */}
+
{`Workers (${workers.length})`}
+
+ {workers.map((worker, index) => {
+ return (
+
+
+ {!!worker.avatar ? (
+
+ ) : (
+
+ )}
+
{worker.username}
+
+
+ );
+ })}
+
+
+
+
+
+ );
+};
+
+WorkingGroup.propTypes = workingGroupPropTypes;
+
+const workingGroupsPropTypes = {
+ groups: arrayOf(shape(workingGroupPropTypes)).isRequired,
+ loading: bool,
+};
+
+const WorkingGroups = ({ groups, loading }) => {
+ return (
+
+
+
+
Working groups
+
{termDefinitions.workingGroups}
+
+
+
+ {loading || !groups.length ? (
+
+ ) : (
+
+ {groups.map((group, index) => {
+ return ;
+ })}
+
+ )}
+
+ );
+};
+
+WorkingGroups.propTypes = workingGroupsPropTypes;
+
+export default WorkingGroups;
diff --git a/src/components/dashboard-page/Team/WorkingGroups/style.scss b/src/components/dashboard-page/Team/WorkingGroups/style.scss
new file mode 100644
index 000000000..e08c30afb
--- /dev/null
+++ b/src/components/dashboard-page/Team/WorkingGroups/style.scss
@@ -0,0 +1,251 @@
+@import '../../../../styles/main';
+
+.dashboard-team-working-groups {
+ margin-bottom: 64px;
+
+ &__description-widget-wrapper {
+ margin-bottom: 16px;
+ padding: 1px;
+ background: linear-gradient(180deg, #52616b 0%, rgba(82, 97, 107, 0) 100%);
+ border-radius: 8px;
+ }
+
+ &__description-widget {
+ @include dashboard-widget;
+ background-color: get-color-from-opaque-and-background(rgb(188, 213, 250), 0.08, $c_black);
+ }
+
+ &__description-widget-heading {
+ margin-bottom: 16px;
+ @include h600;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__description {
+ max-width: 450px;
+ @include t200;
+ color: $dashboard-base-gray-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__group-link {
+ &:not(:last-child) {
+ margin-right: 16px;
+ }
+ }
+
+ &__group {
+ position: relative;
+ width: 272px;
+ height: 100%;
+ background-color: $dashboard-widget-base-background-color;
+ border: 1px solid $dashboard-widget-base-background-color;
+ border-radius: 8px;
+ overflow: hidden;
+
+ &:hover {
+ border: 1px solid #dce1e56b;
+ }
+ }
+
+ &__group-inner-bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 80px;
+ background-color: #ffffff;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: cover;
+ filter: blur(10px);
+ clip-path: border-box;
+ border-top-right-radius: 8px;
+ border-top-left-radius: 8px;
+ z-index: -1;
+ }
+
+ &__group-inner-bg-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 80px;
+ background-color: #101214bf;
+ z-index: -1;
+ }
+
+ &__group-content-container {
+ padding: 52px 16px 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ &__group-logo-wrapper {
+ margin-bottom: 8px;
+ margin-inline: auto;
+ width: 56px;
+ height: 56px;
+ padding: 4px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: #dae2eb;
+ border-radius: 8px;
+ }
+
+ &__group-logo {
+ width: 48px;
+ mix-blend-mode: darken;
+ }
+
+ &__group-name {
+ @include h500;
+ color: $dashboard-base-white-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ text-align: center;
+ text-transform: capitalize;
+ }
+
+ &__group-lead-and-budget {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ &__group-labels {
+ margin-bottom: 4px;
+ @include t300;
+ color: $dashboard-base-gray-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+
+ &.with-margin-bottom-increased {
+ margin-bottom: 8px;
+ }
+ }
+
+ &__group-lead-avatar-name-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ &__group-lead-avatar {
+ width: 32px;
+ height: 32px;
+ background-color: #0f1114;
+ background-repeat: no-repeat;
+ background-size: cover;
+ background-position: center;
+ border: 1px solid #bcd5fa14;
+ border-radius: 50%;
+
+ &.size-reduced {
+ width: 24px;
+ height: 24px;
+ }
+ }
+
+ &__group-lead-username {
+ @include h400;
+ color: $dashboard-base-white-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__group-budget-value {
+ @extend .dashboard-team-working-groups__group-lead-username;
+ color: #fcfcfc;
+ }
+
+ &__group-workers-list {
+ padding: 0;
+ list-style: none;
+
+ margin-top: 4px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ &__group-workers-list-item {
+ margin: 0;
+ padding: 0;
+ }
+
+ &__group-worker-tag {
+ padding: 4px 8px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background-color: #272d33;
+ border-radius: 4px;
+
+ & > svg {
+ border-radius: 50%;
+ }
+ }
+
+ &__group-worker-username {
+ @include t100;
+ font-weight: 500;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__description-widget-heading {
+ margin-bottom: 24px;
+ @include h700;
+ }
+
+ &__group {
+ width: 360px;
+ }
+
+ &__group-content-container {
+ padding: 52px 32px 32px;
+ }
+
+ &__group-lead-and-budget {
+ gap: 0;
+ flex-direction: row;
+ & > div {
+ width: 50%;
+ }
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ margin-bottom: 80px;
+ }
+
+ &__description-widget-wrapper {
+ margin-bottom: 24px;
+ }
+
+ &__description-widget-heading {
+ @include h800;
+ }
+
+ &__group-link {
+ &:not(:last-child) {
+ margin-right: 24px;
+ }
+ }
+
+ &__group {
+ width: 560px;
+ }
+
+ &__group-name {
+ @include h600;
+ }
+
+ &__group-lead-username {
+ @include h500;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Team/index.js b/src/components/dashboard-page/Team/index.js
new file mode 100644
index 000000000..15f56b57d
--- /dev/null
+++ b/src/components/dashboard-page/Team/index.js
@@ -0,0 +1,54 @@
+import React from 'react';
+import { object, bool } from 'prop-types';
+
+import SectionHeader from '../SectionHeader';
+import Council from './Council';
+import CurrentCouncil from './CurrentCouncil';
+import PastCouncil from './PastCouncil';
+import WorkingGroups from './WorkingGroups';
+import Jsgenesis from './Jsgenesis';
+import { CouncilsBlockSkeleton } from './Skeletons';
+
+import { parsePastCouncils, parseWorkingGroups, founders } from './utils';
+
+import './style.scss';
+
+const propTypes = {
+ data: object,
+ loading: bool,
+};
+
+const Team = ({ data, loading }) => {
+ const parsedPastCouncils = parsePastCouncils(data?.council?.currentCouncil);
+
+ const parsedWorkingGroups = parseWorkingGroups(data?.workingGroups);
+
+ return (
+
+
+
+
+ {loading ? (
+
+ ) : (
+ <>
+
+
+
+ {parsedPastCouncils.map((pastCouncil, idx) => {
+ return
;
+ })}
+
+ >
+ )}
+
+
+
+
+
+ );
+};
+
+Team.propTypes = propTypes;
+
+export default Team;
diff --git a/src/components/dashboard-page/Team/style.scss b/src/components/dashboard-page/Team/style.scss
new file mode 100644
index 000000000..e423bf270
--- /dev/null
+++ b/src/components/dashboard-page/Team/style.scss
@@ -0,0 +1,39 @@
+@import '../../../styles/main';
+
+.dashboard-team {
+ @include dashboard-section;
+
+ &__container {
+ @include dashboard-container;
+ }
+
+ &__councils {
+ margin-top: 16px;
+ margin-bottom: 64px;
+
+ display: grid;
+ gap: 16px;
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__councils {
+ grid-template-columns: repeat(2, 1fr);
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ &__councils {
+ margin-top: 24px;
+ margin-bottom: 80px;
+
+ gap: 24px;
+ grid-template-columns: repeat(3, 1fr);
+ }
+ }
+
+ @media #{$screen-min-dashboard-lg} {
+ &__councils {
+ grid-template-columns: repeat(4, 1fr);
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Team/utils.js b/src/components/dashboard-page/Team/utils.js
new file mode 100644
index 000000000..3cc9e377f
--- /dev/null
+++ b/src/components/dashboard-page/Team/utils.js
@@ -0,0 +1,172 @@
+import React from 'react';
+
+import storageWorkingGroupLogo from '../../../assets/images/dashboard/working-groups/storage.png';
+import contentWorkingGroupLogo from '../../../assets/images/dashboard/working-groups/content.png';
+import membershipWorkingGroupLogo from '../../../assets/images/dashboard/working-groups/membership.png';
+import appsWorkingGroupLogo from '../../../assets/images/dashboard/working-groups/apps.png';
+import buildersWorkingGroupLogo from '../../../assets/images/dashboard/working-groups/builders.png';
+import distributionWorkingGroupLogo from '../../../assets/images/dashboard/working-groups/distribution.png';
+import forumWorkingGroupLogo from '../../../assets/images/dashboard/working-groups/forum.png';
+import hrWorkingGroupLogo from '../../../assets/images/dashboard/working-groups/hr.png';
+import marketingWorkingGroupLogo from '../../../assets/images/dashboard/working-groups/marketing.png';
+
+import bedehoAvatar from '../../../assets/images/dashboard/founders/bedeho.png';
+import mokhtarAvatar from '../../../assets/images/dashboard/founders/mokhtar.png';
+
+import { ReactComponent as MailIcon } from '../../../assets/svg/dashboard/mail.svg';
+import { ReactComponent as TwitterLogo } from '../../../assets/svg/dashboard/twitter-logo.svg';
+import { ReactComponent as TelegramLogo } from '../../../assets/svg/dashboard/telegram-logo.svg';
+import { ReactComponent as DiscordLogo } from '../../../assets/svg/dashboard/discord-logo.svg';
+
+import { withFallbackNumVal } from '../../../utils/withFallbackVal';
+
+export const founders = [
+ {
+ name: 'Bedeho Mender',
+ avatar: bedehoAvatar,
+ socialMediaUsernames: [
+ {
+ socialMedia: 'email',
+ username: 'bedeho@jsgenesis.com',
+ },
+ {
+ socialMedia: 'twitter',
+ username: 'bedeho',
+ },
+ {
+ socialMedia: 'telegram',
+ username: 'bedeho',
+ },
+ {
+ socialMedia: 'discord',
+ username: 'bedeho',
+ },
+ ],
+ },
+ {
+ name: 'Mokhtar Naamani',
+ avatar: mokhtarAvatar,
+ socialMediaUsernames: [
+ {
+ socialMedia: 'email',
+ username: 'mokhtar@jsgenesis.com',
+ },
+ {
+ socialMedia: 'twitter',
+ username: 'Mokhtar',
+ },
+ {
+ socialMedia: 'telegram',
+ username: 'Mokhtar',
+ },
+ {
+ socialMedia: 'discord',
+ username: 'Mokhtar',
+ },
+ ],
+ },
+];
+
+export const renderSocialMediaLogo = socialMedia => {
+ switch (socialMedia) {
+ case 'email':
+ return
;
+ case 'twitter':
+ return
;
+ case 'telegram':
+ return
;
+ case 'discord':
+ return
;
+ default:
+ return null;
+ }
+};
+
+export const parseCouncilTermLength = (data = {}) => `${withFallbackNumVal(data?.termLength)} days`;
+
+const desiredSocialMediaOrder = {
+ email: 0,
+ twitter: 1,
+ telegram: 2,
+ discord: 3,
+};
+
+export const parsePastCouncils = (councils = []) =>
+ councils.map(c => ({
+ linkToPioneerProfile: c.link,
+ username: c.handle,
+ avatar: c.avatar,
+ socialMediaUsernames: c.socials
+ .map(social => ({
+ socialMedia: social.type.toLowerCase(),
+ username: social.value,
+ }))
+ .sort((a, b) => desiredSocialMediaOrder[a.socialMedia] - desiredSocialMediaOrder[b.socialMedia]),
+ timesServed: c.timesServed,
+ }));
+
+export const getWorkingGroupName = key => {
+ const groupName = key.replace(/workingGroup/gi, '');
+ // Assuming there is single uppercase char (e.g. operationsAlpha), so not using g flag
+ const caps = groupName.match(/[A-Z]/);
+ const hasUppercase = !caps;
+
+ if (hasUppercase) {
+ return groupName;
+ }
+
+ const capsPart = groupName.substring(groupName.indexOf(caps[0]));
+ return groupName.replace(capsPart, ` ${capsPart}`);
+};
+
+const workingGroupsLogos = {
+ appWorkingGroup: appsWorkingGroupLogo,
+ contentWorkingGroup: contentWorkingGroupLogo,
+ distributionWorkingGroup: distributionWorkingGroupLogo,
+ forumWorkingGroup: forumWorkingGroupLogo,
+ membershipWorkingGroup: membershipWorkingGroupLogo,
+ operationsWorkingGroupAlpha: buildersWorkingGroupLogo,
+ operationsWorkingGroupBeta: hrWorkingGroupLogo,
+ operationsWorkingGroupGamma: marketingWorkingGroupLogo,
+ storageWorkingGroup: storageWorkingGroupLogo,
+};
+
+const getWorkingGroupLead = (workers = []) => {
+ const lead = workers.find(w => w.isLead);
+ return {
+ avatar: lead?.avatar,
+ username: lead?.handle,
+ };
+};
+
+export const parseWorkingGroups = (workingGroups = {}) => {
+ const parsed = [];
+ const keys = Object.keys(workingGroups);
+ for (const key of keys) {
+ const group = workingGroups[key];
+ const lead = getWorkingGroupLead(group.workers);
+
+ if (!group.budget || !group.workers.length || !lead.username) {
+ continue;
+ }
+
+ parsed.push({
+ link: `https://pioneerapp.xyz/#/working-groups/${
+ group.name === 'Human Resources' ? 'hr' : group.name.toLowerCase()
+ }`,
+ name: group.name,
+ logo: workingGroupsLogos[key],
+ // French locale uses space as a separator
+ currentBudget: `${Math.round(group.budget).toLocaleString('fr-FR')} JOY`,
+ lead,
+ workers: group.workers.map(w => ({
+ avatar: w.avatar,
+ username: w.handle,
+ })),
+ });
+ }
+
+ return parsed;
+};
+
+export const jsgenesisLink = 'http://www.jsgenesis.com/';
diff --git a/src/components/dashboard-page/Token/AllocationTableWidget/data.js b/src/components/dashboard-page/Token/AllocationTableWidget/data.js
new file mode 100644
index 000000000..64eba8e4a
--- /dev/null
+++ b/src/components/dashboard-page/Token/AllocationTableWidget/data.js
@@ -0,0 +1,63 @@
+export const columns = [
+ {
+ header: 'purpose',
+ accessorKey: 'purpose',
+ },
+ {
+ header: '% of total supply',
+ accessorKey: 'rateOfTotalSupply',
+ },
+ {
+ header: 'token amount',
+ accessorKey: 'tokenAmount',
+ },
+ {
+ header: 'tge unlock %',
+ accessorKey: 'rateOfTgeUnlock',
+ },
+];
+
+export const data = [
+ {
+ purpose: 'Community FM',
+ rateOfTotalSupply: 21.2189609,
+ tokenAmount: 21218960900,
+ rateOfTgeUnlock: 8,
+ },
+ {
+ purpose: 'JSGenesis FM',
+ rateOfTotalSupply: 31.435,
+ tokenAmount: 3143500000,
+ rateOfTgeUnlock: 8,
+ },
+ {
+ purpose: 'Investors',
+ rateOfTotalSupply: 32.3285352,
+ tokenAmount: 32328535200,
+ rateOfTgeUnlock: 79,
+ },
+ {
+ purpose: 'Member airdrop',
+ rateOfTotalSupply: 0.21735,
+ tokenAmount: 2173500000,
+ rateOfTgeUnlock: 8,
+ },
+ {
+ purpose: 'Strategic partners',
+ rateOfTotalSupply: 3.0013001,
+ tokenAmount: 3001300100,
+ rateOfTgeUnlock: 100,
+ },
+ {
+ purpose: 'Reserved 1',
+ rateOfTotalSupply: 11.7988418,
+ tokenAmount: 11798841800,
+ rateOfTgeUnlock: 0,
+ },
+ {
+ purpose: 'Reserved 2',
+ rateOfTotalSupply: 0.000012,
+ tokenAmount: 12000,
+ rateOfTgeUnlock: 8,
+ },
+];
diff --git a/src/components/dashboard-page/Token/AllocationTableWidget/index.js b/src/components/dashboard-page/Token/AllocationTableWidget/index.js
new file mode 100644
index 000000000..c3be56087
--- /dev/null
+++ b/src/components/dashboard-page/Token/AllocationTableWidget/index.js
@@ -0,0 +1,25 @@
+import React from 'react';
+
+import Table from '../../Table';
+import WidgetHeading from '../../WidgetHeading';
+
+import { columns, data } from './data';
+
+import './style.scss';
+
+const AllocationTableWidget = () => {
+ return (
+
+ );
+};
+
+export default AllocationTableWidget;
diff --git a/src/components/dashboard-page/Token/AllocationTableWidget/style.scss b/src/components/dashboard-page/Token/AllocationTableWidget/style.scss
new file mode 100644
index 000000000..0f3dcde64
--- /dev/null
+++ b/src/components/dashboard-page/Token/AllocationTableWidget/style.scss
@@ -0,0 +1,36 @@
+@import '../../../../styles/main';
+
+.dashboard-token-allocation-table-widget {
+ @include dashboard-widget;
+ @include reset-dashboard-widget-padding;
+
+ overflow-x: hidden;
+
+ &__heading {
+ @include dashboard-widget-heading-padding;
+ margin-bottom: 16px;
+
+ & .dashboard-widget-heading__info-wrapper {
+ top: 36px;
+ bottom: auto;
+ }
+ }
+
+ &__table-wrapper {
+ overflow-x: auto;
+ scrollbar-width: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ }
+
+ &__table {
+ min-width: 584px;
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ &__table {
+ min-width: auto;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Token/Exchange/index.js b/src/components/dashboard-page/Token/Exchange/index.js
new file mode 100644
index 000000000..7518a0d3c
--- /dev/null
+++ b/src/components/dashboard-page/Token/Exchange/index.js
@@ -0,0 +1,163 @@
+import React, { useMemo, useEffect, useState } from 'react';
+import cn from 'classnames';
+import { string, number, object, bool } from 'prop-types';
+
+import WidgetHeading from '../../WidgetHeading';
+import { ExchangeBlockSkeleton } from '../Skeletons';
+import Feature from '../../../Feature';
+import useDashboardMedia from '../../../../utils/useDashboardMedia';
+
+import { ReactComponent as ToggleButtonChevron } from '../../../../assets/svg/dashboard/toggle-button-chevron.svg';
+
+import { parseExchangeOptions, formatNumberWithCommas } from './utils';
+
+import './style.scss';
+
+const exchangeOptionPropTypes = {
+ logo: string.isRequired,
+ name: string.isRequired,
+ volume: number.isRequired,
+ depthUp2: number.isRequired,
+ depthDown2: number.isRequired,
+};
+
+export const ExchangeOption = ({ logo, name, volume, depthUp2, depthDown2 }) => {
+ return (
+
+
+
+
+
+
{name}
+
+
+
Volume (24h)
+
${formatNumberWithCommas(volume)}
+
+
+
+ );
+};
+
+ExchangeOption.propTypes = exchangeOptionPropTypes;
+
+const exchangePropTypes = {
+ data: object,
+ loading: bool,
+};
+
+const Exchange = ({ data, loading }) => {
+ const toggleOptionsVisibilityEnabled = false;
+
+ const parsedExchangeOptions = parseExchangeOptions(data);
+
+ const { currentBreakpoints } = useDashboardMedia();
+ const columnsCount = useMemo(() => {
+ switch (currentBreakpoints) {
+ case 'xxs':
+ return 1;
+ case 'xs':
+ return 2;
+ case 'sm':
+ return 3;
+ default:
+ return 4;
+ }
+ }, [currentBreakpoints]);
+ const placeholdersCount = useMemo(() => {
+ if (columnsCount === 1) {
+ return 0;
+ }
+ return parsedExchangeOptions.length % columnsCount === 0
+ ? 0
+ : columnsCount - (parsedExchangeOptions.length % columnsCount);
+ }, [parsedExchangeOptions, columnsCount]);
+
+ const totalCount = parsedExchangeOptions.length;
+ const initShownCount = useMemo(() => {
+ if (!toggleOptionsVisibilityEnabled) {
+ return totalCount;
+ }
+ switch (currentBreakpoints) {
+ case 'xxs':
+ case 'sm':
+ return 3;
+ default:
+ return 4;
+ }
+ }, [toggleOptionsVisibilityEnabled, totalCount, currentBreakpoints]);
+
+ useEffect(() => {
+ setShownCount(initShownCount);
+ }, [initShownCount]);
+
+ const [shownCount, setShownCount] = useState(initShownCount);
+ const toggleShownCount = () =>
+ setShownCount(prevShownCount => (prevShownCount === initShownCount ? totalCount : initShownCount));
+ const shownExchangeOptions = useMemo(() => parsedExchangeOptions.slice(0, shownCount), [
+ shownCount,
+ parsedExchangeOptions,
+ ]);
+
+ const exchangeOptionsExpanded = useMemo(() => (toggleOptionsVisibilityEnabled ? shownCount === totalCount : true), [
+ toggleOptionsVisibilityEnabled,
+ shownCount,
+ totalCount,
+ ]);
+
+ return (
+
+
+
+ {loading ? (
+
+ ) : (
+ <>
+
+ {shownExchangeOptions.map((exchangeOption, index) => {
+ return
;
+ })}
+ {exchangeOptionsExpanded &&
+ Array.from({ length: placeholdersCount }, (_, i) => {
+ return
;
+ })}
+
+
+
+ {totalCount > initShownCount && (
+
+ {`${exchangeOptionsExpanded ? 'Hide' : 'Show'} more exchanges`}
+
+
+ )}
+
+ >
+ )}
+
+ );
+};
+
+Exchange.propTypes = exchangePropTypes;
+
+export default Exchange;
diff --git a/src/components/dashboard-page/Token/Exchange/style.scss b/src/components/dashboard-page/Token/Exchange/style.scss
new file mode 100644
index 000000000..7d78aa752
--- /dev/null
+++ b/src/components/dashboard-page/Token/Exchange/style.scss
@@ -0,0 +1,204 @@
+@import '../../../../styles/main';
+
+.dashboard-token-exchange {
+ margin-top: 16px;
+
+ &__heading {
+ margin-bottom: 8px;
+
+ & .dashboard-widget-heading__info-wrapper {
+ right: 0;
+
+ @media #{$screen-min-dashboard-xs} {
+ right: -53px;
+ }
+ }
+ }
+
+ &__options {
+ display: grid;
+ gap: 16px;
+ }
+
+ &__option {
+ height: 260px;
+ position: relative;
+ overflow: hidden;
+ padding: 16px 24px;
+ background-color: $dashboard-widget-base-background-color;
+ backdrop-filter: blur(2px);
+ border: 1px solid $dashboard-base-border-color;
+ border-radius: 8px;
+ }
+
+ &__option-inner-bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 156px;
+ background-position: center;
+ background-size: cover;
+ background-repeat: no-repeat;
+ filter: blur(7px);
+ z-index: -1;
+ }
+
+ &__option-logo-name-wrapper {
+ margin-bottom: 24px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ }
+
+ &__option-logo {
+ width: 64px;
+ }
+
+ &__option-name {
+ @include h500;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__option-volume-depth {
+ @include t100;
+ font-weight: 700;
+ color: $dashboard-base-gray-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__option-volume {
+ margin-bottom: 16px;
+ @include h500;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__option-depths-list {
+ padding: 0;
+ list-style: none;
+ display: flex;
+ gap: 32px;
+ }
+
+ &__option-depths-list-item {
+ margin: 0;
+ padding: 0;
+ }
+
+ &__option-depth {
+ @include t300;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__option-placeholder {
+ height: 260px;
+ background-color: $dashboard-widget-base-background-color;
+ opacity: 0.8;
+ border-radius: 8px;
+ backdrop-filter: blur(2px);
+ }
+
+ &__button-toggle-shown-options {
+ height: 48px;
+ margin: 16px auto 0;
+ padding: 12px 20px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ @include t300;
+ font-weight: 600;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ background-color: $dashboard-exchange-button-background-color;
+ border: 1px solid $dashboard-base-borders-color;
+ border-radius: 2px;
+ backdrop-filter: blur(2px);
+ cursor: pointer;
+ transition: all $dashboard-transition-duration $dashboard-transition-timing;
+
+ &.options-expanded > svg {
+ transform: rotate(180deg);
+ }
+
+ &:hover,
+ &:focus {
+ background-color: $dashboard-buttons-base-hover-background-color;
+ }
+ &:active {
+ background-color: $dashboard-buttons-base-pressed-background-color;
+ }
+ }
+
+ @media #{$screen-min-dashboard-xs} {
+ &__options {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ &__option {
+ height: 248px;
+ // padding: 16px;
+ padding-block: 16px;
+ padding-inline: 13px;
+ }
+
+ &__option-volume {
+ @include h400;
+ }
+
+ &__option-depth {
+ @include t200;
+ }
+
+ &__option-placeholder {
+ height: 248px;
+ }
+ }
+
+ @media only screen and (min-width: 445px) {
+ &__option {
+ padding-inline: 16px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__options {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ &__option {
+ height: 300px;
+ padding: 24px;
+ }
+
+ &__option-logo {
+ width: 88px;
+ }
+
+ &__option-volume {
+ @include h500;
+ }
+
+ &__option-depth {
+ @include t300;
+ }
+
+ &__option-placeholder {
+ height: 300px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ &__options {
+ gap: 24px;
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ & {
+ margin-top: 24px;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Token/Exchange/utils.js b/src/components/dashboard-page/Token/Exchange/utils.js
new file mode 100644
index 000000000..72fbc2e59
--- /dev/null
+++ b/src/components/dashboard-page/Token/Exchange/utils.js
@@ -0,0 +1,43 @@
+import mexcLogo from '../../../../assets/images/dashboard/mexc-logo.png';
+import bitgetLogo from '../../../../assets/images/dashboard/bitget-logo.png';
+import gateIoLogo from '../../../../assets/images/dashboard/gatel-o-logo.png';
+import bitmartLogo from '../../../../assets/images/dashboard/bitmart-logo.png';
+import biconomyLogo from '../../../../assets/images/dashboard/biconomy-logo.png';
+import xtLogo from '../../../../assets/images/dashboard/xt-logo.png';
+
+export const formatNumberWithCommas = num => num.toLocaleString('en-US');
+
+const exchangeOptionsLogos = {
+ bitget: bitgetLogo,
+ gate: gateIoLogo,
+ bitmart: bitmartLogo,
+ mxc: mexcLogo,
+ biconomy: biconomyLogo,
+ xt: xtLogo,
+};
+
+const exchangeOptionsLabels = {
+ bitget: 'Bitget',
+ gate: 'GateIO',
+ bitmart: 'BitMart',
+ mxc: 'MEXC',
+ xt: 'XT',
+ biconomy: 'Biconomy',
+};
+
+export const parseExchangeOptions = (data = {}) => {
+ const exchangeOptions = [];
+
+ const keys = Object.keys(data);
+ for (const key of keys) {
+ exchangeOptions.push({
+ logo: exchangeOptionsLogos[key],
+ name: exchangeOptionsLabels[key],
+ volume: data[key].volume,
+ depthUp2: Math.round(data[key].plus2PercentDepth),
+ depthDown2: Math.round(data[key].minus2PercentDepth),
+ });
+ }
+
+ return exchangeOptions;
+};
diff --git a/src/components/dashboard-page/Token/MintingChart/index.js b/src/components/dashboard-page/Token/MintingChart/index.js
new file mode 100644
index 000000000..309deeb3c
--- /dev/null
+++ b/src/components/dashboard-page/Token/MintingChart/index.js
@@ -0,0 +1,170 @@
+import React, { useState } from 'react';
+import cn from 'classnames';
+import { ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
+import { arrayOf, shape, string, number, func, bool } from 'prop-types';
+
+import ChartWrapper from '../../ChartWrapper';
+import useDashboardMedia from '../../../../utils/useDashboardMedia';
+
+import './style.scss';
+
+const dashboardTokenMintingChartPropTypes = {
+ data: arrayOf(
+ shape({
+ pie: string,
+ value: number,
+ label: string,
+ fill: string,
+ })
+ ),
+ withLabelsHidden: bool,
+};
+
+const DashboardTokenMintingChart = ({ data, withLabelsHidden }) => {
+ const { currentBreakpoints } = useDashboardMedia();
+ const [activeCellName, setActiveCellName] = useState('');
+
+ const onCellMouseEnter = event => {
+ const cellName = event.currentTarget.getAttribute('name');
+ setActiveCellName(cellName);
+ };
+
+ const onCellMouseLeave = () => {
+ setActiveCellName('');
+ };
+
+ const shouldBeDim = entry => !!activeCellName && entry.pie !== activeCellName;
+
+ return (
+ <>
+
+
+
+
+ withLabelsHidden
+ ? null
+ : renderCustomLabel(pieLabelProps, setActiveCellName, shouldBeDim, currentBreakpoints)
+ }
+ labelLine={false}
+ isAnimationActive={false}
+ animationDuration={0}
+ >
+ {data.map((entry, index) => {
+ return (
+ |
+ );
+ })}
+
+
+
+
+
+ >
+ );
+};
+
+DashboardTokenMintingChart.propTypes = dashboardTokenMintingChartPropTypes;
+
+function renderCustomLabel(pieLabelProps, setActiveCellName, shouldBeDim, currentBreakpoints) {
+ const isXxs = currentBreakpoints === 'xxs';
+
+ const getX = pieLabelProps => {
+ switch (pieLabelProps.name) {
+ case 'workersRewards':
+ return pieLabelProps.x - (isXxs ? 35 : 40);
+ default:
+ return pieLabelProps.x;
+ }
+ };
+
+ const getY = pieLabelProps => {
+ switch (pieLabelProps.name) {
+ case 'validatorRewards':
+ return pieLabelProps.y - 10;
+ case 'spendingProposals':
+ return pieLabelProps.y + 12;
+ default:
+ return pieLabelProps.y;
+ }
+ };
+
+ return (
+
setActiveCellName(pieLabelProps.name)} onMouseLeave={() => setActiveCellName('')}>
+
+ {`${(pieLabelProps.percent * 100)?.toFixed(2)}%`}
+
+
+ );
+}
+
+const customLegendPropTypes = {
+ data: arrayOf(
+ shape({
+ pie: string,
+ value: number,
+ label: string,
+ fill: string,
+ })
+ ),
+ shouldBeDim: func.isRequired,
+ setActiveCellName: func.isRequired,
+};
+
+function CustomLegend({ data, setActiveCellName, shouldBeDim }) {
+ const onCellBoxMouseEnter = cellName => {
+ setActiveCellName(cellName);
+ };
+
+ const onCellBoxMouseLeave = () => {
+ setActiveCellName('');
+ };
+
+ return (
+
+
+ {data.map(cell => {
+ return (
+
+ onCellBoxMouseEnter(cell.pie)}
+ onMouseLeave={onCellBoxMouseLeave}
+ >
+
+
{cell.label}
+
+
+ );
+ })}
+
+
+ );
+}
+
+CustomLegend.propTypes = customLegendPropTypes;
+
+export default DashboardTokenMintingChart;
diff --git a/src/components/dashboard-page/Token/MintingChart/style.scss b/src/components/dashboard-page/Token/MintingChart/style.scss
new file mode 100644
index 000000000..65d0a650d
--- /dev/null
+++ b/src/components/dashboard-page/Token/MintingChart/style.scss
@@ -0,0 +1,57 @@
+@import '../../../../styles/main';
+
+.dashboard-token-mintning-chart-custom-label {
+ @include t200;
+ font-size: 12px;
+ fill: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+
+ @media #{$screen-min-dashboard-xs} {
+ font-size: 14px;
+ }
+}
+
+.dim {
+ opacity: 0.4;
+}
+
+.dashboard-token-mintning-chart-legend {
+ &__cells-list {
+ padding: 0;
+ list-style: none;
+
+ display: flex;
+ flex-wrap: wrap;
+ gap: 24px;
+ }
+
+ &__cells-list-item {
+ margin: 0;
+ padding: 0;
+ }
+
+ &__cell {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ &__cell-bg {
+ width: 16px;
+ height: 16px;
+ border: 1px solid $dashboard-base-border-color;
+ border-radius: 4px;
+ }
+
+ &__cell-label {
+ @include t200;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ @media #{$screen-min-dashboard-lg} {
+ & {
+ margin-top: auto;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Token/MintingChartWidget/index.js b/src/components/dashboard-page/Token/MintingChartWidget/index.js
new file mode 100644
index 000000000..86a95bbe9
--- /dev/null
+++ b/src/components/dashboard-page/Token/MintingChartWidget/index.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import { object } from 'prop-types';
+
+import WidgetHeading, { DashboardWidgetAltHeading } from '../../WidgetHeading';
+import MintingChart from '../MintingChart';
+
+import { parseInflationPercentage, generateChartData } from './utils';
+
+import './style.scss';
+
+const propTypes = {
+ data: object,
+};
+
+const MintingChartWidget = ({ data }) => {
+ const parsedJoyAnnualInflation = parseInflationPercentage(data?.joyAnnualInflation);
+ const chartData = generateChartData(data?.tokenMintingData);
+
+ // When no tokenMintingData is provided labels are hidden because fallback values are inaccurate
+ const values = Object.values(data?.tokenMintingData || {});
+ const withLabelsHidden = !values.length || values.some(pie => !pie);
+
+ return (
+
+ );
+};
+
+MintingChartWidget.propTypes = propTypes;
+
+export default MintingChartWidget;
diff --git a/src/components/dashboard-page/Token/MintingChartWidget/style.scss b/src/components/dashboard-page/Token/MintingChartWidget/style.scss
new file mode 100644
index 000000000..a6d500742
--- /dev/null
+++ b/src/components/dashboard-page/Token/MintingChartWidget/style.scss
@@ -0,0 +1,25 @@
+@import '../../../../styles/main';
+
+.dashboard-token-minting-chart-widget {
+ @include dashboard-widget;
+ display: flex;
+ flex-direction: column;
+
+ &__heading {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 16px;
+ }
+
+ &__alt-heading {
+ & .dashboard-widget-heading__info-wrapper {
+ right: 0;
+
+ @media #{$screen-min-dashboard-md} {
+ right: -53px;
+ }
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Token/MintingChartWidget/utils.js b/src/components/dashboard-page/Token/MintingChartWidget/utils.js
new file mode 100644
index 000000000..060b7334d
--- /dev/null
+++ b/src/components/dashboard-page/Token/MintingChartWidget/utils.js
@@ -0,0 +1,40 @@
+import { isNaN } from '../../../../utils/withFallbackVal';
+
+export const parseInflationPercentage = val => {
+ if (isNaN(val)) {
+ return '0%';
+ }
+
+ return `${Math.round(val)}%`;
+};
+
+const round2Dec = num => Number(num?.toFixed(2));
+
+export const generateChartData = (data = {}) => {
+ return [
+ {
+ pie: 'creatorPayouts',
+ value: round2Dec(data.creatorPayoutsMintingPercentage || 35),
+ label: 'Creator payouts',
+ fill: '#6C6CFF',
+ },
+ {
+ pie: 'validatorRewards',
+ value: round2Dec(data.validatorMintingPercentage || 25),
+ label: 'Validator rewards',
+ fill: '#7D7EF8',
+ },
+ {
+ pie: 'workersRewards',
+ value: round2Dec(data.workerMintingPercentage || 21),
+ label: 'Workers rewards',
+ fill: '#9B9CF9',
+ },
+ {
+ pie: 'spendingProposals',
+ value: round2Dec(data.spendingProposalsMintingPercentage || 19),
+ label: 'Spending proposals',
+ fill: '#ACACFA',
+ },
+ ];
+};
diff --git a/src/components/dashboard-page/Token/PriceChart/index.js b/src/components/dashboard-page/Token/PriceChart/index.js
new file mode 100644
index 000000000..beefb030d
--- /dev/null
+++ b/src/components/dashboard-page/Token/PriceChart/index.js
@@ -0,0 +1,228 @@
+import React, { useRef, useState } from 'react';
+import {
+ ResponsiveContainer,
+ ComposedChart,
+ XAxis,
+ Text,
+ YAxis,
+ CartesianGrid,
+ Area,
+ Bar,
+ ReferenceLine,
+ Tooltip,
+} from 'recharts';
+import { arrayOf, shape, instanceOf, number } from 'prop-types';
+
+import ChartWrapper from '../../ChartWrapper';
+
+import {
+ formatXAxisTick,
+ renderCustomDot,
+ formatDateToShowInTooltip,
+ formatTimeToShowInTooltip,
+ renderCustomActiveDot,
+} from './utils';
+
+import './style.scss';
+
+const propTypes = {
+ data: arrayOf(shape({ date: instanceOf(Date).isRequired, price: number.isRequired, volume: number.isRequired })),
+};
+
+const PriceChart = ({ data }) => {
+ const maxBarSize = 20;
+
+ const cartesianGridRef = useRef(null);
+ const chartWidth = cartesianGridRef.current?.props.offset.width || 0;
+
+ /**
+ * Triggering re-render to obtain CartesianGrid ref value which is null on the first render.
+ */
+ const [key, setKey] = useState('0');
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/*
+ interval set to 0 to show all ticks
+ formatYAxisTick can accept locale as a second arg
+ */}
+ {
+ const formattedTick = formatXAxisTick(tickProps.payload.value);
+ return (
+
+ {formattedTick}
+
+ );
+ }}
+ />
+ {
+ return (
+
+ {tickProps.payload.value}
+
+ );
+ }}
+ />
+ }
+ content={tooltipContentProps => }
+ />
+ {
+ return renderCustomDot(areaChartProps, data.length);
+ }}
+ activeDot={areaChartActiveDotProps => {
+ return renderCustomActiveDot(areaChartActiveDotProps, chartWidth);
+ }}
+ // isAnimationActive={false}
+ onAnimationEnd={() => setKey('1')}
+ animationDuration={0}
+ />
+
+
+
+
+
+
+
+ );
+};
+
+PriceChart.propTypes = propTypes;
+
+function CustomTooltip(tooltipContentProps) {
+ const { active, payload } = tooltipContentProps;
+
+ if (active && payload && payload.length) {
+ return (
+
+
+
{formatDateToShowInTooltip(payload[0].payload.date)}
+
{formatTimeToShowInTooltip(payload[0].payload.date)}
+
+
+
+ );
+ }
+
+ return null;
+}
+
+function CustomCursor(tooltipCursorProps) {
+ const [points1] = tooltipCursorProps.points;
+
+ return (
+
+
+
+
+ {formatDateToShowInTooltip(tooltipCursorProps.payload[0].payload.date)}
+
+
+ {formatTimeToShowInTooltip(tooltipCursorProps.payload[0].payload.date)}
+
+
+ );
+}
+
+export default PriceChart;
diff --git a/src/components/dashboard-page/Token/PriceChart/prices.json b/src/components/dashboard-page/Token/PriceChart/prices.json
new file mode 100644
index 000000000..3a0931e68
--- /dev/null
+++ b/src/components/dashboard-page/Token/PriceChart/prices.json
@@ -0,0 +1,718 @@
+[
+ [
+ 1689465600000,
+ 0.002403749287049288
+ ],
+ [
+ 1689552000000,
+ 0.0026230537007539117
+ ],
+ [
+ 1689638400000,
+ 0.0029394610689109217
+ ],
+ [
+ 1689724800000,
+ 0.0039004908562720554
+ ],
+ [
+ 1689811200000,
+ 0.0048276740119147735
+ ],
+ [
+ 1689897600000,
+ 0.005423110378359478
+ ],
+ [
+ 1689984000000,
+ 0.006286287931361627
+ ],
+ [
+ 1690070400000,
+ 0.00537372344630757
+ ],
+ [
+ 1690156800000,
+ 0.005354974421615567
+ ],
+ [
+ 1690243200000,
+ 0.004902830791810857
+ ],
+ [
+ 1690329600000,
+ 0.005075262946017998
+ ],
+ [
+ 1690416000000,
+ 0.005160101274649808
+ ],
+ [
+ 1690502400000,
+ 0.005164269692317736
+ ],
+ [
+ 1690588800000,
+ 0.005211051261798137
+ ],
+ [
+ 1690675200000,
+ 0.005094757416127894
+ ],
+ [
+ 1690761600000,
+ 0.005093930574908745
+ ],
+ [
+ 1690848000000,
+ 0.005060021679776899
+ ],
+ [
+ 1690934400000,
+ 0.004706387272045649
+ ],
+ [
+ 1691020800000,
+ 0.0049287443154978705
+ ],
+ [
+ 1691107200000,
+ 0.005233360594993115
+ ],
+ [
+ 1691193600000,
+ 0.0053576032990753164
+ ],
+ [
+ 1691280000000,
+ 0.005613433795153593
+ ],
+ [
+ 1691366400000,
+ 0.006402829806024963
+ ],
+ [
+ 1691452800000,
+ 0.007325563141043553
+ ],
+ [
+ 1691539200000,
+ 0.00795738822199759
+ ],
+ [
+ 1691625600000,
+ 0.007956579080176048
+ ],
+ [
+ 1691712000000,
+ 0.008291904980542775
+ ],
+ [
+ 1691798400000,
+ 0.010458753824044232
+ ],
+ [
+ 1691884800000,
+ 0.009573825472019707
+ ],
+ [
+ 1691971200000,
+ 0.011142108965171461
+ ],
+ [
+ 1692057600000,
+ 0.011991135106169417
+ ],
+ [
+ 1692144000000,
+ 0.014755881917642131
+ ],
+ [
+ 1692230400000,
+ 0.016088283910739715
+ ],
+ [
+ 1692316800000,
+ 0.017685418461584182
+ ],
+ [
+ 1692403200000,
+ 0.014780806631339311
+ ],
+ [
+ 1692489600000,
+ 0.014455354758677432
+ ],
+ [
+ 1692576000000,
+ 0.01731671606595348
+ ],
+ [
+ 1692662400000,
+ 0.014727881408281707
+ ],
+ [
+ 1692748800000,
+ 0.013435424649234619
+ ],
+ [
+ 1692835200000,
+ 0.013023291960402025
+ ],
+ [
+ 1692921600000,
+ 0.013450789763750887
+ ],
+ [
+ 1693008000000,
+ 0.013410984422846568
+ ],
+ [
+ 1693094400000,
+ 0.013361840350147959
+ ],
+ [
+ 1693180800000,
+ 0.016068344706129625
+ ],
+ [
+ 1693267200000,
+ 0.01998878713807136
+ ],
+ [
+ 1693353600000,
+ 0.016099234587356907
+ ],
+ [
+ 1693440000000,
+ 0.016016241677700823
+ ],
+ [
+ 1693526400000,
+ 0.013853435165394307
+ ],
+ [
+ 1693612800000,
+ 0.01697252624274907
+ ],
+ [
+ 1693699200000,
+ 0.01609991616922879
+ ],
+ [
+ 1693785600000,
+ 0.012978881708522065
+ ],
+ [
+ 1693872000000,
+ 0.013527997592731338
+ ],
+ [
+ 1693958400000,
+ 0.013160493755918323
+ ],
+ [
+ 1694044800000,
+ 0.015269347840231769
+ ],
+ [
+ 1694131200000,
+ 0.01566082521270367
+ ],
+ [
+ 1694217600000,
+ 0.017230976671873458
+ ],
+ [
+ 1694304000000,
+ 0.016066581779682673
+ ],
+ [
+ 1694390400000,
+ 0.016083443422886718
+ ],
+ [
+ 1694476800000,
+ 0.01662028103898112
+ ],
+ [
+ 1694563200000,
+ 0.019658216418989627
+ ],
+ [
+ 1694649600000,
+ 0.021116947072976
+ ],
+ [
+ 1694736000000,
+ 0.02811185488150947
+ ],
+ [
+ 1694822400000,
+ 0.023614940559480303
+ ],
+ [
+ 1694908800000,
+ 0.02399864520428051
+ ],
+ [
+ 1694995200000,
+ 0.025606375810070584
+ ],
+ [
+ 1695081600000,
+ 0.025743503506352405
+ ],
+ [
+ 1695168000000,
+ 0.02519289722717402
+ ],
+ [
+ 1695254400000,
+ 0.02830686930147346
+ ],
+ [
+ 1695340800000,
+ 0.02808389502363692
+ ],
+ [
+ 1695427200000,
+ 0.028904899118416087
+ ],
+ [
+ 1695513600000,
+ 0.03667605242447073
+ ],
+ [
+ 1695600000000,
+ 0.03495149170191266
+ ],
+ [
+ 1695686400000,
+ 0.032915431419856436
+ ],
+ [
+ 1695772800000,
+ 0.031004445487255138
+ ],
+ [
+ 1695859200000,
+ 0.03440767787354204
+ ],
+ [
+ 1695945600000,
+ 0.033870412269202274
+ ],
+ [
+ 1696032000000,
+ 0.03666558620071114
+ ],
+ [
+ 1696118400000,
+ 0.035807994583700736
+ ],
+ [
+ 1696204800000,
+ 0.04148079456989206
+ ],
+ [
+ 1696291200000,
+ 0.044056790513759415
+ ],
+ [
+ 1696377600000,
+ 0.04688292496587872
+ ],
+ [
+ 1696464000000,
+ 0.04122528748915876
+ ],
+ [
+ 1696550400000,
+ 0.03825849278503077
+ ],
+ [
+ 1696636800000,
+ 0.03746198648872118
+ ],
+ [
+ 1696723200000,
+ 0.03725806866273032
+ ],
+ [
+ 1696809600000,
+ 0.0367285283080413
+ ],
+ [
+ 1696896000000,
+ 0.035665636855345864
+ ],
+ [
+ 1696982400000,
+ 0.030066886996040425
+ ],
+ [
+ 1697068800000,
+ 0.02639120949344975
+ ],
+ [
+ 1697155200000,
+ 0.02510382061095612
+ ],
+ [
+ 1697241600000,
+ 0.03002924144504968
+ ],
+ [
+ 1697328000000,
+ 0.03361027565072798
+ ],
+ [
+ 1697414400000,
+ 0.028660688872714373
+ ],
+ [
+ 1697500800000,
+ 0.030467250212436323
+ ],
+ [
+ 1697587200000,
+ 0.03152919547918971
+ ],
+ [
+ 1697673600000,
+ 0.02867834209803486
+ ],
+ [
+ 1697760000000,
+ 0.026882433778980547
+ ],
+ [
+ 1697846400000,
+ 0.026325439949353543
+ ],
+ [
+ 1697932800000,
+ 0.027843210416610087
+ ],
+ [
+ 1698019200000,
+ 0.02826533236893828
+ ],
+ [
+ 1698105600000,
+ 0.027810102137281044
+ ],
+ [
+ 1698192000000,
+ 0.027231990207960684
+ ],
+ [
+ 1698278400000,
+ 0.02444542963262164
+ ],
+ [
+ 1698364800000,
+ 0.024329081745316894
+ ],
+ [
+ 1698451200000,
+ 0.02189090351385071
+ ],
+ [
+ 1698537600000,
+ 0.021435260625273185
+ ],
+ [
+ 1698624000000,
+ 0.029059600252500928
+ ],
+ [
+ 1698710400000,
+ 0.029086877695856456
+ ],
+ [
+ 1698796800000,
+ 0.027599210549392007
+ ],
+ [
+ 1698883200000,
+ 0.025940153512624468
+ ],
+ [
+ 1698969600000,
+ 0.025206577144683048
+ ],
+ [
+ 1699056000000,
+ 0.025293651866029297
+ ],
+ [
+ 1699142400000,
+ 0.027576642976660563
+ ],
+ [
+ 1699228800000,
+ 0.027685309338940003
+ ],
+ [
+ 1699315200000,
+ 0.030015086465560576
+ ],
+ [
+ 1699401600000,
+ 0.03492898926890199
+ ],
+ [
+ 1699488000000,
+ 0.036115069723850104
+ ],
+ [
+ 1699574400000,
+ 0.0333806994544884
+ ],
+ [
+ 1699660800000,
+ 0.034367070323970565
+ ],
+ [
+ 1699747200000,
+ 0.03434551186479354
+ ],
+ [
+ 1699833600000,
+ 0.03356580433492285
+ ],
+ [
+ 1699920000000,
+ 0.03306948675412605
+ ],
+ [
+ 1700006400000,
+ 0.03206619911918483
+ ],
+ [
+ 1700092800000,
+ 0.032695073576768235
+ ],
+ [
+ 1700179200000,
+ 0.034259585362705486
+ ],
+ [
+ 1700265600000,
+ 0.03579681507895371
+ ],
+ [
+ 1700352000000,
+ 0.03715911732367493
+ ],
+ [
+ 1700438400000,
+ 0.04245193252093659
+ ],
+ [
+ 1700524800000,
+ 0.0532110040530789
+ ],
+ [
+ 1700611200000,
+ 0.04427291257319545
+ ],
+ [
+ 1700697600000,
+ 0.049899155549442055
+ ],
+ [
+ 1700784000000,
+ 0.046988275097291525
+ ],
+ [
+ 1700870400000,
+ 0.050730334515657054
+ ],
+ [
+ 1700956800000,
+ 0.0518622613328732
+ ],
+ [
+ 1701043200000,
+ 0.056299868101259254
+ ],
+ [
+ 1701129600000,
+ 0.052447969392417595
+ ],
+ [
+ 1701216000000,
+ 0.05944652741011785
+ ],
+ [
+ 1701302400000,
+ 0.056288537431765304
+ ],
+ [
+ 1701388800000,
+ 0.05471207450456362
+ ],
+ [
+ 1701475200000,
+ 0.054380483565571555
+ ],
+ [
+ 1701561600000,
+ 0.050491899520490366
+ ],
+ [
+ 1701648000000,
+ 0.04846365424943356
+ ],
+ [
+ 1701734400000,
+ 0.0484167716868272
+ ],
+ [
+ 1701820800000,
+ 0.04550130443052248
+ ],
+ [
+ 1701907200000,
+ 0.04442152403211948
+ ],
+ [
+ 1701993600000,
+ 0.04415009645948336
+ ],
+ [
+ 1702080000000,
+ 0.047341183582104755
+ ],
+ [
+ 1702166400000,
+ 0.04800873807770477
+ ],
+ [
+ 1702252800000,
+ 0.04820118987637915
+ ],
+ [
+ 1702339200000,
+ 0.05449841077121347
+ ],
+ [
+ 1702425600000,
+ 0.05051020845806704
+ ],
+ [
+ 1702512000000,
+ 0.049635176895987336
+ ],
+ [
+ 1702598400000,
+ 0.050073748822680654
+ ],
+ [
+ 1702684800000,
+ 0.05030720183862041
+ ],
+ [
+ 1702771200000,
+ 0.04784719426807474
+ ],
+ [
+ 1702857600000,
+ 0.04564719268036106
+ ],
+ [
+ 1702944000000,
+ 0.04209656779665743
+ ],
+ [
+ 1703030400000,
+ 0.04171696430438485
+ ],
+ [
+ 1703116800000,
+ 0.051227984313937616
+ ],
+ [
+ 1703203200000,
+ 0.045296835271883266
+ ],
+ [
+ 1703289600000,
+ 0.047387658206076794
+ ],
+ [
+ 1703376000000,
+ 0.042415189508634134
+ ],
+ [
+ 1703462400000,
+ 0.03951236648577742
+ ],
+ [
+ 1703548800000,
+ 0.04398363002313072
+ ],
+ [
+ 1703635200000,
+ 0.04620843857279482
+ ],
+ [
+ 1703721600000,
+ 0.051662974792883905
+ ],
+ [
+ 1703808000000,
+ 0.04468620956437801
+ ],
+ [
+ 1703894400000,
+ 0.04428239414981519
+ ],
+ [
+ 1703980800000,
+ 0.04611294282495205
+ ],
+ [
+ 1704067200000,
+ 0.04358478863001019
+ ],
+ [
+ 1704153600000,
+ 0.04507809018558449
+ ],
+ [
+ 1704240000000,
+ 0.04946136802639571
+ ],
+ [
+ 1704326400000,
+ 0.04459013233215187
+ ],
+ [
+ 1704412800000,
+ 0.04268974255309182
+ ],
+ [
+ 1704499200000,
+ 0.04458213712819918
+ ],
+ [
+ 1704585600000,
+ 0.04342631075182041
+ ],
+ [
+ 1704672000000,
+ 0.044106207970157164
+ ],
+ [
+ 1704758400000,
+ 0.045654383648141145
+ ],
+ [
+ 1704844800000,
+ 0.0450421630924218
+ ]
+]
\ No newline at end of file
diff --git a/src/components/dashboard-page/Token/PriceChart/style.scss b/src/components/dashboard-page/Token/PriceChart/style.scss
new file mode 100644
index 000000000..3afa0d7d1
--- /dev/null
+++ b/src/components/dashboard-page/Token/PriceChart/style.scss
@@ -0,0 +1,73 @@
+@import '../../../../styles//main';
+
+.custom-axis-tick {
+ @include t100;
+ color: $dashboard-charts-base-tick-color;
+ fill: $dashboard-charts-base-tick-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+}
+
+.custom-dot-text {
+ @include t100;
+ fill: #ffffff;
+ font-feature-settings: $dashboard-font-feature-settings;
+}
+
+.custom-cursor-text {
+ @include t100;
+ font-feature-settings: $dashboard-font-feature-settings;
+}
+
+.custom-tooltip {
+ padding: 8px;
+ width: 225px;
+ background-color: $dashboard-charts-custom-tooltip-background-color;
+ border-radius: 8px;
+
+ &.width-reduced {
+ width: fit-content;
+ }
+
+ &__header {
+ margin-bottom: 12px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ &__payload-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ }
+
+ &__payload-list-item {
+ margin: 0;
+ padding: 0;
+ display: flex;
+ align-items: center;
+
+ &:first-of-type {
+ margin-bottom: 8px;
+ }
+ }
+
+ &__text-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ }
+
+ &__text {
+ @include t100;
+ color: $dashboard-charts-base-tick-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ margin-right: 4px;
+ }
+
+ &__accent-text {
+ @include t100;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+}
diff --git a/src/components/dashboard-page/Token/PriceChart/utils.js b/src/components/dashboard-page/Token/PriceChart/utils.js
new file mode 100644
index 000000000..4ce5fc440
--- /dev/null
+++ b/src/components/dashboard-page/Token/PriceChart/utils.js
@@ -0,0 +1,128 @@
+import React from 'react';
+
+import fallbackPrices from './prices.json';
+import fallbackVolume from './volume.json';
+
+import { isNaN } from '../../../../utils/withFallbackVal';
+
+export const generateCoinMarketCapStats = (prices = fallbackPrices, volume = fallbackVolume) => {
+ const data = [];
+
+ // num of price points is equal to num of volume points (equal timeframes)
+ for (let i = 0; i < prices.length; i += 1) {
+ data.push({
+ date: new Date(prices[i][0]),
+ price: prices[i][1],
+ volume: volume[i][1],
+ scaledVolume: volume[i][1] / 20,
+ });
+ }
+
+ return data;
+};
+
+export const formatXAxisTick = (datestr, locale = 'en-US') => {
+ const date = new Date(datestr);
+ const day = date.getDate();
+
+ if (day === 1) {
+ return date.toLocaleString(locale, { month: 'short' });
+ }
+
+ if (day === 11 || day === 21) {
+ return day.toString();
+ }
+
+ return '';
+};
+
+export const renderCustomDot = (areaChartDotProps, dataLength) => {
+ if (areaChartDotProps.key !== `dot-${dataLength - 1}`) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ {Number(areaChartDotProps.value[1]).toFixed(3)}
+
+
+
+ );
+};
+
+export const formatDateToShowInTooltip = (datestr, locale = 'en-US') => {
+ const date = new Date(datestr);
+ const day = date.toLocaleString(locale, { day: '2-digit' });
+ const month = date.toLocaleString(locale, { month: 'short' });
+ const year = date.toLocaleString(locale, { year: 'numeric' });
+
+ return `${day} ${month} ${year}`;
+};
+
+export const formatTimeToShowInTooltip = (datestr, locale = 'en-US') => {
+ return datestr.toLocaleTimeString(locale);
+};
+
+export const renderCustomActiveDot = (areaChartActiveDotProps, chartWidth) => {
+ return (
+
+
+
+
+
+
+
+
+ {Number(areaChartActiveDotProps.payload.price).toFixed(3)}
+
+
+ );
+};
+
+export const parsePriceWeeklyChange = price => {
+ if (isNaN(price)) {
+ return '0% Last week';
+ }
+
+ const roundedPiceWeeklyChange = Math.round(price);
+ const roundedPiceWeeklyChangeWithSign =
+ roundedPiceWeeklyChange > 0 ? `+${roundedPiceWeeklyChange}%` : `${roundedPiceWeeklyChange}%`;
+
+ return `${roundedPiceWeeklyChangeWithSign} Last week`;
+};
diff --git a/src/components/dashboard-page/Token/PriceChart/volume.json b/src/components/dashboard-page/Token/PriceChart/volume.json
new file mode 100644
index 000000000..e668357c9
--- /dev/null
+++ b/src/components/dashboard-page/Token/PriceChart/volume.json
@@ -0,0 +1,718 @@
+[
+ [
+ 1689465600000,
+ 37380.11054744234
+ ],
+ [
+ 1689552000000,
+ 57585.8133776156
+ ],
+ [
+ 1689638400000,
+ 71112.47116683479
+ ],
+ [
+ 1689724800000,
+ 79364.27357466579
+ ],
+ [
+ 1689811200000,
+ 59640.239900243025
+ ],
+ [
+ 1689897600000,
+ 76137.41595871303
+ ],
+ [
+ 1689984000000,
+ 65881.95515193448
+ ],
+ [
+ 1690070400000,
+ 55828.81611410621
+ ],
+ [
+ 1690156800000,
+ 67348.29450848063
+ ],
+ [
+ 1690243200000,
+ 73392.30591825716
+ ],
+ [
+ 1690329600000,
+ 73893.4363703377
+ ],
+ [
+ 1690416000000,
+ 62421.97138987962
+ ],
+ [
+ 1690502400000,
+ 68379.45643403169
+ ],
+ [
+ 1690588800000,
+ 73632.77371476065
+ ],
+ [
+ 1690675200000,
+ 73085.35983675947
+ ],
+ [
+ 1690761600000,
+ 80484.54345385637
+ ],
+ [
+ 1690848000000,
+ 76901.67904717663
+ ],
+ [
+ 1690934400000,
+ 124013.79149416614
+ ],
+ [
+ 1691020800000,
+ 117371.92141589534
+ ],
+ [
+ 1691107200000,
+ 74858.70566517649
+ ],
+ [
+ 1691193600000,
+ 91510.14834806883
+ ],
+ [
+ 1691280000000,
+ 91499.35066593415
+ ],
+ [
+ 1691366400000,
+ 92230.73084149597
+ ],
+ [
+ 1691452800000,
+ 90344.7367522947
+ ],
+ [
+ 1691539200000,
+ 110981.85387357305
+ ],
+ [
+ 1691625600000,
+ 82111.7025238478
+ ],
+ [
+ 1691712000000,
+ 99175.43789497996
+ ],
+ [
+ 1691798400000,
+ 124235.27515198827
+ ],
+ [
+ 1691884800000,
+ 113746.70382108602
+ ],
+ [
+ 1691971200000,
+ 81431.78682463993
+ ],
+ [
+ 1692057600000,
+ 88687.67836620547
+ ],
+ [
+ 1692144000000,
+ 174747.18470501062
+ ],
+ [
+ 1692230400000,
+ 159355.33779590618
+ ],
+ [
+ 1692316800000,
+ 241108.7412116671
+ ],
+ [
+ 1692403200000,
+ 103211.93993367895
+ ],
+ [
+ 1692489600000,
+ 87584.91353283991
+ ],
+ [
+ 1692576000000,
+ 97016.13203147524
+ ],
+ [
+ 1692662400000,
+ 87605.89409514323
+ ],
+ [
+ 1692748800000,
+ 108196.04127214385
+ ],
+ [
+ 1692835200000,
+ 80289.58509676623
+ ],
+ [
+ 1692921600000,
+ 112806.07074822676
+ ],
+ [
+ 1693008000000,
+ 95051.11035398432
+ ],
+ [
+ 1693094400000,
+ 91045.41846737635
+ ],
+ [
+ 1693180800000,
+ 126751.49628807658
+ ],
+ [
+ 1693267200000,
+ 148630.0164636525
+ ],
+ [
+ 1693353600000,
+ 130285.27630462007
+ ],
+ [
+ 1693440000000,
+ 147708.53560168456
+ ],
+ [
+ 1693526400000,
+ 132891.07102603695
+ ],
+ [
+ 1693612800000,
+ 146850.4261284493
+ ],
+ [
+ 1693699200000,
+ 125778.80669341184
+ ],
+ [
+ 1693785600000,
+ 155592.40994736837
+ ],
+ [
+ 1693872000000,
+ 100252.03687299542
+ ],
+ [
+ 1693958400000,
+ 139282.82831218812
+ ],
+ [
+ 1694044800000,
+ 178500.25296516737
+ ],
+ [
+ 1694131200000,
+ 159680.31405457613
+ ],
+ [
+ 1694217600000,
+ 118417.74898555511
+ ],
+ [
+ 1694304000000,
+ 153083.85711173806
+ ],
+ [
+ 1694390400000,
+ 120331.89221887213
+ ],
+ [
+ 1694476800000,
+ 196699.35326505502
+ ],
+ [
+ 1694563200000,
+ 210830.72075945692
+ ],
+ [
+ 1694649600000,
+ 153049.90356835863
+ ],
+ [
+ 1694736000000,
+ 298893.4576331535
+ ],
+ [
+ 1694822400000,
+ 166765.27520179615
+ ],
+ [
+ 1694908800000,
+ 133578.48205282958
+ ],
+ [
+ 1694995200000,
+ 185974.11708196104
+ ],
+ [
+ 1695081600000,
+ 155746.9667037046
+ ],
+ [
+ 1695168000000,
+ 159255.8774938111
+ ],
+ [
+ 1695254400000,
+ 212394.42545371663
+ ],
+ [
+ 1695340800000,
+ 112457.09377237491
+ ],
+ [
+ 1695427200000,
+ 150078.73353607015
+ ],
+ [
+ 1695513600000,
+ 372606.8315048145
+ ],
+ [
+ 1695600000000,
+ 188151.7376257603
+ ],
+ [
+ 1695686400000,
+ 249930.70936858212
+ ],
+ [
+ 1695772800000,
+ 117013.56270828347
+ ],
+ [
+ 1695859200000,
+ 303030.440139283
+ ],
+ [
+ 1695945600000,
+ 94038.2993420375
+ ],
+ [
+ 1696032000000,
+ 178830.4246032882
+ ],
+ [
+ 1696118400000,
+ 132723.12077460747
+ ],
+ [
+ 1696204800000,
+ 156192.73295405132
+ ],
+ [
+ 1696291200000,
+ 149298.91638519423
+ ],
+ [
+ 1696377600000,
+ 345966.6230034496
+ ],
+ [
+ 1696464000000,
+ 123415.90281286852
+ ],
+ [
+ 1696550400000,
+ 137413.5599464598
+ ],
+ [
+ 1696636800000,
+ 146991.34885752323
+ ],
+ [
+ 1696723200000,
+ 108824.88931478828
+ ],
+ [
+ 1696809600000,
+ 126626.33160913317
+ ],
+ [
+ 1696896000000,
+ 212198.459338105
+ ],
+ [
+ 1696982400000,
+ 79342.22002841216
+ ],
+ [
+ 1697068800000,
+ 149611.03848489668
+ ],
+ [
+ 1697155200000,
+ 109541.46739890586
+ ],
+ [
+ 1697241600000,
+ 161650.5273374734
+ ],
+ [
+ 1697328000000,
+ 97312.56950930877
+ ],
+ [
+ 1697414400000,
+ 98738.56836607427
+ ],
+ [
+ 1697500800000,
+ 142940.40824025805
+ ],
+ [
+ 1697587200000,
+ 136701.01964931106
+ ],
+ [
+ 1697673600000,
+ 139489.22220877555
+ ],
+ [
+ 1697760000000,
+ 146115.49585076058
+ ],
+ [
+ 1697846400000,
+ 115389.13555910187
+ ],
+ [
+ 1697932800000,
+ 115214.63196909428
+ ],
+ [
+ 1698019200000,
+ 160958.47365695986
+ ],
+ [
+ 1698105600000,
+ 126650.68862657159
+ ],
+ [
+ 1698192000000,
+ 304481.19949557446
+ ],
+ [
+ 1698278400000,
+ 94456.35246870725
+ ],
+ [
+ 1698364800000,
+ 269588.86952244525
+ ],
+ [
+ 1698451200000,
+ 251834.36245305432
+ ],
+ [
+ 1698537600000,
+ 271859.65663891606
+ ],
+ [
+ 1698624000000,
+ 364460.59532569675
+ ],
+ [
+ 1698710400000,
+ 252524.2929764031
+ ],
+ [
+ 1698796800000,
+ 203832.25304125162
+ ],
+ [
+ 1698883200000,
+ 319217.10610618035
+ ],
+ [
+ 1698969600000,
+ 248501.62123293328
+ ],
+ [
+ 1699056000000,
+ 316487.7046102427
+ ],
+ [
+ 1699142400000,
+ 282535.3569926999
+ ],
+ [
+ 1699228800000,
+ 308317.95520994544
+ ],
+ [
+ 1699315200000,
+ 274475.73163920746
+ ],
+ [
+ 1699401600000,
+ 311068.1540544093
+ ],
+ [
+ 1699488000000,
+ 340180.63230492367
+ ],
+ [
+ 1699574400000,
+ 324930.26645046595
+ ],
+ [
+ 1699660800000,
+ 250542.65052201136
+ ],
+ [
+ 1699747200000,
+ 249773.68298451637
+ ],
+ [
+ 1699833600000,
+ 268408.8131537027
+ ],
+ [
+ 1699920000000,
+ 256591.6917489089
+ ],
+ [
+ 1700006400000,
+ 290612.7036728708
+ ],
+ [
+ 1700092800000,
+ 351641.24215835513
+ ],
+ [
+ 1700179200000,
+ 330482.3902242282
+ ],
+ [
+ 1700265600000,
+ 312780.2119449469
+ ],
+ [
+ 1700352000000,
+ 616095.1644229831
+ ],
+ [
+ 1700438400000,
+ 448878.08166690666
+ ],
+ [
+ 1700524800000,
+ 953222.6707208449
+ ],
+ [
+ 1700611200000,
+ 882672.6208891775
+ ],
+ [
+ 1700697600000,
+ 599770.7534641664
+ ],
+ [
+ 1700784000000,
+ 522279.6434542416
+ ],
+ [
+ 1700870400000,
+ 460760.59496674105
+ ],
+ [
+ 1700956800000,
+ 458327.74860743654
+ ],
+ [
+ 1701043200000,
+ 528638.7542352431
+ ],
+ [
+ 1701129600000,
+ 517562.1673126066
+ ],
+ [
+ 1701216000000,
+ 402996.43057764554
+ ],
+ [
+ 1701302400000,
+ 288623.64560636505
+ ],
+ [
+ 1701388800000,
+ 210650.98723090673
+ ],
+ [
+ 1701475200000,
+ 327847.14543817705
+ ],
+ [
+ 1701561600000,
+ 431473.7768749786
+ ],
+ [
+ 1701648000000,
+ 400723.84749886475
+ ],
+ [
+ 1701734400000,
+ 448636.9253491802
+ ],
+ [
+ 1701820800000,
+ 393633.6418378756
+ ],
+ [
+ 1701907200000,
+ 349821.3896734906
+ ],
+ [
+ 1701993600000,
+ 319485.694722151
+ ],
+ [
+ 1702080000000,
+ 440309.0350760624
+ ],
+ [
+ 1702166400000,
+ 434778.82565701613
+ ],
+ [
+ 1702252800000,
+ 420635.9374437579
+ ],
+ [
+ 1702339200000,
+ 996810.6389759185
+ ],
+ [
+ 1702425600000,
+ 393118.35550506186
+ ],
+ [
+ 1702512000000,
+ 469405.8964970261
+ ],
+ [
+ 1702598400000,
+ 429696.4168070741
+ ],
+ [
+ 1702684800000,
+ 505105.8480787296
+ ],
+ [
+ 1702771200000,
+ 448491.40548890847
+ ],
+ [
+ 1702857600000,
+ 302128.07593771373
+ ],
+ [
+ 1702944000000,
+ 487637.95245405904
+ ],
+ [
+ 1703030400000,
+ 385711.0187598367
+ ],
+ [
+ 1703116800000,
+ 624352.6323805057
+ ],
+ [
+ 1703203200000,
+ 284848.07020716753
+ ],
+ [
+ 1703289600000,
+ 334852.9107089477
+ ],
+ [
+ 1703376000000,
+ 309343.6525877646
+ ],
+ [
+ 1703462400000,
+ 434473.81481239107
+ ],
+ [
+ 1703548800000,
+ 310486.93837636185
+ ],
+ [
+ 1703635200000,
+ 516209.7228166347
+ ],
+ [
+ 1703721600000,
+ 558396.2819024139
+ ],
+ [
+ 1703808000000,
+ 987323.113148558
+ ],
+ [
+ 1703894400000,
+ 368273.2009678718
+ ],
+ [
+ 1703980800000,
+ 424077.5250998653
+ ],
+ [
+ 1704067200000,
+ 367250.45358823857
+ ],
+ [
+ 1704153600000,
+ 366037.01112289436
+ ],
+ [
+ 1704240000000,
+ 508430.46440207656
+ ],
+ [
+ 1704326400000,
+ 438163.73632458533
+ ],
+ [
+ 1704412800000,
+ 355114.07929143077
+ ],
+ [
+ 1704499200000,
+ 650432.5557961204
+ ],
+ [
+ 1704585600000,
+ 410907.87743849703
+ ],
+ [
+ 1704672000000,
+ 374582.9087458903
+ ],
+ [
+ 1704758400000,
+ 334920.26671658765
+ ],
+ [
+ 1704844800000,
+ 348070.68404682196
+ ]
+]
\ No newline at end of file
diff --git a/src/components/dashboard-page/Token/PriceChartWidget/index.js b/src/components/dashboard-page/Token/PriceChartWidget/index.js
new file mode 100644
index 000000000..e4df20c43
--- /dev/null
+++ b/src/components/dashboard-page/Token/PriceChartWidget/index.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import cn from 'classnames';
+import { string, object } from 'prop-types';
+
+import WidgetHeading from '../../WidgetHeading';
+import PriceChart from '../PriceChart';
+
+import { generateCoinMarketCapStats, parsePriceWeeklyChange } from '../PriceChart/utils';
+
+import './style.scss';
+
+const propTypes = {
+ widgetCn: string,
+ data: object,
+};
+
+const PriceChartWidget = ({ widgetCn, data }) => {
+ const chartData = generateCoinMarketCapStats(data?.longTermPriceData, data?.longTermVolumeData);
+ const currentPrice = Number(chartData.at(-1)?.price)?.toFixed(6);
+ const currentPriceWithCurrency = `$${currentPrice}`;
+ const growthRate = parsePriceWeeklyChange(data?.priceWeeklyChange);
+
+ return (
+
+
+
{currentPriceWithCurrency}
+
{growthRate}
+
+
+ );
+};
+
+PriceChartWidget.propTypes = propTypes;
+
+export default PriceChartWidget;
diff --git a/src/components/dashboard-page/Token/PriceChartWidget/style.scss b/src/components/dashboard-page/Token/PriceChartWidget/style.scss
new file mode 100644
index 000000000..c9ba8dbc2
--- /dev/null
+++ b/src/components/dashboard-page/Token/PriceChartWidget/style.scss
@@ -0,0 +1,33 @@
+@import '../../../../styles/main';
+
+.dashboard-token-price-chart-widget {
+ @include dashboard-widget;
+ padding-right: 0;
+
+ &__current-price {
+ @include h600;
+ color: $dashboard-base-white-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__growth-rate {
+ @include dashboard-widget-helper-text;
+ margin-bottom: 16px;
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ & {
+ padding-right: 0px;
+ }
+
+ &__current-price {
+ @include h700;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ &__current-price {
+ @include h800;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Token/ReleaseScheduleChart/index.js b/src/components/dashboard-page/Token/ReleaseScheduleChart/index.js
new file mode 100644
index 000000000..83e6e53c7
--- /dev/null
+++ b/src/components/dashboard-page/Token/ReleaseScheduleChart/index.js
@@ -0,0 +1,268 @@
+import React, { useEffect, useState } from 'react';
+import {
+ ResponsiveContainer,
+ AreaChart,
+ XAxis,
+ YAxis,
+ Text,
+ Area,
+ CartesianGrid,
+ ReferenceLine,
+ Tooltip,
+} from 'recharts';
+import cn from 'classnames';
+import { useMediaQuery } from 'react-responsive';
+import { arrayOf, objectOf, string } from 'prop-types';
+
+import {
+ generateChartData,
+ formatXAxisTick,
+ formatYAxisTick,
+ renderCustomLabel,
+ areasLabels,
+ areasPalette,
+ getMonthsSinceLaunch,
+ getHighlightedDate,
+} from './utils';
+
+import './style.scss';
+
+const ReleaseScheduleChart = () => {
+ const [chartData] = useState(() => generateChartData());
+ const areas = Object.keys(chartData[0]).filter(key => key !== 'month');
+
+ const [activeAreaName, setActiveAreaName] = useState('');
+
+ const [isXxsScreen, setIsXxsScreen] = useState(false);
+ const isXxs = useMediaQuery({ maxWidth: 424 });
+ useEffect(() => {
+ setIsXxsScreen(isXxs);
+ }, [isXxs]);
+
+ const xAxisDataKey = 'month';
+ const xAxisValues = chartData.map(val => val.month);
+
+ const monthsSinceLaunch = getMonthsSinceLaunch();
+ const maxXAxisVal = 24;
+
+ return (
+
+
+ setActiveAreaName('')}>
+
+ {
+ const isLast = xAxisValues.indexOf(tickProps.payload.value) === xAxisValues.length - 1;
+ const formattedTick = formatXAxisTick(tickProps.payload.value, isLast);
+ return (
+
+ {formattedTick}
+
+ );
+ }}
+ interval={0}
+ tickLine={false}
+ tickMargin={16}
+ />
+ {
+ const formattedTick = formatYAxisTick(tickProps.payload.value);
+ return (
+
+ {formattedTick}
+
+ );
+ }}
+ ticks={[0, 25, 50, 75, 100]}
+ domain={[0, 100]}
+ tickLine={false}
+ tickMargin={28}
+ axisLine={{ stroke: '#BBD9F621' }}
+ />
+ {areas.map(area => {
+ return (
+ {
+ setActiveAreaName(area.name);
+ }}
+ isAnimationActive={false}
+ animationDuration={0}
+ />
+ );
+ })}
+ {monthsSinceLaunch <= maxXAxisVal && (
+
+ )}
+ }
+ cursor={ }
+ wrapperStyle={{ top: '-75px' }}
+ position={isXxsScreen ? { x: 0 } : undefined}
+ />
+
+
+
+
+ );
+};
+
+function CustomTooltip(tooltipContentProps) {
+ const { active, payload } = tooltipContentProps;
+
+ if (active && payload && payload.length) {
+ const innerPayload = payload[0].payload;
+ const innerPayloadAreasKeys = Object.keys(innerPayload).filter(key => key !== 'month');
+
+ const activeAreaName = tooltipContentProps.activeAreaName;
+
+ return (
+
+
+
{getHighlightedDate(innerPayload.month)}
+
{`${innerPayload.month}th month`}
+
+
+ {innerPayloadAreasKeys.map(areaKey => {
+ return (
+
+
+
+
+ {`${areasLabels[areaKey]}: `}
+ {/* eslint-disable-next-line max-len */}
+ {`${innerPayload[areaKey]}%`}
+
+
+
+ );
+ })}
+
+
+ );
+ }
+
+ return null;
+}
+
+function CustomCursor(tooltipCursorProps) {
+ const [points1] = tooltipCursorProps.points;
+
+ const innerPayload = tooltipCursorProps.payload[0].payload;
+
+ return (
+
+
+
+
+ {getHighlightedDate(innerPayload.month)}
+
+
+ {`${innerPayload.month}th month`}
+
+
+ );
+}
+
+const customLegendPropTypes = {
+ areas: arrayOf(string).isRequired,
+ areasLabels: objectOf(string).isRequired,
+ areasPalette: objectOf(string).isRequired,
+};
+
+function CustomLegend({ areas, areasLabels, areasPalette, activeAreaName, setActiveAreaName }) {
+ return (
+
+
+
Months after launch date: 09 Dec 2022
+
+
+ {areas.map(area => {
+ // areaKey should be unique
+ return (
+
+ setActiveAreaName(area)}
+ onMouseLeave={() => setActiveAreaName('')}
+ >
+
+
{areasLabels[area]}
+
+
+ );
+ })}
+
+
+ );
+}
+
+CustomLegend.propTypes = customLegendPropTypes;
+
+export default ReleaseScheduleChart;
diff --git a/src/components/dashboard-page/Token/ReleaseScheduleChart/style.scss b/src/components/dashboard-page/Token/ReleaseScheduleChart/style.scss
new file mode 100644
index 000000000..c8cb9aaf5
--- /dev/null
+++ b/src/components/dashboard-page/Token/ReleaseScheduleChart/style.scss
@@ -0,0 +1,102 @@
+@import '../../../../styles/main';
+
+.recharts-surface {
+ overflow: visible;
+}
+
+.token-release-schedule-chart {
+ &__areas-list {
+ padding: 0;
+ list-style: none;
+ }
+
+ &__areas-list-item {
+ margin: 0;
+ padding: 0;
+ }
+}
+
+.area {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ &.dim {
+ opacity: 0.4;
+ }
+}
+
+.token-release-schedule-chart-legend {
+ margin-top: 8px;
+
+ &__notice {
+ padding: 4px 8px;
+ margin-bottom: 32px;
+ margin-inline: auto;
+ width: fit-content;
+ text-align: center;
+ background-color: $dashboard-widget-base-background-color;
+ border-radius: 4px;
+ }
+
+ &__notice-text {
+ @include t100;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__areas-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 24px;
+ }
+
+ &__area-bg {
+ width: 16px;
+ height: 16px;
+ border: 1px solid $dashboard-base-border-color;
+ border-radius: 4px;
+ }
+
+ &__area-label {
+ @include t200;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+}
+
+.reference-line-custom-label {
+ @include t100;
+ fill: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+}
+
+.token-release-schedule-chart-tooltip {
+ padding: 8px;
+ // width: 256px;
+ background-color: $dashboard-charts-custom-tooltip-background-color;
+ border-radius: 8px;
+
+ &__header {
+ margin-bottom: 16px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ &__areas-list-item {
+ &:not(:last-of-type) {
+ margin-bottom: 8px;
+ }
+ }
+
+ &__text {
+ @include t100;
+ color: $dashboard-charts-tooltip-gray-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+
+ &.accent {
+ color: $dashboard-content-base-text-color;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Token/ReleaseScheduleChart/utils.js b/src/components/dashboard-page/Token/ReleaseScheduleChart/utils.js
new file mode 100644
index 000000000..cdd40bb6e
--- /dev/null
+++ b/src/components/dashboard-page/Token/ReleaseScheduleChart/utils.js
@@ -0,0 +1,289 @@
+import React from 'react';
+
+export const areasLabels = {
+ communityFoundingMembers: 'Community Founding Members',
+ reserved1: 'Reserved 1',
+ reserved2: 'Reserved 2',
+ strategicPartners: 'Strategic Partners',
+ membershipAirdrop: 'Membership Airdrop',
+ investors: 'Investors',
+ jsgenesisFoundingMembers: 'Jsgenesis FoundingMembers',
+};
+
+export const areasPalette = {
+ communityFoundingMembers: '#ACACFA',
+ reserved1: '#9B9CF9',
+ reserved2: '#9B9CF9',
+ strategicPartners: '#6C6CFF',
+ membershipAirdrop: '#8D8DF9',
+ investors: '#7D7EF8',
+ jsgenesisFoundingMembers: '#6C6CFF',
+};
+
+export const formatXAxisTick = (num, isLast) => {
+ return num % 5 === 0 || isLast ? num : '';
+};
+
+export const formatYAxisTick = num => `${num}%`;
+
+export function renderCustomLabel(referenceLineLabelProps) {
+ const labelWidth = 43;
+ const labelHeight = 20;
+ return (
+
+
+
+ Now
+
+
+ );
+}
+
+const releaseSchedule = {
+ communityFoundingMembers: {
+ 0: 1.7,
+ 1: 2.51,
+ 2: 3.32,
+ 3: 4.14,
+ 4: 4.95,
+ 5: 5.76,
+ 6: 6.58,
+ 7: 7.39,
+ 8: 8.2,
+ 9: 9.02,
+ 10: 9.83,
+ 11: 10.64,
+ 12: 11.46,
+ 13: 12.27,
+ 14: 13.09,
+ 15: 13.9,
+ 16: 14.71,
+ 17: 15.53,
+ 18: 16.34,
+ 19: 17.15,
+ 20: 17.97,
+ 21: 18.78,
+ 22: 19.59,
+ 23: 20.41,
+ 24: 21.22,
+ },
+ reserved1: {
+ 0: 0,
+ 1: 0.98,
+ 2: 1.97,
+ 3: 2.95,
+ 4: 3.93,
+ 5: 4.92,
+ 6: 5.9,
+ 7: 6.88,
+ 8: 7.87,
+ 9: 8.85,
+ 10: 9.83,
+ 11: 10.82,
+ 12: 11.8,
+ 13: 11.8,
+ 14: 11.8,
+ 15: 11.8,
+ 16: 11.8,
+ 17: 11.8,
+ 18: 11.8,
+ 19: 11.8,
+ 20: 11.8,
+ 21: 11.8,
+ 22: 11.8,
+ 23: 11.8,
+ 24: 11.8,
+ },
+ reserved2: {
+ 0: 0,
+ 1: 0,
+ 2: 0,
+ 3: 0,
+ 4: 0,
+ 5: 0,
+ 6: 0,
+ 7: 0,
+ 8: 0,
+ 9: 0,
+ 10: 0,
+ 11: 0,
+ 12: 0,
+ 13: 0,
+ 14: 0,
+ 15: 0,
+ 16: 0,
+ 17: 0,
+ 18: 0,
+ 19: 0,
+ 20: 0,
+ 21: 0,
+ 22: 0,
+ 23: 0,
+ 24: 0,
+ },
+ strategicPartners: {
+ 0: 3,
+ 1: 3,
+ 2: 3,
+ 3: 3,
+ 4: 3,
+ 5: 3,
+ 6: 3,
+ 7: 3,
+ 8: 3,
+ 9: 3,
+ 10: 3,
+ 11: 3,
+ 12: 3,
+ 13: 3,
+ 14: 3,
+ 15: 3,
+ 16: 3,
+ 17: 3,
+ 18: 3,
+ 19: 3,
+ 20: 3,
+ 21: 3,
+ 22: 3,
+ 23: 3,
+ 24: 3,
+ },
+ membershipAirdrop: {
+ 0: 0.02,
+ 1: 0.03,
+ 2: 0.03,
+ 3: 0.04,
+ 4: 0.05,
+ 5: 0.06,
+ 6: 0.07,
+ 7: 0.08,
+ 8: 0.08,
+ 9: 0.09,
+ 10: 0.1,
+ 11: 0.11,
+ 12: 0.12,
+ 13: 0.13,
+ 14: 0.13,
+ 15: 0.14,
+ 16: 0.15,
+ 17: 0.16,
+ 18: 0.17,
+ 19: 0.18,
+ 20: 0.18,
+ 21: 0.19,
+ 22: 0.2,
+ 23: 0.21,
+ 24: 0.22,
+ },
+ investors: {
+ 0: 25.54,
+ 1: 26.11,
+ 2: 26.67,
+ 3: 27.24,
+ 4: 27.8,
+ 5: 28.37,
+ 6: 28.93,
+ 7: 29.5,
+ 8: 30.07,
+ 9: 30.63,
+ 10: 31.2,
+ 11: 31.76,
+ 12: 32.33,
+ 13: 32.33,
+ 14: 32.33,
+ 15: 32.33,
+ 16: 32.33,
+ 17: 32.33,
+ 18: 32.33,
+ 19: 32.33,
+ 20: 32.33,
+ 21: 32.33,
+ 22: 32.33,
+ 23: 32.33,
+ 24: 32.33,
+ },
+ jsgenesisFoundingMembers: {
+ 0: 2.51,
+ 1: 3.72,
+ 2: 4.92,
+ 3: 6.13,
+ 4: 7.33,
+ 5: 8.54,
+ 6: 9.74,
+ 7: 10.95,
+ 8: 12.15,
+ 9: 13.36,
+ 10: 14.56,
+ 11: 15.77,
+ 12: 16.97,
+ 13: 18.18,
+ 14: 19.38,
+ 15: 20.59,
+ 16: 21.79,
+ 17: 23.0,
+ 18: 24.2,
+ 19: 25.41,
+ 20: 26.61,
+ 21: 27.82,
+ 22: 29.02,
+ 23: 30.23,
+ 24: 31.44,
+ },
+};
+
+export const generateChartData = () => {
+ const data = [];
+
+ for (let i = 0; i <= 24; i += 1) {
+ data.push({
+ month: i,
+ communityFoundingMembers: releaseSchedule.communityFoundingMembers[i],
+ jsgenesisFoundingMembers: releaseSchedule.jsgenesisFoundingMembers[i],
+ investors: releaseSchedule.investors[i],
+ membershipAirdrop: releaseSchedule.membershipAirdrop[i],
+ strategicPartners: releaseSchedule.strategicPartners[i],
+ reserved1: releaseSchedule.reserved1[i],
+ reserved2: releaseSchedule.reserved2[i],
+ });
+ }
+
+ return data;
+};
+
+export const getMonthsSinceLaunch = () => {
+ const launchDate = new Date(2022, 11, 9);
+ const currentDate = new Date();
+
+ const diffInYears = currentDate.getFullYear() - launchDate.getFullYear();
+ const diffInMonths = currentDate.getMonth() - launchDate.getMonth();
+ return diffInYears * 12 + diffInMonths;
+};
+
+export const getHighlightedDate = monthIndex => {
+ const launchDate = {
+ year: 2022,
+ month: 11,
+ day: 9,
+ };
+
+ const monthsSum = launchDate.month + monthIndex;
+ const yearsInMonthsSum = monthsSum >= 12 ? Math.floor(monthsSum / 12) : 0;
+
+ const currentMonth = monthsSum - yearsInMonthsSum * 12;
+
+ const date = new Date(launchDate.year + yearsInMonthsSum, currentMonth, launchDate.day);
+
+ return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
+};
diff --git a/src/components/dashboard-page/Token/ReleaseScheduleChartWidget/index.js b/src/components/dashboard-page/Token/ReleaseScheduleChartWidget/index.js
new file mode 100644
index 000000000..c26701c36
--- /dev/null
+++ b/src/components/dashboard-page/Token/ReleaseScheduleChartWidget/index.js
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import WidgetHeading from '../../WidgetHeading';
+import ReleaseScheduleChart from '../ReleaseScheduleChart';
+
+import './style.scss';
+
+const ReleaseScheduleChartWidget = () => {
+ return (
+
+
+
+
+ );
+};
+
+export default ReleaseScheduleChartWidget;
diff --git a/src/components/dashboard-page/Token/ReleaseScheduleChartWidget/style.scss b/src/components/dashboard-page/Token/ReleaseScheduleChartWidget/style.scss
new file mode 100644
index 000000000..2008ff480
--- /dev/null
+++ b/src/components/dashboard-page/Token/ReleaseScheduleChartWidget/style.scss
@@ -0,0 +1,13 @@
+@import '../../../../styles/main';
+
+.dashboard-token-release-schedule-chart-widget {
+ @include dashboard-widget;
+
+ margin-top: 16px;
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ margin-top: 24px;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Token/RoiTableWidget/data.js b/src/components/dashboard-page/Token/RoiTableWidget/data.js
new file mode 100644
index 000000000..2a839e305
--- /dev/null
+++ b/src/components/dashboard-page/Token/RoiTableWidget/data.js
@@ -0,0 +1,24 @@
+export const columns = [
+ {
+ header: 'Time',
+ accessorKey: 'time',
+ },
+ {
+ header: 'Return',
+ accessorKey: 'return',
+ },
+];
+
+export const parseData = (data = {}) => {
+ const result = [];
+ const keys = Object.keys(data);
+ for (const key of keys) {
+ const numericPart = key.match(/\d/g)?.join('');
+ result.push({
+ time: key.replace(numericPart, `${numericPart} `),
+ return: `${data[key] > 0 ? '+' : ''}${data[key]?.toFixed(2)}%`,
+ });
+ }
+
+ return result;
+};
diff --git a/src/components/dashboard-page/Token/RoiTableWidget/index.js b/src/components/dashboard-page/Token/RoiTableWidget/index.js
new file mode 100644
index 000000000..aacd40837
--- /dev/null
+++ b/src/components/dashboard-page/Token/RoiTableWidget/index.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import { object } from 'prop-types';
+
+import WidgetHeading from '../../WidgetHeading';
+import Table from '../../Table';
+import Skeleton from '../../Skeleton';
+
+import { columns, parseData } from './data';
+
+import './style.scss';
+
+const propTypes = {
+ data: object,
+};
+
+const RoiTableWidget = ({ data }) => {
+ const parsedData = parseData(data);
+
+ if (!data) {
+ return
;
+ }
+
+ return (
+
+ );
+};
+
+RoiTableWidget.propTypes = propTypes;
+
+export default RoiTableWidget;
diff --git a/src/components/dashboard-page/Token/RoiTableWidget/style.scss b/src/components/dashboard-page/Token/RoiTableWidget/style.scss
new file mode 100644
index 000000000..865c4dc21
--- /dev/null
+++ b/src/components/dashboard-page/Token/RoiTableWidget/style.scss
@@ -0,0 +1,23 @@
+@import '../../../../styles/main';
+
+.dashboard-token-roi-table-widget {
+ @include dashboard-widget;
+ @include reset-dashboard-widget-padding;
+
+ &__heading {
+ @include dashboard-widget-heading-padding;
+ margin-bottom: 16px;
+
+ & .dashboard-widget-heading__info-wrapper {
+ right: 0;
+
+ @media #{$screen-min-dashboard-xs} {
+ right: -53px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ padding-right: 0;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Token/Skeletons/index.js b/src/components/dashboard-page/Token/Skeletons/index.js
new file mode 100644
index 000000000..a185d6b7a
--- /dev/null
+++ b/src/components/dashboard-page/Token/Skeletons/index.js
@@ -0,0 +1,62 @@
+import React from 'react';
+import cn from 'classnames';
+
+import Skeleton from '../../Skeleton';
+
+import './style.scss';
+
+export const PriceBlockSkeleton = () => {
+ return (
+
+
+ {Array.from({ length: 3 }, (_, i) => {
+ return (
+
+ );
+ })}
+
+ );
+};
+
+export const SupplyBlockSkeleton = () => {
+ return
;
+};
+
+export const ExchangeBlockSkeleton = bps => {
+ const skeletonsQuantity = bps === 'xxs' || bps === 'xs' || bps === 'sm' ? 4 : 8;
+ return (
+
+ {Array.from({ length: skeletonsQuantity }, (_, i) => {
+ return ;
+ })}
+
+ );
+};
+
+export const AllocationMintingBlockSkeleton = () => {
+ return (
+
+
+
+
+ );
+};
+
+export const SupplyAprBlockSkeleton = () => {
+ return (
+
+ {Array.from({ length: 2 }, (_, i) => {
+ return ;
+ })}
+
+ );
+};
+
+export const RoiSupplyBlockSkeleton = () => {
+ return (
+
+
+
+
+ );
+};
diff --git a/src/components/dashboard-page/Token/Skeletons/style.scss b/src/components/dashboard-page/Token/Skeletons/style.scss
new file mode 100644
index 000000000..aeeb9d556
--- /dev/null
+++ b/src/components/dashboard-page/Token/Skeletons/style.scss
@@ -0,0 +1,213 @@
+@import '../../../../styles/main';
+
+.price-block-skeleton {
+ display: grid;
+ gap: 16px;
+
+ &__chart {
+ width: 100%;
+ height: 528px;
+ }
+
+ &__stats-widget {
+ width: 100%;
+ height: 136px;
+
+ &.height-reduced {
+ height: 112px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ & {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ &__chart {
+ grid-column: 1 / span 3;
+ }
+
+ &__stats-widget {
+ height: 184px;
+
+ &.height-reduced {
+ height: 184px;
+ }
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ gap: 24px;
+ grid-template-rows: repeat(2, 1fr) 160px;
+ }
+
+ &__chart {
+ height: auto;
+ grid-row: 1 / span 3;
+ grid-column: 1 / span 2;
+ }
+
+ &__stats-widget {
+ height: 192px;
+
+ &.height-reduced {
+ height: 160px;
+ }
+ }
+ }
+}
+
+.supply-block-skeleton {
+ margin-top: 16px;
+ width: 100%;
+ height: 425px;
+
+ @media #{$screen-min-dashboard-sm} {
+ height: 345px;
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ margin-top: 24px;
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ height: 225px;
+ }
+}
+
+.exchange-block-skeleton {
+ display: grid;
+ gap: 16px;
+
+ &__option {
+ width: 100%;
+ height: 260px;
+ }
+
+ @media #{$screen-min-dashboard-xs} {
+ & {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ &__option {
+ height: 248px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ & {
+ max-height: 300px;
+ overflow-y: hidden;
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ &__option {
+ height: 300px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ max-height: none;
+ gap: 24px;
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ &__option {
+ height: 300px;
+ }
+ }
+}
+
+.allocation-minting-block-skeleton {
+ margin-top: 16px;
+ display: grid;
+ gap: 16px;
+
+ &__allocation {
+ width: 100%;
+ height: 440px;
+ }
+
+ &__minting {
+ width: 100%;
+ height: 440px;
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ margin-top: 24px;
+ gap: 24px;
+ grid-template-columns: minmax(550px, 1fr) 1fr;
+ }
+ }
+}
+
+.supply-apr-block-skeleton {
+ margin-top: 16px;
+ display: grid;
+ gap: 16px;
+
+ &__stats-widget {
+ width: 100%;
+ height: 120px;
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ & {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ &__stats-widget {
+ height: 152px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ margin-top: 24px;
+ gap: 24px;
+ }
+ }
+}
+
+.roi-supply-block-skeleton {
+ margin-top: 16px;
+ display: grid;
+ gap: 16px;
+
+ &__roi {
+ width: 100%;
+ height: 512px;
+
+ &.height-auto {
+ height: auto;
+ }
+ }
+
+ &__supply {
+ width: 100%;
+ height: 512px;
+
+ &.height-auto {
+ height: auto;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ margin-top: 24px;
+ gap: 24px;
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ &__roi {
+ grid-column: 1 / span 1;
+ }
+
+ &__supply {
+ grid-column: 2 / span 2;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Token/SupplyDistributionTableWidget/data.js b/src/components/dashboard-page/Token/SupplyDistributionTableWidget/data.js
new file mode 100644
index 000000000..d11398733
--- /dev/null
+++ b/src/components/dashboard-page/Token/SupplyDistributionTableWidget/data.js
@@ -0,0 +1,42 @@
+export const columns = [
+ {
+ header: 'Type',
+ accessorKey: 'type',
+ },
+ {
+ header: 'Supply',
+ accessorKey: 'supply',
+ },
+ {
+ header: '% of circulating supply',
+ accessorKey: 'rateOfCirculatingSupply',
+ },
+];
+
+const formatSupply = num => Math.round(num)?.toLocaleString('fr-FR');
+const formatSupplyRate = num => `${Math.round(num)}%`;
+
+export const supplyDistributionTypeLabels = {
+ top100Addresses: 'Supply In Top 100 Addresses',
+ top1PercentAddresses: 'Supply In Top 1% Addresses',
+ addressesOver10MUSD: 'Supply In Addresses > $10M',
+ addressesOver100KUSD: 'Supply In Addresses > $100K',
+ addressesOver10KUSD: 'Supply In Addresses > $10K',
+ addressesOver1KUSD: 'Supply In Addresses > $1K',
+ addressesOver100USD: 'Supply In Addresses > $100',
+ addressesOver1MJOY: 'Supply In Addresses > 1M $JOY',
+};
+
+export const parseData = (data = {}) => {
+ const result = [];
+ const keys = Object.keys(data);
+ for (const key of keys) {
+ result.push({
+ type: supplyDistributionTypeLabels[key],
+ supply: formatSupply(data[key]?.supply),
+ rateOfCirculatingSupply: formatSupplyRate(data[key]?.percentOfCirculatingSupply),
+ });
+ }
+
+ return result;
+};
diff --git a/src/components/dashboard-page/Token/SupplyDistributionTableWidget/index.js b/src/components/dashboard-page/Token/SupplyDistributionTableWidget/index.js
new file mode 100644
index 000000000..4985155dd
--- /dev/null
+++ b/src/components/dashboard-page/Token/SupplyDistributionTableWidget/index.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import { object } from 'prop-types';
+
+import WidgetHeading from '../../WidgetHeading';
+import Table from '../../Table';
+import Skeleton from '../../Skeleton';
+
+import { columns, parseData } from './data';
+
+import './style.scss';
+
+const propTypes = {
+ data: object,
+};
+
+const SupplyDistributionTableWidget = ({ data }) => {
+ const parsedData = parseData(data);
+
+ if (!data) {
+ return
;
+ }
+
+ return (
+
+ );
+};
+
+SupplyDistributionTableWidget.propTypes = propTypes;
+
+export default SupplyDistributionTableWidget;
diff --git a/src/components/dashboard-page/Token/SupplyDistributionTableWidget/style.scss b/src/components/dashboard-page/Token/SupplyDistributionTableWidget/style.scss
new file mode 100644
index 000000000..460b4a819
--- /dev/null
+++ b/src/components/dashboard-page/Token/SupplyDistributionTableWidget/style.scss
@@ -0,0 +1,23 @@
+@import '../../../../styles/main';
+
+.dashboard-token-supply-distribution-table-widget {
+ @include dashboard-widget;
+ @include reset-dashboard-widget-padding;
+
+ &__heading {
+ @include dashboard-widget-heading-padding;
+ margin-bottom: 16px;
+ }
+
+ &__table {
+ & th {
+ width: 33.33%;
+ }
+ & th:nth-of-type(3n) {
+ @include t100;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ text-transform: none;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Token/SupplyWidget/data.js b/src/components/dashboard-page/Token/SupplyWidget/data.js
new file mode 100644
index 000000000..ff8c43028
--- /dev/null
+++ b/src/components/dashboard-page/Token/SupplyWidget/data.js
@@ -0,0 +1,40 @@
+import { isNaN } from '../../../../utils/withFallbackVal';
+
+const parseNumWithCommaAsSeparator = (data = {}, key) => {
+ const num = data[key];
+
+ if (isNaN(num)) {
+ return '0 JOY';
+ }
+
+ return `${num?.toLocaleString('en-US')} JOY`;
+};
+
+const convertJoyValToUsDollarsMils = (data = {}, key) => {
+ const val = data[key];
+ const price = data?.price;
+
+ if (isNaN(val) || isNaN(price)) {
+ return '$0M';
+ }
+
+ return `$${((val * price) / 1000000).toFixed(1)}M`;
+};
+
+export const getTokenSupplyMetrics = (data = {}) => [
+ {
+ figure: 'Circulating supply',
+ tokenRate: parseNumWithCommaAsSeparator(data, 'circulatingSupply'),
+ rate: convertJoyValToUsDollarsMils(data, 'circulatingSupply'),
+ },
+ {
+ figure: 'Total supply',
+ tokenRate: parseNumWithCommaAsSeparator(data, 'totalSupply'),
+ rate: convertJoyValToUsDollarsMils(data, 'totalSupply'),
+ },
+];
+
+export const learWhyVideo = {
+ source: 'https://gleev.xyz/video/329910',
+ duration: '9:12min',
+};
diff --git a/src/components/dashboard-page/Token/SupplyWidget/index.js b/src/components/dashboard-page/Token/SupplyWidget/index.js
new file mode 100644
index 000000000..932be222f
--- /dev/null
+++ b/src/components/dashboard-page/Token/SupplyWidget/index.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import { string, object } from 'prop-types';
+
+import WidgetHeading from '../../WidgetHeading';
+
+import { ReactComponent as WarningIcon } from '../../../../assets/svg/dashboard/warning-icon.svg';
+import { ReactComponent as PlayAltIcon } from '../../../../assets/svg/dashboard/play-alt-icon.svg';
+
+import { getTokenSupplyMetrics, learWhyVideo } from './data';
+
+import './style.scss';
+
+const dashboardTokenSupplyStatsPropTypes = {
+ figure: string.isRequired,
+ tokenRate: string.isRequired,
+ rate: string.isRequired,
+};
+
+const SupplyStats = ({ figure, tokenRate, rate }) => {
+ return (
+
+
{figure}
+
{tokenRate}
+
{rate}
+
+ );
+};
+
+SupplyStats.propTypes = dashboardTokenSupplyStatsPropTypes;
+
+const supplyWidgetPropTypes = {
+ data: object,
+};
+
+const SupplyWidget = ({ data }) => {
+ const tokenSupplyMetrics = getTokenSupplyMetrics(data);
+
+ return (
+
+
+
+ {tokenSupplyMetrics.map((tokenSupplyStats, index) => {
+ return
;
+ })}
+
+
+
+ );
+};
+
+SupplyWidget.propTypes = supplyWidgetPropTypes;
+
+export default SupplyWidget;
diff --git a/src/components/dashboard-page/Token/SupplyWidget/style.scss b/src/components/dashboard-page/Token/SupplyWidget/style.scss
new file mode 100644
index 000000000..4f7782ffb
--- /dev/null
+++ b/src/components/dashboard-page/Token/SupplyWidget/style.scss
@@ -0,0 +1,101 @@
+@import '../../../../styles/main';
+
+.dashboard-token-supply-widget {
+ @include dashboard-widget;
+ margin-top: 16px;
+
+ &__content {
+ display: grid;
+ gap: 40px;
+ }
+
+ &__notice-text-wrapper {
+ display: flex;
+ align-items: center;
+ margin-bottom: 8px;
+ }
+
+ &__notice-text {
+ @include dashboard-widget-helper-text;
+ margin-left: 8px;
+ }
+
+ &__notice-button {
+ width: 100%;
+ padding: 12px 20px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ @include t300;
+ font-weight: 600;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ background-color: $dashboard-widget-base-background-color;
+ border: 1px solid $dashboard-base-borders-color;
+ border-radius: 2px;
+ cursor: pointer;
+ transition: all $dashboard-transition-duration $dashboard-transition-timing;
+
+ & > svg {
+ margin-right: 8px;
+ }
+
+ & > span {
+ color: $dashboard-base-gray-text-color;
+ }
+
+ &:hover,
+ &:focus {
+ border-color: $dashboard-base-border-color;
+ background-color: $dashboard-widget-base-info-states-background-color;
+ }
+
+ &:active {
+ background-color: $dashboard-widget-base-background-color;
+ transition-duration: 0ms;
+ }
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__content {
+ grid-template-rows: repeat(2, auto);
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ &__notice {
+ grid-column: 1 / span 2;
+ }
+
+ &__notice-button {
+ width: fit-content;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ margin-top: 24px;
+ }
+ }
+
+ @media #{$screen-min-dashboard-lg} {
+ &__content {
+ grid-template-rows: 1fr;
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ &__notice {
+ grid-column: 3 / 3;
+ }
+ }
+}
+
+.dashboard-token-supply-stats {
+ &__figure,
+ &__rate {
+ @include dashboard-widget-helper-text;
+ }
+
+ &__token-rate {
+ @include dashboard-widget-text;
+ }
+}
diff --git a/src/components/dashboard-page/Token/index.js b/src/components/dashboard-page/Token/index.js
new file mode 100644
index 000000000..b0eb12843
--- /dev/null
+++ b/src/components/dashboard-page/Token/index.js
@@ -0,0 +1,114 @@
+import React from 'react';
+import { bool, object } from 'prop-types';
+
+import SectionHeader from '../SectionHeader';
+import PriceChartWidget from './PriceChartWidget';
+import StatsWidget from '../StatsWidget';
+import SupplyWidget from './SupplyWidget';
+// import DashboardJoyCarousel from '../JoyCarousel';
+import Exchange from './Exchange';
+import ReleaseScheduleChartWidget from './ReleaseScheduleChartWidget';
+import AllocationTableWidget from './AllocationTableWidget';
+import MintingChartWidget from './MintingChartWidget';
+import RoiTableWidget from './RoiTableWidget';
+import SupplyDistributionTableWidget from './SupplyDistributionTableWidget';
+import {
+ PriceBlockSkeleton,
+ SupplyBlockSkeleton,
+ AllocationMintingBlockSkeleton,
+ SupplyAprBlockSkeleton,
+ RoiSupplyBlockSkeleton,
+} from './Skeletons';
+
+import { getTokenPriceMetrics, parsePercentage } from './utils';
+
+import './style.scss';
+
+const propTypes = {
+ data: object,
+ loading: bool,
+};
+
+const Token = ({ data, loading }) => {
+ const tokenPriceMetrics = getTokenPriceMetrics(data);
+
+ const supplyStakedForValidation = parsePercentage(data?.percentSupplyStakedForValidation);
+ const aprOnStaking = parsePercentage(data?.apr);
+
+ return (
+
+
+
+
+ {loading ? (
+
+ ) : (
+
+
+ {tokenPriceMetrics.map((tokenPriceStats, index) => {
+ return (
+
+ );
+ })}
+
+ )}
+
+ {loading ?
:
}
+
+ {/*
*/}
+
+
+
+
+
+ {loading ? (
+
+ ) : (
+
+ )}
+
+ {loading ? (
+
+ ) : (
+
+
+
+
+ )}
+
+ {loading || (!data?.roi && !data?.supplyDistribution) ? (
+
+ ) : (
+
+
+
+
+ )}
+
+
+ );
+};
+
+Token.propTypes = propTypes;
+
+export default Token;
diff --git a/src/components/dashboard-page/Token/style.scss b/src/components/dashboard-page/Token/style.scss
new file mode 100644
index 000000000..a88e9cdf9
--- /dev/null
+++ b/src/components/dashboard-page/Token/style.scss
@@ -0,0 +1,79 @@
+@import '../../../styles/main';
+
+.dashboard-token {
+ @include dashboard-section;
+
+ &__container {
+ @include dashboard-container;
+ }
+
+ &__widget-tooltip-alt-placement {
+ & .dashboard-widget-heading__info-wrapper {
+ right: 0;
+
+ @media #{$screen-min-dashboard-xs} {
+ right: -53px;
+ }
+ }
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__price-metrics-grid {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ &__price-chart-widget {
+ grid-column: 1 / span 3;
+ }
+
+ &__percentage-widgets-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ &__price-metrics-grid {
+ gap: 24px;
+ grid-template-rows: repeat(2, 192px) 160px;
+ }
+
+ &__price-chart-widget {
+ grid-row: 1 / span 3;
+ grid-column: 1 / span 2;
+ }
+
+ &__allocation-minting-grid {
+ gap: 24px;
+ grid-template-columns: minmax(550px, 1fr) 1fr;
+ }
+
+ &__percentage-widgets-grid {
+ gap: 24px;
+ }
+
+ &__stats-tables-grid {
+ gap: 24px;
+ grid-template-columns: repeat(3, 1fr);
+
+ & > .dashboard-token-roi-table-widget {
+ grid-column: 1 / span 1;
+ }
+
+ & > .dashboard-token-supply-distribution-table-widget {
+ grid-column: 2 / span 2;
+ }
+ }
+ }
+}
+
+.grid-indents {
+ margin-top: 16px;
+
+ display: grid;
+ gap: 16px;
+
+ @media #{$screen-min-dashboard-md} {
+ margin-top: 24px;
+ gap: 24px;
+ }
+}
diff --git a/src/components/dashboard-page/Token/utils.js b/src/components/dashboard-page/Token/utils.js
new file mode 100644
index 000000000..13cdda6c6
--- /dev/null
+++ b/src/components/dashboard-page/Token/utils.js
@@ -0,0 +1,47 @@
+import { isNaN } from '../../../utils/withFallbackVal';
+
+const parseNumToMil = (data = {}, key) => {
+ const metrics = data[key];
+ if (isNaN(metrics)) {
+ return '$0M';
+ }
+ return `$${(metrics / 1000000).toFixed(1)}M`;
+};
+
+const parseMetricsWeeklyChange = (data = {}, key) => {
+ const metrics = data[key];
+ if (isNaN(metrics)) {
+ return '0%';
+ }
+ const roundedMetrics = Math.round(metrics);
+ const metricsWithSign = roundedMetrics > 0 ? `+${roundedMetrics}` : roundedMetrics;
+ return `${metricsWithSign}%`;
+};
+
+export const getTokenPriceMetrics = (data = {}) => [
+ {
+ figure: 'Marketcap',
+ rate: parseNumToMil(data, 'marketCap'),
+ growthRate: parseMetricsWeeklyChange(data, 'marketCapWeeklyChange'),
+ termDefinitionKey: 'marketcap',
+ },
+ {
+ figure: 'Volume (24h)',
+ rate: parseNumToMil(data, 'volume'),
+ growthRate: parseMetricsWeeklyChange(data, 'volumeWeeklyChange'),
+ termDefinitionKey: 'volume',
+ },
+ {
+ figure: 'FDV',
+ rate: parseNumToMil(data, 'fullyDilutedValue'),
+ termDefinitionKey: 'fdv',
+ },
+];
+
+export const parsePercentage = val => {
+ const shouldReturnInt = val?.toFixed(1)?.includes('.0');
+ if (isNaN(val)) {
+ return '0%';
+ }
+ return `${val?.toFixed(shouldReturnInt ? 0 : 1)}%`;
+};
diff --git a/src/components/dashboard-page/Traction/Chart/index.js b/src/components/dashboard-page/Traction/Chart/index.js
new file mode 100644
index 000000000..6cc4ba9be
--- /dev/null
+++ b/src/components/dashboard-page/Traction/Chart/index.js
@@ -0,0 +1,90 @@
+import React, { useState, useEffect } from 'react';
+import { ResponsiveContainer, BarChart, CartesianGrid, XAxis, YAxis, Text, Bar } from 'recharts';
+import { bool, arrayOf, shape, string, number } from 'prop-types';
+
+import ChartWrapper from '../../ChartWrapper';
+import useDashboardMedia from '../../../../utils/useDashboardMedia';
+
+const propTypes = {
+ withBarGapExtended: bool,
+ data: arrayOf(
+ shape({
+ month: string.isRequired,
+ 0: number.isRequired,
+ 1: number.isRequired,
+ 2: number.isRequired,
+ 3: number.isRequired,
+ })
+ ),
+ chartHeight: number,
+ withYAxisMarginReduced: bool,
+};
+
+const Chart = ({ withBarGapExtended, data, chartHeight, withYAxisMarginReduced }) => {
+ const { currentBreakpoints } = useDashboardMedia();
+ const [barGap, setBarGap] = useState(1);
+
+ useEffect(() => {
+ switch (currentBreakpoints) {
+ case 'xs':
+ return setBarGap(1.6);
+ case 'sm':
+ return setBarGap(3);
+ case 'md':
+ return setBarGap(1.8);
+ case 'lg':
+ case 'xl':
+ return setBarGap(withBarGapExtended ? 2.8 : 1.6);
+ default:
+ return setBarGap(1);
+ }
+ }, [currentBreakpoints, withBarGapExtended]);
+
+ const isYAxisMarginReduced =
+ withYAxisMarginReduced &&
+ (currentBreakpoints === 'xxs' || currentBreakpoints === 'xs' || currentBreakpoints === 'sm');
+
+ return (
+
+
+
+
+ (
+
+ {tickProps.payload.value}
+
+ )}
+ tickLine={false}
+ tickMargin={24}
+ interval={0}
+ />
+ (
+
+ {tickProps.payload.value}
+
+ )}
+ tickLine={false}
+ tickMargin={isYAxisMarginReduced ? 10 : 28}
+ />
+ {Array.from({ length: 4 }, (_, i) => {
+ return (
+
+ );
+ })}
+
+
+
+ );
+};
+
+Chart.propTypes = propTypes;
+Chart.defaultProps = {
+ chartHeight: 250,
+};
+
+export default Chart;
diff --git a/src/components/dashboard-page/Traction/ChartWidget/index.js b/src/components/dashboard-page/Traction/ChartWidget/index.js
new file mode 100644
index 000000000..eddf7986b
--- /dev/null
+++ b/src/components/dashboard-page/Traction/ChartWidget/index.js
@@ -0,0 +1,54 @@
+import React from 'react';
+import { string, number, arrayOf, shape, bool } from 'prop-types';
+
+import WidgetHeading from '../../WidgetHeading';
+
+import Chart from '../Chart';
+
+import './style.scss';
+
+const propTypes = {
+ heading: string.isRequired,
+ valueOfIndicator: string.isRequired,
+ growthRate: number.isRequired,
+ indicator: string.isRequired,
+ chartData: arrayOf(
+ shape({
+ month: string.isRequired,
+ 0: number.isRequired,
+ 1: number.isRequired,
+ 2: number.isRequired,
+ 3: number.isRequired,
+ })
+ ),
+ chartHeight: number,
+ termDefinitionKey: string,
+ withYAxisMarginReduced: bool,
+};
+
+const ChartWidget = ({
+ heading,
+ valueOfIndicator,
+ growthRate,
+ indicator,
+ chartData,
+ chartHeight,
+ termDefinitionKey,
+ withYAxisMarginReduced,
+}) => {
+ const growthRateWithSign = growthRate > 0 ? `+${growthRate}` : growthRate;
+
+ return (
+
+
+
{valueOfIndicator}
+
{`${growthRateWithSign}% Changes`}
+
{`${indicator} per week`}
+
+
+ );
+};
+
+ChartWidget.propTypes = propTypes;
+
+export default ChartWidget;
diff --git a/src/components/dashboard-page/Traction/ChartWidget/style.scss b/src/components/dashboard-page/Traction/ChartWidget/style.scss
new file mode 100644
index 000000000..3139c4fc6
--- /dev/null
+++ b/src/components/dashboard-page/Traction/ChartWidget/style.scss
@@ -0,0 +1,44 @@
+@import '../../../../styles/main';
+
+.dashboard-traction-chart-widget {
+ @include dashboard-widget;
+ $tick-height-overlapped-by-padding: 8px;
+
+ padding-bottom: 16px + $tick-height-overlapped-by-padding;
+
+ &__indicator-value {
+ @include dashboard-widget-text;
+ }
+
+ &__growth-rate {
+ margin-bottom: 16px;
+ @include dashboard-widget-helper-text;
+ }
+
+ &__indicator {
+ margin-bottom: 8px;
+ @include t300;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ & {
+ padding-bottom: 32px + $tick-height-overlapped-by-padding;
+ }
+
+ &__indicator-value {
+ @include h700;
+ }
+
+ &__indicator {
+ @include t400;
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ &__indicator-value {
+ @include h800;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Traction/Metrics/index.js b/src/components/dashboard-page/Traction/Metrics/index.js
new file mode 100644
index 000000000..0d4f09613
--- /dev/null
+++ b/src/components/dashboard-page/Traction/Metrics/index.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import { string, number } from 'prop-types';
+
+import WidgetHeading from '../../WidgetHeading';
+
+import './style.scss';
+
+const propTypes = {
+ indicator: string.isRequired,
+ value: string.isRequired,
+ growthRate: number,
+ termDefinitionKey: string,
+};
+
+const Metrics = ({ indicator, value, growthRate, termDefinitionKey }) => {
+ return (
+
+
+
{value}
+ {!!growthRate &&
{growthRate}
}
+
+ );
+};
+
+Metrics.propTypes = propTypes;
+
+export default Metrics;
diff --git a/src/components/dashboard-page/Traction/Metrics/style.scss b/src/components/dashboard-page/Traction/Metrics/style.scss
new file mode 100644
index 000000000..e626375a4
--- /dev/null
+++ b/src/components/dashboard-page/Traction/Metrics/style.scss
@@ -0,0 +1,11 @@
+@import '../../../../styles/main';
+
+.dashboard-traction-metrics {
+ &__value {
+ @include dashboard-widget-text;
+ }
+
+ &__growth-rate {
+ @include dashboard-widget-helper-text;
+ }
+}
diff --git a/src/components/dashboard-page/Traction/Skeletons/index.js b/src/components/dashboard-page/Traction/Skeletons/index.js
new file mode 100644
index 000000000..329b5dca6
--- /dev/null
+++ b/src/components/dashboard-page/Traction/Skeletons/index.js
@@ -0,0 +1,15 @@
+import React from 'react';
+
+import Skeleton from '../../Skeleton';
+
+import './style.scss';
+
+export const TractionContentSkeleton = () => {
+ return (
+
+ {Array.from({ length: 4 }, (_, i) => {
+ return ;
+ })}
+
+ );
+};
diff --git a/src/components/dashboard-page/Traction/Skeletons/style.scss b/src/components/dashboard-page/Traction/Skeletons/style.scss
new file mode 100644
index 000000000..c0b101e53
--- /dev/null
+++ b/src/components/dashboard-page/Traction/Skeletons/style.scss
@@ -0,0 +1,22 @@
+@import '../../../../styles/main';
+
+.traction-content-skeleton {
+ display: grid;
+ gap: 16px;
+
+ &__item {
+ width: 100%;
+ height: 440px;
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ & {
+ gap: 24px;
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ &__item {
+ height: 520px;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/Traction/data.js b/src/components/dashboard-page/Traction/data.js
new file mode 100644
index 000000000..97abe2b4b
--- /dev/null
+++ b/src/components/dashboard-page/Traction/data.js
@@ -0,0 +1,127 @@
+import { isNaN, withFallbackNumVal } from '../../../utils/withFallbackVal';
+
+export const chartMockData = [
+ {
+ month: 'Jan',
+ 0: 100,
+ 1: 250,
+ 2: 500,
+ 3: 300,
+ },
+ {
+ month: 'Feb',
+ 0: 800,
+ 1: 400,
+ 2: 600,
+ 3: 300,
+ },
+ {
+ month: 'Mar',
+ 0: 750,
+ 1: 320,
+ 2: 550,
+ 3: 800,
+ },
+ {
+ month: 'Apr',
+ 0: 700,
+ 1: 300,
+ 2: 980,
+ 3: 1000,
+ },
+ {
+ month: 'May',
+ 0: 720,
+ 1: 450,
+ 2: 600,
+ 3: 320,
+ },
+ {
+ month: 'Jun',
+ 0: 600,
+ 1: 800,
+ 2: 980,
+ 3: 550,
+ },
+];
+
+export const withFallbackValues = chartData => (!!chartData.length ? chartData : chartMockData);
+
+const numAddSeparators = num => {
+ if (isNaN(num)) {
+ return 0;
+ }
+ return num?.toLocaleString('fr-FR');
+};
+export const roundWeeklyRate = num => {
+ if (isNaN(num)) {
+ return 0;
+ }
+ return Math.round(num);
+};
+const roundWeeklyRateWithSign = num => {
+ if (isNaN(num)) {
+ return '0% Last week';
+ }
+ return `${num > 0 ? '+' : ''}${Math.round(num)}% Last week`;
+};
+
+export const parseStats = (data = {}) => [
+ {
+ indicator: 'Average block time',
+ value: `${withFallbackNumVal(data.averageBlockTime)} sec`,
+ termDefinitionKey: 'averageBlockTime',
+ },
+ {
+ indicator: 'Transactions',
+ value: numAddSeparators(data.totalNumberOfTransactions),
+ growthRate: roundWeeklyRateWithSign(data.totalNumberOfTransactionsWeeklyChange),
+ termDefinitionKey: 'transactions',
+ },
+ {
+ indicator: 'Holders',
+ value: numAddSeparators(data.totalNumberOfAccountHolders),
+ growthRate: roundWeeklyRateWithSign(data.totalNumberOfAccountHoldersWeeklyChange),
+ termDefinitionKey: 'holders',
+ },
+ {
+ indicator: 'Daily active accounts',
+ value: numAddSeparators(data.numberOfDailyActiveAccounts),
+ growthRate: roundWeeklyRateWithSign(data.numberOfDailyActiveAccountsWeeklyChange),
+ termDefinitionKey: 'dailyActiveAccounts',
+ },
+];
+
+export const parseNumToThsdWith1Dec = num => {
+ if (isNaN(num)) {
+ return '0K';
+ }
+
+ const quotient = num / 1000;
+ if (quotient < 1000) {
+ return `${quotient.toFixed(1)}K`;
+ }
+
+ return `${(quotient / 1000).toFixed(1)}M`;
+};
+
+export const parseChartData = (data = [], tokenPriceInUsd = 1) => {
+ const monthSpan = 4;
+ const result = [];
+
+ for (let i = 0; i < data?.length; i += monthSpan) {
+ const monthData = data.slice(i, i + 4);
+
+ const parsedMonthData = {
+ month: new Date(data[i].from).toLocaleDateString('en-US', { month: 'short' }),
+ };
+
+ for (let j = 0; j < monthData.length; j += 1) {
+ const amount = monthData[j].amount;
+ parsedMonthData[j] = !!amount ? amount * tokenPriceInUsd : monthData[j].numberOfItems;
+ }
+
+ result.push(parsedMonthData);
+ }
+ return result;
+};
diff --git a/src/components/dashboard-page/Traction/index.js b/src/components/dashboard-page/Traction/index.js
new file mode 100644
index 000000000..077a45b7b
--- /dev/null
+++ b/src/components/dashboard-page/Traction/index.js
@@ -0,0 +1,119 @@
+import React, { useMemo } from 'react';
+import cn from 'classnames';
+import { object, bool } from 'prop-types';
+
+import SectionHeader from '../SectionHeader';
+import ChartWidget from './ChartWidget';
+import WidgetHeading from '../WidgetHeading';
+import Metrics from './Metrics';
+import Feature from '../../Feature';
+import { TractionContentSkeleton } from './Skeletons';
+
+import useDashboardMedia from '../../../utils/useDashboardMedia/index.js';
+
+import {
+ chartMockData,
+ parseStats,
+ parseNumToThsdWith1Dec,
+ roundWeeklyRate,
+ parseChartData,
+ withFallbackValues,
+} from './data.js';
+
+import './style.scss';
+
+const propTypes = {
+ data: object,
+ loading: bool,
+};
+
+const Traction = ({ data, loading }) => {
+ const { currentBreakpoints } = useDashboardMedia();
+ const commentsAndReactionsChartHeight = useMemo(() => (currentBreakpoints === 'md' ? 314 : 250), [
+ currentBreakpoints,
+ ]);
+
+ const parsedStats = parseStats(data?.traction);
+
+ const parsedWeeklyChannelData = parseChartData(data?.traction?.weeklyChannelData);
+
+ const parsedWeeklyVideoData = parseChartData(data?.traction?.weeklyVideoData);
+
+ const parsedWeeklyCommentsAndReactionsData = parseChartData(data?.traction?.weeklyCommentsAndReactionsData);
+
+ const parsedWeeklyVolumeOfSoldNFTs = parseChartData(data?.traction?.weeklyVolumeOfSoldNFTs, data?.token?.price);
+
+ const isFeatureEnabled = false;
+
+ return (
+
+
+
+
+ {loading ? (
+
+ ) : (
+
+
+
+
+
+
+
+ {parsedStats.map((m, i) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+Traction.propTypes = propTypes;
+
+export default Traction;
diff --git a/src/components/dashboard-page/Traction/style.scss b/src/components/dashboard-page/Traction/style.scss
new file mode 100644
index 000000000..1e73c1f37
--- /dev/null
+++ b/src/components/dashboard-page/Traction/style.scss
@@ -0,0 +1,69 @@
+@import '../../../styles/main';
+
+.dashboard-traction {
+ @include dashboard-section;
+
+ &__container {
+ @include dashboard-container;
+ }
+
+ &__grid {
+ display: grid;
+ gap: 16px;
+ }
+
+ &__metrics {
+ @include dashboard-widget;
+ }
+
+ &__metrics-wrapper {
+ display: grid;
+ gap: 40px;
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ &__metrics-wrapper {
+ grid-template-columns: repeat(2, 1fr);
+ grid-template-rows: repeat(2, 1fr);
+ }
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ &__grid {
+ gap: 24px;
+ grid-template-columns: repeat(2, 1fr);
+ grid-template-rows: min-content;
+ }
+
+ &__metrics-wrapper {
+ gap: 16px;
+ grid-template-columns: 1fr;
+ grid-template-rows: min-content;
+ }
+ }
+
+ @media #{$screen-min-dashboard-lg} {
+ &__grid {
+ grid-template-columns: repeat(6, 1fr);
+ grid-template-rows: repeat(3, min-content);
+
+ & .dashboard-traction-chart-widget {
+ grid-column: span 2;
+
+ &.with-feature-enabled:nth-last-child(-n + 2) {
+ grid-column: span 3;
+ }
+ }
+ }
+
+ &__metrics {
+ grid-column: 1 / span 6;
+ }
+
+ &__metrics-wrapper {
+ gap: 0;
+ grid-template-columns: repeat(4, 1fr);
+ grid-template-rows: 1fr;
+ }
+ }
+}
diff --git a/src/components/dashboard-page/WidgetHeading/index.js b/src/components/dashboard-page/WidgetHeading/index.js
new file mode 100644
index 000000000..e23a6985f
--- /dev/null
+++ b/src/components/dashboard-page/WidgetHeading/index.js
@@ -0,0 +1,85 @@
+import React from 'react';
+import cn from 'classnames';
+import { string, bool } from 'prop-types';
+
+import { ReactComponent as InfoIcon } from '../../../assets/svg/dashboard/info-icon.svg';
+
+import { termDefinitions } from '../../../data/pages/dashboard/termDefinitions';
+
+import './style.scss';
+
+const propTypes = {
+ heading: string.isRequired,
+ termDefinitionKey: string,
+ headingWrapperCn: string,
+ isDim: bool,
+ helperText: string,
+};
+
+const defaultProps = {
+ headingWrapperCn: 'base-margin',
+};
+
+const DashboardWidgetHeading = ({ heading, termDefinitionKey, headingWrapperCn, isDim, helperText }) => {
+ return (
+
+
+ {heading}
+ {!!helperText && (
+ <>
+ {helperText}
+ >
+ )}
+
+ {termDefinitionKey && !!termDefinitions[termDefinitionKey] && (
+
+
+
+
{termDefinitions[termDefinitionKey]}
+
+
+ )}
+
+ );
+};
+
+const altPropTypes = {};
+
+const altDefaultProps = {
+ info:
+ // eslint-disable-next-line max-len
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. In lacinia elit sem, condimentum malesuada dolor imperdiet sit amet.',
+};
+
+export const DashboardWidgetAltHeading = ({
+ headingLabel,
+ headingValue,
+ info,
+ headingWrapperCn,
+ termDefinitionKey,
+}) => {
+ return (
+
+
+
{`${headingLabel}:`}
+
{headingValue}
+
+ {!!termDefinitionKey && !!termDefinitions[termDefinitionKey] && (
+
+
+
+
{termDefinitions[termDefinitionKey]}
+
+
+ )}
+
+ );
+};
+
+DashboardWidgetHeading.propTypes = propTypes;
+DashboardWidgetHeading.defaultProps = defaultProps;
+
+DashboardWidgetAltHeading.propTypes = altPropTypes;
+DashboardWidgetAltHeading.defaultProps = altDefaultProps;
+
+export default DashboardWidgetHeading;
diff --git a/src/components/dashboard-page/WidgetHeading/style.scss b/src/components/dashboard-page/WidgetHeading/style.scss
new file mode 100644
index 000000000..9f2c8f5d4
--- /dev/null
+++ b/src/components/dashboard-page/WidgetHeading/style.scss
@@ -0,0 +1,96 @@
+@import '../../../styles/main';
+
+.dashboard-widget-heading {
+ display: flex;
+ align-items: center;
+
+ &.base-margin {
+ margin-bottom: 16px;
+ }
+
+ &.dim-heading {
+ margin-bottom: 0;
+ }
+
+ &__heading {
+ @include dashboard-widget-heading;
+
+ & > span {
+ color: $dashboard-base-gray-text-color;
+ }
+
+ &.dim-heading {
+ @include dashboard-widget-helper-text;
+ }
+ }
+
+ &__icon-wrapper {
+ position: relative;
+ margin-left: 4px;
+ padding: 8px;
+ max-height: 32px;
+ border-radius: 50%;
+ color: $dashboard-widget-base-info-icon-fill;
+
+ &:hover {
+ color: $dashboard-widget-base-info-icon-states-fill;
+ background-color: $dashboard-widget-base-info-states-background-color;
+
+ & .dashboard-widget-heading__info-wrapper {
+ opacity: 1;
+ visibility: visible;
+ }
+ }
+ }
+
+ // as TractionCard modal
+
+ &__info-wrapper {
+ position: absolute;
+ right: -53px;
+ bottom: 36px;
+ z-index: 2000;
+ width: 139px;
+ margin: auto;
+ padding: 8px;
+ background-color: #343d44;
+ border-radius: 2px;
+ opacity: 0;
+ visibility: hidden;
+ transition: visibility 0.3s linear, opacity 0.3s linear;
+ transition-delay: 0.1s;
+ }
+
+ &__info {
+ @include t100;
+ color: $dashboard-base-white-text-color;
+ }
+
+ &__wrapper {
+ padding: 4px 12px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ background-color: $dashboard-widget-base-background-color;
+ border-radius: 4px;
+ }
+
+ &__label {
+ @include t300;
+ color: $dashboard-base-gray-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ &__value {
+ @include t300;
+ font-weight: 600;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+ }
+
+ // @media #{$screen-min-dashboard-xs} {
+ // &__info-wrapper {
+ // right: -53px;
+ // }
+ // }
+}
diff --git a/src/components/index-page/Traction/style.scss b/src/components/index-page/Traction/style.scss
index 15c5edff6..1a456663c 100644
--- a/src/components/index-page/Traction/style.scss
+++ b/src/components/index-page/Traction/style.scss
@@ -119,7 +119,7 @@
margin: auto;
padding: 8px;
color: $c_white;
- background-color: #343d44;
+ background-color: $dashboard-widget-info-modal-background-color;
border-radius: 2px;
opacity: 0;
visibility: hidden;
diff --git a/src/components/roadmap-page/Quarters/index.js b/src/components/roadmap-page/Quarters/index.js
index 58a429241..d15e834e5 100644
--- a/src/components/roadmap-page/Quarters/index.js
+++ b/src/components/roadmap-page/Quarters/index.js
@@ -16,7 +16,7 @@ import scrollToActiveElement from '../../../utils/scrollToActiveElement';
import './style.scss';
-const parseQuarters = data => {
+export const parseQuarters = data => {
if (data.length === 0) return [];
let index = 0;
diff --git a/src/data/pages/dashboard/termDefinitions.js b/src/data/pages/dashboard/termDefinitions.js
new file mode 100644
index 000000000..65de417f3
--- /dev/null
+++ b/src/data/pages/dashboard/termDefinitions.js
@@ -0,0 +1,77 @@
+/* eslint-disable quotes */
+/* eslint-disable max-len */
+
+export const termDefinitions = {
+ price:
+ 'Displays the current trading price of the token, its percentage change from the last week, and a detailed daily price chart with a volume chart overlay.',
+ marketcap:
+ 'The total market capitalization, derived from multiplying the current price by the circulating supply of tokens. Percentage change from the previous week is also shown.',
+ volume:
+ 'Total trading volume over the past 24 hours for markets trading Joystream. Percentage change from the previous week is also shown.',
+ fdv:
+ 'The theoretical market cap if all tokens were in circulation, calculated by multiplying the current price by the total supply.',
+ supply:
+ 'Shows the circulating supply (total number of tokens currently in circulation) and total supply (the total amount of tokens that exist, including those not currently circulating) of Joystream tokens.',
+ whereToBuyJoy:
+ 'Lists trading markets for JOY, the current 24-hour trading volume, and the capital needed to move the price by 2%.',
+ releaseSchedule:
+ 'A graph showing the release of JOY tokens into circulation from the genesis block to when all tokens will be fully circulated. Note: The vesting happens linearly (i.e., a chunk of JOY is released every block or every ~6 seconds).',
+ tokenAllocation:
+ 'Displays allocation of tokens on genesis block for different purposes. It shows absolute values, percentages of total supply as well as liquidity percentages of those tokens at genesis block.',
+ minting:
+ 'A pie chart indicating the percentage of tokens minted within the last year allocated for different purposes.',
+ annualInflation: 'The annual increase in the number of tokens, presented as a percentage of the total supply.',
+ supplyStakedForValidation: 'The percentage of the total JOY supply staked for validation purposes.',
+ apr:
+ 'The Annual Percentage Rate (APR) represents the yearly return you can expect from staking your assets on our platform. It quantifies the rewards earned from supporting the network through staking over a one-year period.',
+ roi: 'A table showing the potential return on investment based on the time the tokens were purchased.',
+ supplyDistribution:
+ 'A table displaying the distribution of JOY tokens across various address ranges and the percentage of the circulating supply they represent.',
+ including:
+ "Highlights several of Joystream's most significant investors. The complete list includes over 40 backers.",
+ finalVentureRound:
+ 'At its final venture round in March of 2022, Joystream raised $5.85 million which brought the total fundraise to $13 million at a $60 million valuation.',
+ contentCreators:
+ 'The current number of content creators, the weekly change, and a graph showing the trend over the last few months.',
+ videos: 'The current number of videos, the weekly change, and a graph showing the trend over the last few months.',
+ commentsAndReactions:
+ 'The current number of comments and reactions, the weekly change, and a graph showing the trend over the last few months.',
+ nfts:
+ 'The current trading volume of NFTs, the weekly change, and a graph showing the trend over the last few months.',
+ crts:
+ 'The current trading volume of CRTs, the weekly change, and a graph showing the trend over the last few months.',
+ chainMetrics: "This section provides key performance indicators for the blockchain's health and activity.",
+ averageBlockTime: 'The average duration in seconds for the network to produce a new block.',
+ transactions:
+ 'The number of transactions created on the blockchain since the genesis block. The weekly change in this value is also shown.',
+ holders: 'The total number of accounts holding more than 0 JOY. The weekly change in this value is also shown.',
+ dailyActiveAccounts:
+ 'The number of accounts that have interacted with the blockchain in the last 24 hours. The weekly change in this value is also shown.',
+ githubStats: 'Statistics from the Joystream GitHub organization, covering all active public repositories.',
+ stars: 'The total number of stars across all public Joystream repositories.',
+ commits: 'The total number of commits across all public Joystream repositories.',
+ commitsThisWeek: 'Number of commits made to Joystream’s repositories in this week.',
+ openPrs: 'The total number of open Pull Requests across all public Joystream repositories.',
+ openIssues: 'The total number of open Issues across all public Joystream repositories.',
+ repositories: 'Total number of public repositories associated with the Joystream GitHub organization.',
+ followers: 'The total number of users following the Joystream organization.',
+ contributions: 'A graph tracking the number of commits to Joystream’s repositories over time.',
+ contributors: 'Key contributors and developers involved in Joystream’s projects and repositories.',
+ tweetScout:
+ 'The TweetScout score and level of the official Joystream twitter account. To learn more about it, follow the link below.',
+ featuredFollowers: "Joystream Twitter account's most prominent followers.",
+ openEvents: 'Upcoming events on Joystream’s official Discord server.',
+ council:
+ "Council Members are elected by stakeholders in the system to act in the long-term interest of the platform. They are responsible for allocating resources, and hiring working group leads to run the platform's day-to-day operations.",
+ currentTerm:
+ 'The term number associated with the current council. This number denotes the number of different councils that were elected since the genesis block.',
+ termLength: 'The duration of the current council term.',
+ currentCouncil:
+ 'Information about the current council, including election time, term duration, salary, and council members.',
+ timesServed: 'The total number of terms served by an individual as a councilor.',
+ workingGroups:
+ 'A working group is an organizational body, subject to the oversight of the council, which is responsible for the day-to-day functioning of some subsystem of the Joystream platform. There is exactly one working group per subsystem.',
+ jsgenesis:
+ 'Jsgenesis is the company and legal entity initially responsible for building and developing the Joystream platform. Our role is to build the infrastructure, network and tools so that the users have a reliable foundation to keep the project running.',
+ positioning: 'Compares Joystream with other content protocols and services in both the web2 and web3 spaces.',
+};
diff --git a/src/pages/dashboard/index.js b/src/pages/dashboard/index.js
new file mode 100644
index 000000000..7b4b2aa69
--- /dev/null
+++ b/src/pages/dashboard/index.js
@@ -0,0 +1,94 @@
+import React, { useState } from 'react';
+import { graphql } from 'gatsby';
+import { useI18next, useTranslation } from 'gatsby-plugin-react-i18next';
+import scrollTo from 'gatsby-plugin-smoothscroll';
+
+import useAxios from '../../utils/useAxios';
+import { ScrollProvider } from '../../components/_enhancers/ScrollContext';
+import SiteMetadata from '../../components/SiteMetadata';
+
+import Header from '../../components/dashboard-page/Header';
+import Hero from '../../components/dashboard-page/Hero';
+import Token from '../../components/dashboard-page/Token';
+import Backers from '../../components/dashboard-page/Backers';
+import History from '../../components/dashboard-page/History';
+import Traction from '../../components/dashboard-page/Traction';
+import Engineering from '../../components/dashboard-page/Engineering';
+import Community from '../../components/dashboard-page/Community';
+import Team from '../../components/dashboard-page/Team';
+import Comparison from '../../components/dashboard-page/Comparison';
+import Roadmap from '../../components/dashboard-page/Roadmap';
+import Feature from '../../components/Feature';
+
+import { anchors } from '../../components/dashboard-page/Header/data';
+
+import './style.scss';
+
+const Dashboard = pageProps => {
+ // TODO: Add dashboard.json to locales/[locale] so that t func with appropriate keys can be used
+ const { language } = useI18next();
+ const { t } = useTranslation();
+
+ const [data, loading] = useAxios('https://status.joystream.org/dashboard-data');
+
+ const [withScrollInitiallyUp] = useState(() => !pageProps.location.hash);
+
+ const [activeAnchor, setActiveAnchor] = useState(() => anchors[0]);
+ const onAnchorClick = activeAnchor => {
+ setActiveAnchor(activeAnchor);
+ scrollTo(`#${activeAnchor.toLowerCase()}`);
+ };
+
+ const embedded = !pageProps.location.pathname.includes('/dashboard');
+ const historyHidden = true;
+
+ return (
+ <>
+ {/* TODO: Remove later (for demonstration purposes) */}
+
+
+ {!embedded &&
}
+
+
+ {!embedded && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default Dashboard;
+
+export const query = graphql`
+ query($language: String!) {
+ locales: allLocale(filter: { language: { eq: $language } }) {
+ ...LanguageQueryFields
+ }
+ }
+`;
diff --git a/src/pages/dashboard/style.scss b/src/pages/dashboard/style.scss
new file mode 100644
index 000000000..acb58293c
--- /dev/null
+++ b/src/pages/dashboard/style.scss
@@ -0,0 +1,11 @@
+@import '../../styles//main';
+
+.black-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background-color: #000;
+ z-index: -1;
+}
diff --git a/src/pages/index.js b/src/pages/index.js
index 52dd717bc..ccd749e57 100644
--- a/src/pages/index.js
+++ b/src/pages/index.js
@@ -18,10 +18,11 @@ import Video from '../components/index-page/Video';
import Traction from '../components/index-page/Traction';
import Upcoming from '../components/index-page/Upcoming';
import Creators from '../components/index-page/Creators';
+import Dashboard from './dashboard';
import './style.scss';
-const IndexPage = () => {
+const IndexPage = pageProps => {
const { t } = useTranslation();
const { language } = useI18next();
@@ -63,6 +64,8 @@ const IndexPage = () => {
+
+
);
};
diff --git a/src/styles/_main.scss b/src/styles/_main.scss
index 88f3b2d30..de6d8c0ff 100644
--- a/src/styles/_main.scss
+++ b/src/styles/_main.scss
@@ -5,3 +5,4 @@
@import './common/colors';
@import './common/text';
@import './common/structure';
+@import './layout/dashboard';
diff --git a/src/styles/common/_media.scss b/src/styles/common/_media.scss
index 05ad1b20f..e42469554 100644
--- a/src/styles/common/_media.scss
+++ b/src/styles/common/_media.scss
@@ -6,13 +6,13 @@ $breakpoints: (
xl: 1400px,
);
-@function media-min-width($screen-sizes-name) {
- $breakpoint: map-get($breakpoints, $screen-sizes-name);
+@function media-min-width($screen-sizes-name, $custom-breakpoints: $breakpoints) {
+ $breakpoint: map-get($custom-breakpoints, $screen-sizes-name);
@return ('only screen and (min-width: #{if($breakpoint, $breakpoint, $screen-sizes-name)})');
}
-@function media-max-width($screen-sizes-name) {
- $breakpoint: map-get($breakpoints, $screen-sizes-name);
+@function media-max-width($screen-sizes-name, $custom-breakpoints: $breakpoints) {
+ $breakpoint: map-get($custom-breakpoints, $screen-sizes-name);
@return ('only screen and (max-width: #{if($breakpoint, $breakpoint, $screen-sizes-name) - 1px})');
}
diff --git a/src/styles/layout/_dashboard.scss b/src/styles/layout/_dashboard.scss
new file mode 100644
index 000000000..3eb52bca9
--- /dev/null
+++ b/src/styles/layout/_dashboard.scss
@@ -0,0 +1,208 @@
+@import '../common/media';
+
+// vars
+
+$dashboard-breakpoints: (
+ xxs: 320px,
+ xs: 425px,
+ sm: 768px,
+ md: 1024px,
+ lg: 1440px,
+ xl: 1920px,
+ xxl: 2560px,
+);
+
+// media
+
+$screen-min-dashboard-xs: media-min-width('xs', $dashboard-breakpoints);
+$screen-min-dashboard-sm: media-min-width('sm', $dashboard-breakpoints);
+$screen-min-dashboard-md: media-min-width('md', $dashboard-breakpoints);
+$screen-min-dashboard-lg: media-min-width('lg', $dashboard-breakpoints);
+$screen-min-dashboard-xl: media-min-width('xl', $dashboard-breakpoints);
+$screen-min-dashboard-xxl: media-min-width('xxl', $dashboard-breakpoints);
+
+// layout bakground colors (with borders)
+
+$dashboard-header-background-color: #000000;
+$dashboard-navbar-background-color: #bcd5fa14;
+$dashboard-header-border-bottom-color: #bbd9f621;
+$dashboard-base-border-color: #cbe0f145;
+$dashboard-hero-video-player-modal-border-color: #7174ff;
+$dashboard-hero-video-player-modal-background-color: #181c20;
+$dashboard-modals-overlay-color: #101214bf;
+$dashboard-section-header-tooltip-background-color: #343d44;
+$dashboard-charts-custom-tooltip-background-color: #272d33;
+$dashboard-widget-tags-background-color: #272d33;
+$dashboard-backer-widget-background-color: #0f1114;
+$dashboard-history-modal-background-color: #272d33;
+$dashboard-custom-scrollbar-track-background: #bbd9f621;
+$dashboard-custom-scrollbar-thumb-background: #c2e0ff33;
+$dashboard-contibutor-widget-background-color: #0f1114;
+$dashboard-contibutor-widget-border-color: #dce1e56b;
+$dashboard-follower-widget-background-color: #0f1114;
+$dashboard-follower-widget-hover-border-color: #dce1e56b;
+$dashboard-open-event-widget-label-background-color: #0f1114;
+
+$dashboard-secondary-widget-background-color: #0f1114;
+
+// buttons background colors (with borders)
+
+$dashboard-header-chat-button-background-color: #bcd5fa14;
+$dashboard-header-buttons-border-color: #bbd9f621;
+$dashboard-buttons-base-hover-background-color: #bbd9f621;
+$dashboard-buttons-base-pressed-background-color: #bcd5fa14;
+$dashboard-header-navbar-buttons-border-color: #bbd9f621;
+$dahboard-header-navbar-buttons-states-background-color: #c2e0ff33;
+$dashboard-header-navbar-buttons-states-border-color: #dce1e56b;
+$dashboard-hero-play-video-button-background-color: rgba(0, 0, 0, 0.6);
+$dashboard-hero-play-video-button-hover-background-color: rgba(0, 0, 0, 0.7);
+$dashboard-widget-base-background-color: #bcd5fa14;
+$dashboard-widget-base-info-icon-fill: #7b8a95;
+$dashboard-widget-base-info-icon-states-fill: #f4f6f8;
+$dashboard-widget-base-info-states-background-color: #bbd9f621;
+$dashboard-widget-info-modal-background-color: #343d44;
+$dashboard-base-borders-color: #bbd9f621;
+$dashboard-widget-accent-background-color: #4038ff;
+$dashboard-widget-accent-hover-background-color: #5a58ff;
+$dashboard-widget-accent-active-background-color: #342ecf;
+$dashboard-carousel-buttons-background-color: #c2e0ff33;
+$dashboard-carousel-buttons-hover-background-color: #bbd9f621;
+$dashboard-carousel-buttons-pressed-background-color: #c2e0ff33;
+$dashboard-press-story-hover-border-color: #dce1e56b;
+$dashboard-history-modal-buttons-background-color: #c2e0ff33;
+$dashboard-exchange-button-background-color: #c2e0ff33;
+$dashboard-progress-bar-color: #bbd9f621;
+
+// buttons colors
+
+$dashboard-header-buttons-color: #f4f6f8;
+$dashboard-header-navbar-buttons-color: #b5c1c9;
+$dashboard-header-navbar-buttons-states-color: #f4f6f8;
+$dashboard-history-stages-buttons-color: #6c6cff;
+
+// text
+
+$dashboard-font-feature-settings: 'clig' off, 'liga' off;
+
+// text colors
+
+$dashboard-base-white-text-color: #ffffff;
+$dashboard-base-gray-text-color: #b5c1c9;
+$dashboard-content-base-text-color: #f4f6f8;
+$dashboard-charts-base-tick-color: #7b8a95;
+$dashboard-charts-tooltip-gray-color: #7b8a95;
+$dashboard-markdown-anchor-color: #7174ff;
+$dashboard-followers-gray-text-color: #7b8a95;
+$dashboard-open-events-gray-text-color: #7b8a95;
+
+// transition func and duration
+
+$dashboard-transition-timing: cubic-bezier(0, 0, 0.3, 1);
+$dashboard-transition-duration: 150ms;
+
+// sizes
+
+$dashboard-header-height: 65px;
+$dashboard-header-nav-height: 50px;
+$dashboard-header-sum-of-heights: calc($dashboard-header-height + $dashboard-header-nav-height);
+
+$dashboard-header-z-index: 1000;
+
+// mixins
+
+@mixin dashboard-section {
+ padding-block: 40px;
+
+ @media #{$screen-min-dashboard-xs} {
+ padding-block: 48px;
+ }
+
+ @media #{$screen-min-dashboard-sm} {
+ padding-block: 64px;
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ padding-block: 80px;
+ }
+}
+
+@mixin dashboard-container {
+ padding-inline: 16px;
+
+ @media #{$screen-min-dashboard-md} {
+ padding-inline: 32px;
+ }
+
+ @media #{$screen-min-dashboard-lg} {
+ width: 1440px;
+ margin: 0 auto;
+ }
+}
+
+@mixin dashboard-buttons-states {
+ transition: background-color $dashboard-transition-duration $dashboard-transition-timing;
+
+ &:hover,
+ &:focus {
+ background-color: $dashboard-buttons-base-hover-background-color;
+ }
+
+ &:active {
+ background-color: $dashboard-buttons-base-pressed-background-color;
+ transition-duration: 0ms;
+ }
+}
+
+@mixin dashboard-widget {
+ padding: 16px;
+ background-color: $dashboard-widget-base-background-color;
+ border-radius: 8px;
+
+ @media #{$screen-min-dashboard-sm} {
+ padding: 32px;
+ }
+}
+
+@mixin dashboard-widget-heading {
+ @include h400;
+ color: $dashboard-content-base-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+}
+
+@mixin dashboard-widget-text {
+ @include h500;
+ color: $dashboard-base-white-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+
+ @media #{$screen-min-dashboard-xs} {
+ @include h600;
+ }
+
+ @media #{$screen-min-dashboard-md} {
+ @include h700;
+ }
+}
+
+@mixin dashboard-widget-helper-text {
+ @include t300;
+ color: $dashboard-base-gray-text-color;
+ font-feature-settings: $dashboard-font-feature-settings;
+
+ @media #{$screen-min-dashboard-xs} {
+ @include t400;
+ }
+}
+
+@mixin dashboard-widget-heading-padding {
+ padding: 16px 16px 0px;
+ @media #{$screen-min-dashboard-sm} {
+ padding: 32px 32px 0px;
+ }
+}
+
+@mixin reset-dashboard-widget-padding {
+ padding: 0;
+ @media #{$screen-min-dashboard-sm} {
+ padding: 0;
+ }
+}
diff --git a/src/utils/getRandomInt/index.js b/src/utils/getRandomInt/index.js
new file mode 100644
index 000000000..cbf368f97
--- /dev/null
+++ b/src/utils/getRandomInt/index.js
@@ -0,0 +1,5 @@
+const getRandomInt = (min, max) => {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+};
+
+export default getRandomInt;
diff --git a/src/utils/useDashboardMedia/index.js b/src/utils/useDashboardMedia/index.js
new file mode 100644
index 000000000..80656aabf
--- /dev/null
+++ b/src/utils/useDashboardMedia/index.js
@@ -0,0 +1,31 @@
+import { useState, useEffect } from 'react';
+import { useMediaQuery } from 'react-responsive';
+
+export default function useDashboardMedia() {
+ const [currentBreakpoints, setCurrentBreakpoints] = useState('xxs');
+
+ const isXxs = useMediaQuery({ maxWidth: 424 });
+ const isXs = useMediaQuery({ minWidth: 425, maxWidth: 767 });
+ const isSm = useMediaQuery({ minWidth: 768, maxWidth: 1023 });
+ const isMd = useMediaQuery({ minWidth: 1024, maxWidth: 1439 });
+ const isLg = useMediaQuery({ minWidth: 1440, maxWidth: 1919 });
+ const isXl = useMediaQuery({ minWidth: 1920 });
+
+ useEffect(() => {
+ if (isXxs) {
+ setCurrentBreakpoints('xxs');
+ } else if (isXs) {
+ setCurrentBreakpoints('xs');
+ } else if (isSm) {
+ setCurrentBreakpoints('sm');
+ } else if (isMd) {
+ setCurrentBreakpoints('md');
+ } else if (isLg) {
+ setCurrentBreakpoints('lg');
+ } else if (isXl) {
+ setCurrentBreakpoints('xl');
+ }
+ }, [isXxs, isXs, isSm, isMd, isLg, isXl]);
+
+ return { currentBreakpoints };
+}
diff --git a/src/utils/withFallbackVal/index.js b/src/utils/withFallbackVal/index.js
new file mode 100644
index 000000000..5165d0ca9
--- /dev/null
+++ b/src/utils/withFallbackVal/index.js
@@ -0,0 +1,5 @@
+// Other funcs for different data types may be added like withFallbackStrVal etc.
+
+export const withFallbackNumVal = num => num || 0;
+
+export const isNaN = val => Number.isNaN(Number(val));
diff --git a/yarn.lock b/yarn.lock
index df9dd9b3f..9614da73a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1783,6 +1783,13 @@
lodash "^4.17.15"
moment "^2.24.0"
+"@mapbox/hast-util-table-cell-style@^0.2.0":
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.2.1.tgz#b8e92afdd38b668cf0762400de980073d2ade101"
+ integrity sha512-LyQz4XJIdCdY/+temIhD/Ed0x/p4GAOUycpFSEK2Ads1CPKZy6b7V/2ROEtQiLLQ8soIs0xe/QAoR6kwpyW/yw==
+ dependencies:
+ unist-util-visit "^1.4.1"
+
"@mdx-js/util@^2.0.0-next.8":
version "2.0.0-next.8"
resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-2.0.0-next.8.tgz#66ecc27b78e07a3ea2eb1a8fc5a99dfa0ba96690"
@@ -3043,6 +3050,13 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008"
integrity sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==
+"@types/mdast@^3.0.0":
+ version "3.0.15"
+ resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5"
+ integrity sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==
+ dependencies:
+ "@types/unist" "^2"
+
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@@ -3190,6 +3204,11 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==
+"@types/unist@^2", "@types/unist@^2.0.3":
+ version "2.0.10"
+ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc"
+ integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==
+
"@types/vfile-message@*":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/vfile-message/-/vfile-message-2.0.0.tgz#690e46af0fdfc1f9faae00cd049cc888957927d5"
@@ -6576,6 +6595,13 @@ debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6, debug@^3.2
dependencies:
ms "^2.1.1"
+debug@^4.0.0:
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+ integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+ dependencies:
+ ms "2.1.2"
+
debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
@@ -7078,6 +7104,15 @@ dom-serializer@^1.0.1, dom-serializer@~1.2.0:
domhandler "^4.0.0"
entities "^2.0.0"
+dom-serializer@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
+ integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==
+ dependencies:
+ domelementtype "^2.3.0"
+ domhandler "^5.0.2"
+ entities "^4.2.0"
+
dom-walk@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
@@ -7098,6 +7133,11 @@ domelementtype@^2.0.1, domelementtype@^2.1.0:
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e"
integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==
+domelementtype@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
+ integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
+
domexception@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
@@ -7112,13 +7152,6 @@ domhandler@^2.3.0:
dependencies:
domelementtype "1"
-domhandler@^3.3.0:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.3.0.tgz#6db7ea46e4617eb15cf875df68b2b8524ce0037a"
- integrity sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==
- dependencies:
- domelementtype "^2.0.1"
-
domhandler@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.0.0.tgz#01ea7821de996d85f69029e81fa873c21833098e"
@@ -7126,6 +7159,13 @@ domhandler@^4.0.0:
dependencies:
domelementtype "^2.1.0"
+domhandler@^5.0, domhandler@^5.0.2, domhandler@^5.0.3:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
+ integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
+ dependencies:
+ domelementtype "^2.3.0"
+
domutils@^1.5.1, domutils@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
@@ -7134,7 +7174,7 @@ domutils@^1.5.1, domutils@^1.7.0:
dom-serializer "0"
domelementtype "1"
-domutils@^2.4.2, domutils@^2.4.3, domutils@^2.4.4:
+domutils@^2.4.3, domutils@^2.4.4:
version "2.4.4"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3"
integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA==
@@ -7143,6 +7183,15 @@ domutils@^2.4.2, domutils@^2.4.3, domutils@^2.4.4:
domelementtype "^2.0.1"
domhandler "^4.0.0"
+domutils@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"
+ integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==
+ dependencies:
+ dom-serializer "^2.0.0"
+ domelementtype "^2.3.0"
+ domhandler "^5.0.3"
+
dot-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
@@ -7422,6 +7471,11 @@ entities@^2.0.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
+entities@^4.2.0, entities@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
+ integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
+
entities@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
@@ -9051,6 +9105,13 @@ gatsby-plugin-sharp@^2.14.4:
svgo "1.3.2"
uuid "3.4.0"
+gatsby-plugin-smoothscroll@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/gatsby-plugin-smoothscroll/-/gatsby-plugin-smoothscroll-1.2.0.tgz#249c0ad660e167043652b34277f44d16ca2bf4b6"
+ integrity sha512-wfIK06xwbNx91nHVg1YJwlLUJc0EmfWqV8KgvlNr6gFa9pqMx5Mprdp5jDRloAi3+9K0dVCybPO8FfaZ0i4HgA==
+ dependencies:
+ smoothscroll-polyfill "^0.4.4"
+
gatsby-plugin-split-css@^2.0.3:
version "2.0.3"
resolved "https://registry.npmjs.org/gatsby-plugin-split-css/-/gatsby-plugin-split-css-2.0.3.tgz#843c0121b846c08e2e5418ee56ca55c972ee296b"
@@ -10062,6 +10123,19 @@ hasha@^5.2.0:
is-stream "^2.0.0"
type-fest "^0.8.0"
+hast-to-hyperscript@^9.0.0:
+ version "9.0.1"
+ resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz#9b67fd188e4c81e8ad66f803855334173920218d"
+ integrity sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA==
+ dependencies:
+ "@types/unist" "^2.0.3"
+ comma-separated-tokens "^1.0.0"
+ property-information "^5.3.0"
+ space-separated-tokens "^1.0.0"
+ style-to-object "^0.3.0"
+ unist-util-is "^4.0.0"
+ web-namespaces "^1.0.0"
+
hast-util-parse-selector@^2.0.0:
version "2.2.5"
resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a"
@@ -10216,14 +10290,13 @@ html-tags@^3.0.0:
integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==
html-to-react@^1.3.4:
- version "1.4.5"
- resolved "https://registry.yarnpkg.com/html-to-react/-/html-to-react-1.4.5.tgz#59091c11021d1ef315ef738460abb6a4a41fe1ce"
- integrity sha512-KONZUDFPg5OodWaQu2ymfkDmU0JA7zB1iPfvyHehTmMUZnk0DS7/TyCMTzsLH6b4BvxX15g88qZCXFhJWktsmA==
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/html-to-react/-/html-to-react-1.7.0.tgz#1664a0233a930ab1b12c442ddef0f1b72e7459f4"
+ integrity sha512-b5HTNaTGyOj5GGIMiWVr1k57egAZ/vGy0GGefnCQ1VW5hu9+eku8AXHtf2/DeD95cj/FKBKYa1J7SWBOX41yUQ==
dependencies:
- domhandler "^3.3.0"
- htmlparser2 "^5.0"
+ domhandler "^5.0"
+ htmlparser2 "^9.0"
lodash.camelcase "^4.3.0"
- ramda "^0.27.1"
html-webpack-plugin@^4.0.0-beta.2:
version "4.5.1"
@@ -10252,16 +10325,6 @@ htmlparser2@^3.10.0, htmlparser2@^3.10.1:
inherits "^2.0.1"
readable-stream "^3.1.1"
-htmlparser2@^5.0:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-5.0.1.tgz#7daa6fc3e35d6107ac95a4fc08781f091664f6e7"
- integrity sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==
- dependencies:
- domelementtype "^2.0.1"
- domhandler "^3.3.0"
- domutils "^2.4.2"
- entities "^2.0.0"
-
htmlparser2@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.0.tgz#c2da005030390908ca4c91e5629e418e0665ac01"
@@ -10272,6 +10335,16 @@ htmlparser2@^6.0.0:
domutils "^2.4.4"
entities "^2.0.0"
+htmlparser2@^9.0:
+ version "9.1.0"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.1.0.tgz#cdb498d8a75a51f739b61d3f718136c369bc8c23"
+ integrity sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==
+ dependencies:
+ domelementtype "^2.3.0"
+ domhandler "^5.0.3"
+ domutils "^3.1.0"
+ entities "^4.5.0"
+
http-cache-semantics@3.8.1:
version "3.8.1"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2"
@@ -12359,7 +12432,7 @@ lock@^1.0.0:
lodash.camelcase@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
- integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY=
+ integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==
lodash.clonedeep@4.5.0:
version "4.5.0"
@@ -12730,6 +12803,43 @@ mdast-util-compact@^2.0.0:
dependencies:
unist-util-visit "^2.0.0"
+mdast-util-definitions@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz#c5c1a84db799173b4dcf7643cda999e440c24db2"
+ integrity sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==
+ dependencies:
+ unist-util-visit "^2.0.0"
+
+mdast-util-from-markdown@^0.8.0:
+ version "0.8.5"
+ resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz#d1ef2ca42bc377ecb0463a987910dae89bd9a28c"
+ integrity sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+ mdast-util-to-string "^2.0.0"
+ micromark "~2.11.0"
+ parse-entities "^2.0.0"
+ unist-util-stringify-position "^2.0.0"
+
+mdast-util-to-hast@^10.2.0:
+ version "10.2.0"
+ resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-10.2.0.tgz#61875526a017d8857b71abc9333942700b2d3604"
+ integrity sha512-JoPBfJ3gBnHZ18icCwHR50orC9kNH81tiR1gs01D8Q5YpV6adHNO9nKNuFBCJQ941/32PT1a63UF/DitmS3amQ==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+ "@types/unist" "^2.0.0"
+ mdast-util-definitions "^4.0.0"
+ mdurl "^1.0.0"
+ unist-builder "^2.0.0"
+ unist-util-generated "^1.0.0"
+ unist-util-position "^3.0.0"
+ unist-util-visit "^2.0.0"
+
+mdast-util-to-string@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz#b8cfe6a713e1091cb5b728fc48885a4767f8b97b"
+ integrity sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==
+
mdn-data@2.0.14:
version "2.0.14"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
@@ -12740,6 +12850,11 @@ mdn-data@2.0.4:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==
+mdurl@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
+ integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==
+
meant@^1.0.1, meant@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/meant/-/meant-1.0.3.tgz#67769af9de1d158773e928ae82c456114903554c"
@@ -12848,6 +12963,14 @@ microevent.ts@~0.1.1:
resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0"
integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==
+micromark@~2.11.0:
+ version "2.11.4"
+ resolved "https://registry.yarnpkg.com/micromark/-/micromark-2.11.4.tgz#d13436138eea826383e822449c9a5c50ee44665a"
+ integrity sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==
+ dependencies:
+ debug "^4.0.0"
+ parse-entities "^2.0.0"
+
micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8:
version "3.1.10"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
@@ -15130,7 +15253,7 @@ proper-lockfile@^4.1.1:
retry "^0.12.0"
signal-exit "^3.0.2"
-property-information@^5.0.0:
+property-information@^5.0.0, property-information@^5.3.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69"
integrity sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==
@@ -15328,11 +15451,6 @@ ramda@^0.25.0:
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.25.0.tgz#8fdf68231cffa90bc2f9460390a0cb74a29b29a9"
integrity sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ==
-ramda@^0.27.1:
- version "0.27.1"
- resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9"
- integrity sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==
-
randexp@0.4.6:
version "0.4.6"
resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
@@ -15812,6 +15930,16 @@ react-refresh@^0.8.3:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
+react-remark@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/react-remark/-/react-remark-2.1.0.tgz#dd68a32ab2d022e598b27dbfb754400e8f68555c"
+ integrity sha512-7dEPxRGQ23sOdvteuRGaQAs9cEOH/BOeCN4CqsJdk3laUDIDYRCWnM6a3z92PzXHUuxIRLXQNZx7SiO0ijUcbw==
+ dependencies:
+ rehype-react "^6.0.0"
+ remark-parse "^9.0.0"
+ remark-rehype "^8.0.0"
+ unified "^9.0.0"
+
react-resize-detector@^6.6.3:
version "6.7.2"
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-6.7.2.tgz#780901b8b5f6a26cccbb12fc0518e03196d58c50"
@@ -16266,6 +16394,14 @@ regjsparser@^0.6.4:
dependencies:
jsesc "~0.5.0"
+rehype-react@^6.0.0:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/rehype-react/-/rehype-react-6.2.1.tgz#9b9bf188451ad6f63796b784fe1f51165c67b73a"
+ integrity sha512-f9KIrjktvLvmbGc7si25HepocOg4z0MuNOtweigKzBcDjiGSTGhyz6VSgaV5K421Cq1O+z4/oxRJ5G9owo0KVg==
+ dependencies:
+ "@mapbox/hast-util-table-cell-style" "^0.2.0"
+ hast-to-hyperscript "^9.0.0"
+
relateurl@^0.2.7:
version "0.2.7"
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
@@ -16335,6 +16471,20 @@ remark-parse@^6.0.0, remark-parse@^6.0.3:
vfile-location "^2.0.0"
xtend "^4.0.1"
+remark-parse@^9.0.0:
+ version "9.0.0"
+ resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-9.0.0.tgz#4d20a299665880e4f4af5d90b7c7b8a935853640"
+ integrity sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==
+ dependencies:
+ mdast-util-from-markdown "^0.8.0"
+
+remark-rehype@^8.0.0:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-8.1.0.tgz#610509a043484c1e697437fa5eb3fd992617c945"
+ integrity sha512-EbCu9kHgAxKmW1yEYjx3QafMyGY3q8noUbNUI5xyKbaFP89wbhDrKxyIQNukNYthzjNHZu6J7hwFg7hRm1svYA==
+ dependencies:
+ mdast-util-to-hast "^10.2.0"
+
remark-stringify@^6.0.0:
version "6.0.4"
resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-6.0.4.tgz#16ac229d4d1593249018663c7bddf28aafc4e088"
@@ -18799,6 +18949,18 @@ unified@^8.4.2:
trough "^1.0.0"
vfile "^4.0.0"
+unified@^9.0.0:
+ version "9.2.2"
+ resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.2.tgz#67649a1abfc3ab85d2969502902775eb03146975"
+ integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==
+ dependencies:
+ bail "^1.0.0"
+ extend "^3.0.0"
+ is-buffer "^2.0.0"
+ is-plain-obj "^2.0.0"
+ trough "^1.0.0"
+ vfile "^4.0.0"
+
union-value@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
@@ -18840,6 +19002,11 @@ unique-string@^2.0.0:
dependencies:
crypto-random-string "^2.0.0"
+unist-builder@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-2.0.3.tgz#77648711b5d86af0942f334397a33c5e91516436"
+ integrity sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==
+
unist-util-find-all-after@^1.0.2:
version "1.0.5"
resolved "https://registry.yarnpkg.com/unist-util-find-all-after/-/unist-util-find-all-after-1.0.5.tgz#5751a8608834f41d117ad9c577770c5f2f1b2899"
@@ -18847,6 +19014,11 @@ unist-util-find-all-after@^1.0.2:
dependencies:
unist-util-is "^3.0.0"
+unist-util-generated@^1.0.0:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-1.1.6.tgz#5ab51f689e2992a472beb1b35f2ce7ff2f324d4b"
+ integrity sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==
+
unist-util-is@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-3.0.0.tgz#d9e84381c2468e82629e4a5be9d7d05a2dd324cd"
@@ -18857,6 +19029,11 @@ unist-util-is@^4.0.0:
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.0.4.tgz#3e9e8de6af2eb0039a59f50c9b3e99698a924f50"
integrity sha512-3dF39j/u423v4BBQrk1AQ2Ve1FxY5W3JKwXxVFzBODQ6WEvccguhgp802qQLKSnxPODE6WuRZtV+ohlUg4meBA==
+unist-util-position@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-3.1.0.tgz#1c42ee6301f8d52f47d14f62bbdb796571fa2d47"
+ integrity sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==
+
unist-util-remove-position@^1.0.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-1.1.4.tgz#ec037348b6102c897703eee6d0294ca4755a2020"
@@ -18903,7 +19080,7 @@ unist-util-visit-parents@^3.0.0:
"@types/unist" "^2.0.0"
unist-util-is "^4.0.0"
-unist-util-visit@^1.1.0, unist-util-visit@^1.3.0:
+unist-util-visit@^1.1.0, unist-util-visit@^1.3.0, unist-util-visit@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.4.1.tgz#4724aaa8486e6ee6e26d7ff3c8685960d560b1e3"
integrity sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==
@@ -19632,6 +19809,11 @@ weakmap-polyfill@2.0.4:
resolved "https://registry.npmjs.org/weakmap-polyfill/-/weakmap-polyfill-2.0.4.tgz#bcc301e4c8eb4eda3e406f08f1a691093e407884"
integrity sha512-ZzxBf288iALJseijWelmECm/1x7ZwQn3sMYIkDr2VvZp7r6SEKuT8D0O9Wiq6L9Nl5mazrOMcmiZE/2NCenaxw==
+web-namespaces@^1.0.0:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec"
+ integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==
+
web-vitals@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/web-vitals/-/web-vitals-1.1.2.tgz#06535308168986096239aa84716e68b4c6ae6d1c"