diff --git a/.stylelintrc b/.stylelintrc index 6050faa70..47f4d234b 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -220,6 +220,9 @@ "speak" ], { "unspecified": "bottomAlphabetical" } - ] + ], + "declaration-block-trailing-semicolon": null, + "length-zero-no-unit": null, + "declaration-block-single-line-max-declarations": null } } diff --git a/README.md b/README.md index b8d4a405a..adae1f9e4 100644 --- a/README.md +++ b/README.md @@ -138,3 +138,33 @@ To edit members, one just needs to update the objects that are already there to Things to keep note of: - The verification page will use the `memberHandle` object property value as the route on the website for this specific profile. If that value is not a valid path value then the `substituteUserRoute` value is used instead. This should be removed when not in use. - The safety list items and title have special strings as text (e.g., `verification.title.outreachSpecialist`). These strings are used to allow for internationalization and are replaced based off of the language set by the user with correct translations. To reference those that are already pre-written/available, you can find the translations for this page on here: `src/locales/en/verification.json` under `safety.can...` and `safety.willNever...`. If there isn't a translation that matches one that you want to add, you are free to create one with regular english language sentences and these translations will be created by the dev during the PR process. + +------------ + +This section outlines the steps to control the visibility of the "History" section on the dashboard page. + +#### Functionality: + +- toggling the `historyHidden` flag, located in `/pages/dashboard/index.js`, controls the visibility of the "History" section on the dashboard; +- when the flag is set to `true`, the section is hidden, and the corresponding "History" anchor is removed from the navigation; +- conversely, setting the flag to `false` displays the section and adds the "History" anchor back to the navigation. + +------------ + +This section details the process of populating the "History" section on the dashboard page with data retrieved from an API. + +#### Data Parsing: + +- `DashboardHistory` component expects data to be passed through the data prop; +- create a dedicated utility function within `src/components/dashboard-page/History/data.js` to handle API data parsing; +- this function should transform the raw API response into an array of objects structured as follows: + - `img` - URL of the image associated with the history entry; + - `date` - Date of the history entry; + - `shortDescr` - Short description of the history entry; + - `longDescr` - Optional longer description of the history entry; +- import the parsing function into `src/components/dashboard-page/History/data.js`; +- pass the fetched API data as an argument to the parsing function. + +The function will return the processed data in the required format. + +------------ \ No newline at end of file diff --git a/gatsby-config.js b/gatsby-config.js index 2208ea68f..e48aa11aa 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -24,7 +24,7 @@ module.exports = { }, }, }, - 'gatsby-plugin-anchor-links', + // 'gatsby-plugin-anchor-links', 'gatsby-transformer-sharp', 'gatsby-plugin-sharp', { diff --git a/package.json b/package.json index aca341ab1..f3e77c6c1 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "gatsby-plugin-sass": "^5.25.0", "gatsby-plugin-sharp": "^2.14.4", "gatsby-plugin-split-css": "^2.0.3", + "gatsby-plugin-smoothscroll": "^1.2.0", "gatsby-plugin-stylelint": "^3.2.0", "gatsby-plugin-svgr": "^2.0.2", "gatsby-source-filesystem": "^2.1.0", @@ -59,6 +60,7 @@ "react-modal": "^3.8.2", "react-player": "^2.6.0", "react-plx": "^2.0.1", + "react-remark": "^2.1.0", "react-responsive": "^9.0.0-beta.6", "react-spring": "^8.0.27", "react-waypoint": "^10.1.0", @@ -104,7 +106,7 @@ "stylelint:fix": "stylelint \"src/**/*.scss\" --fix", "lint:all": "yarn lint && yarn stylelint --fix", "build": "NODE_OPTIONS=--openssl-legacy-provider gatsby build", - "develop": "NODE_OPTIONS=--openssl-legacy-provider gatsby develop", + "develop": "cross-env NODE_OPTIONS=--openssl-legacy-provider gatsby develop", "format": "prettier --write src/**/*.{js,jsx}", "start": "yarn develop", "serve": "NODE_OPTIONS=--openssl-legacy-provider gatsby serve", diff --git a/public/lightpaper.pdf b/public/lightpaper.pdf deleted file mode 100644 index 8d3dfc692..000000000 Binary files a/public/lightpaper.pdf and /dev/null differ diff --git a/public/lightpaper_ru.pdf b/public/lightpaper_ru.pdf deleted file mode 100644 index 54b9ef089..000000000 Binary files a/public/lightpaper_ru.pdf and /dev/null differ diff --git a/public/verification.htm b/public/verification.htm deleted file mode 100644 index fd9e04a24..000000000 --- a/public/verification.htm +++ /dev/null @@ -1 +0,0 @@ -

Jsgenesis.com Verification

It is hereby verifified that the domain jsgenesis.com is authoritative for the project, and that the following trusted e-mails can be trusted

  1. cmc@jsgenesis.com
  2. bedeho@jsgenesis.com

Jsgenesis is the entity that was organised the team that developed the Joystream blockchain and network. IMPORTANTLY, it can be trusted for the purposes of verifying CMC Annex C information.

diff --git a/src/assets/images/dashboard/biconomy-logo.png b/src/assets/images/dashboard/biconomy-logo.png new file mode 100644 index 000000000..7cc58b113 Binary files /dev/null and b/src/assets/images/dashboard/biconomy-logo.png differ diff --git a/src/assets/images/dashboard/bitget-logo.png b/src/assets/images/dashboard/bitget-logo.png new file mode 100644 index 000000000..8ba721106 Binary files /dev/null and b/src/assets/images/dashboard/bitget-logo.png differ diff --git a/src/assets/images/dashboard/bitmart-logo.png b/src/assets/images/dashboard/bitmart-logo.png new file mode 100644 index 000000000..1ff402761 Binary files /dev/null and b/src/assets/images/dashboard/bitmart-logo.png differ diff --git a/src/assets/images/dashboard/coindesk-article-alt-screenshot.png b/src/assets/images/dashboard/coindesk-article-alt-screenshot.png new file mode 100644 index 000000000..62183847c Binary files /dev/null and b/src/assets/images/dashboard/coindesk-article-alt-screenshot.png differ diff --git a/src/assets/images/dashboard/dashboard-hero-video-overlay.png b/src/assets/images/dashboard/dashboard-hero-video-overlay.png new file mode 100644 index 000000000..e3dcb27cf Binary files /dev/null and b/src/assets/images/dashboard/dashboard-hero-video-overlay.png differ diff --git a/src/assets/images/dashboard/founders/bedeho.png b/src/assets/images/dashboard/founders/bedeho.png new file mode 100644 index 000000000..c35a79c72 Binary files /dev/null and b/src/assets/images/dashboard/founders/bedeho.png differ diff --git a/src/assets/images/dashboard/founders/mokhtar.png b/src/assets/images/dashboard/founders/mokhtar.png new file mode 100644 index 000000000..e50f34e02 Binary files /dev/null and b/src/assets/images/dashboard/founders/mokhtar.png differ diff --git a/src/assets/images/dashboard/gatel-o-logo.png b/src/assets/images/dashboard/gatel-o-logo.png new file mode 100644 index 000000000..92cd6ccc4 Binary files /dev/null and b/src/assets/images/dashboard/gatel-o-logo.png differ diff --git a/src/assets/images/dashboard/generic-event-picture.png b/src/assets/images/dashboard/generic-event-picture.png new file mode 100644 index 000000000..469a99262 Binary files /dev/null and b/src/assets/images/dashboard/generic-event-picture.png differ diff --git a/src/assets/images/dashboard/glass-video-overlay-texture.png b/src/assets/images/dashboard/glass-video-overlay-texture.png new file mode 100644 index 000000000..20b74ff4a Binary files /dev/null and b/src/assets/images/dashboard/glass-video-overlay-texture.png differ diff --git a/src/assets/images/dashboard/history-stage-feb-2023.jpeg b/src/assets/images/dashboard/history-stage-feb-2023.jpeg new file mode 100644 index 000000000..140c60f8f Binary files /dev/null and b/src/assets/images/dashboard/history-stage-feb-2023.jpeg differ diff --git a/src/assets/images/dashboard/history-stage-jul-2023.jpg b/src/assets/images/dashboard/history-stage-jul-2023.jpg new file mode 100644 index 000000000..085dbbfb1 Binary files /dev/null and b/src/assets/images/dashboard/history-stage-jul-2023.jpg differ diff --git a/src/assets/images/dashboard/history-stage-jun-2019.jpg b/src/assets/images/dashboard/history-stage-jun-2019.jpg new file mode 100644 index 000000000..5f3fcb4c0 Binary files /dev/null and b/src/assets/images/dashboard/history-stage-jun-2019.jpg differ diff --git a/src/assets/images/dashboard/history-stage-nov-2019.webp b/src/assets/images/dashboard/history-stage-nov-2019.webp new file mode 100644 index 000000000..179fd4204 Binary files /dev/null and b/src/assets/images/dashboard/history-stage-nov-2019.webp differ diff --git a/src/assets/images/dashboard/mexc-logo.png b/src/assets/images/dashboard/mexc-logo.png new file mode 100644 index 000000000..5bf5a903d Binary files /dev/null and b/src/assets/images/dashboard/mexc-logo.png differ diff --git a/src/assets/images/dashboard/tweetscout-logo.png b/src/assets/images/dashboard/tweetscout-logo.png new file mode 100644 index 000000000..1b0fa09a3 Binary files /dev/null and b/src/assets/images/dashboard/tweetscout-logo.png differ diff --git a/src/assets/images/dashboard/working-groups/apps.png b/src/assets/images/dashboard/working-groups/apps.png new file mode 100644 index 000000000..6e24c2155 Binary files /dev/null and b/src/assets/images/dashboard/working-groups/apps.png differ diff --git a/src/assets/images/dashboard/working-groups/builders.png b/src/assets/images/dashboard/working-groups/builders.png new file mode 100644 index 000000000..bb1409434 Binary files /dev/null and b/src/assets/images/dashboard/working-groups/builders.png differ diff --git a/src/assets/images/dashboard/working-groups/content.png b/src/assets/images/dashboard/working-groups/content.png new file mode 100644 index 000000000..a2bfdf1be Binary files /dev/null and b/src/assets/images/dashboard/working-groups/content.png differ diff --git a/src/assets/images/dashboard/working-groups/distribution.png b/src/assets/images/dashboard/working-groups/distribution.png new file mode 100644 index 000000000..348b39749 Binary files /dev/null and b/src/assets/images/dashboard/working-groups/distribution.png differ diff --git a/src/assets/images/dashboard/working-groups/forum.png b/src/assets/images/dashboard/working-groups/forum.png new file mode 100644 index 000000000..6bbc9b451 Binary files /dev/null and b/src/assets/images/dashboard/working-groups/forum.png differ diff --git a/src/assets/images/dashboard/working-groups/hr.png b/src/assets/images/dashboard/working-groups/hr.png new file mode 100644 index 000000000..0f119898a Binary files /dev/null and b/src/assets/images/dashboard/working-groups/hr.png differ diff --git a/src/assets/images/dashboard/working-groups/marketing.png b/src/assets/images/dashboard/working-groups/marketing.png new file mode 100644 index 000000000..fd6c51127 Binary files /dev/null and b/src/assets/images/dashboard/working-groups/marketing.png differ diff --git a/src/assets/images/dashboard/working-groups/membership.png b/src/assets/images/dashboard/working-groups/membership.png new file mode 100644 index 000000000..bf7a6559f Binary files /dev/null and b/src/assets/images/dashboard/working-groups/membership.png differ diff --git a/src/assets/images/dashboard/working-groups/storage.png b/src/assets/images/dashboard/working-groups/storage.png new file mode 100644 index 000000000..04dc4bf97 Binary files /dev/null and b/src/assets/images/dashboard/working-groups/storage.png differ diff --git a/src/assets/images/dashboard/xt-logo.png b/src/assets/images/dashboard/xt-logo.png new file mode 100644 index 000000000..05a6c72ce Binary files /dev/null and b/src/assets/images/dashboard/xt-logo.png differ diff --git a/src/assets/svg/dashboard/accent-pointer.svg b/src/assets/svg/dashboard/accent-pointer.svg new file mode 100644 index 000000000..f2780617f --- /dev/null +++ b/src/assets/svg/dashboard/accent-pointer.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svg/dashboard/arrow-back.svg b/src/assets/svg/dashboard/arrow-back.svg new file mode 100644 index 000000000..97ec95393 --- /dev/null +++ b/src/assets/svg/dashboard/arrow-back.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/dashboard/cancel-rejected-icon.svg b/src/assets/svg/dashboard/cancel-rejected-icon.svg new file mode 100644 index 000000000..08da91f3d --- /dev/null +++ b/src/assets/svg/dashboard/cancel-rejected-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/dashboard/chat-button-icon.svg b/src/assets/svg/dashboard/chat-button-icon.svg new file mode 100644 index 000000000..04b37506f --- /dev/null +++ b/src/assets/svg/dashboard/chat-button-icon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/assets/svg/dashboard/check-accepted-icon.svg b/src/assets/svg/dashboard/check-accepted-icon.svg new file mode 100644 index 000000000..42cedd06d --- /dev/null +++ b/src/assets/svg/dashboard/check-accepted-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/dashboard/chevron-right.svg b/src/assets/svg/dashboard/chevron-right.svg new file mode 100644 index 000000000..fc676568f --- /dev/null +++ b/src/assets/svg/dashboard/chevron-right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svg/dashboard/copy-link-icon.svg b/src/assets/svg/dashboard/copy-link-icon.svg new file mode 100644 index 000000000..ff2817007 --- /dev/null +++ b/src/assets/svg/dashboard/copy-link-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svg/dashboard/custom-chart-dot.svg b/src/assets/svg/dashboard/custom-chart-dot.svg new file mode 100644 index 000000000..f175bcaa4 --- /dev/null +++ b/src/assets/svg/dashboard/custom-chart-dot.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/dashboard/dashboard-play-video-icon.svg b/src/assets/svg/dashboard/dashboard-play-video-icon.svg new file mode 100644 index 000000000..eae9363fa --- /dev/null +++ b/src/assets/svg/dashboard/dashboard-play-video-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svg/dashboard/discord-logo.svg b/src/assets/svg/dashboard/discord-logo.svg new file mode 100644 index 000000000..fdbddcac8 --- /dev/null +++ b/src/assets/svg/dashboard/discord-logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/svg/dashboard/empty-avatar.svg b/src/assets/svg/dashboard/empty-avatar.svg new file mode 100644 index 000000000..50ea972b1 --- /dev/null +++ b/src/assets/svg/dashboard/empty-avatar.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/svg/dashboard/error.svg b/src/assets/svg/dashboard/error.svg new file mode 100644 index 000000000..e85186da5 --- /dev/null +++ b/src/assets/svg/dashboard/error.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svg/dashboard/exchanger-icon.svg b/src/assets/svg/dashboard/exchanger-icon.svg new file mode 100644 index 000000000..cf8d5fc7f --- /dev/null +++ b/src/assets/svg/dashboard/exchanger-icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/svg/dashboard/exclamation-mark-icon.svg b/src/assets/svg/dashboard/exclamation-mark-icon.svg new file mode 100644 index 000000000..a8039a989 --- /dev/null +++ b/src/assets/svg/dashboard/exclamation-mark-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/dashboard/gleev-logo.svg b/src/assets/svg/dashboard/gleev-logo.svg new file mode 100644 index 000000000..1e59f0255 --- /dev/null +++ b/src/assets/svg/dashboard/gleev-logo.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/svg/dashboard/info-icon.svg b/src/assets/svg/dashboard/info-icon.svg new file mode 100644 index 000000000..957c1ac56 --- /dev/null +++ b/src/assets/svg/dashboard/info-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/dashboard/mail.svg b/src/assets/svg/dashboard/mail.svg new file mode 100644 index 000000000..0ac24605c --- /dev/null +++ b/src/assets/svg/dashboard/mail.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/svg/dashboard/new-tab.svg b/src/assets/svg/dashboard/new-tab.svg new file mode 100644 index 000000000..ee5ee0240 --- /dev/null +++ b/src/assets/svg/dashboard/new-tab.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svg/dashboard/next-story-pointer.svg b/src/assets/svg/dashboard/next-story-pointer.svg new file mode 100644 index 000000000..ad6db9b15 --- /dev/null +++ b/src/assets/svg/dashboard/next-story-pointer.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svg/dashboard/play-alt-icon.svg b/src/assets/svg/dashboard/play-alt-icon.svg new file mode 100644 index 000000000..17e9c81e2 --- /dev/null +++ b/src/assets/svg/dashboard/play-alt-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/dashboard/prev-story-pointer.svg b/src/assets/svg/dashboard/prev-story-pointer.svg new file mode 100644 index 000000000..b3b1d66ca --- /dev/null +++ b/src/assets/svg/dashboard/prev-story-pointer.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/dashboard/sound.svg b/src/assets/svg/dashboard/sound.svg new file mode 100644 index 000000000..bf5495809 --- /dev/null +++ b/src/assets/svg/dashboard/sound.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/svg/dashboard/success.svg b/src/assets/svg/dashboard/success.svg new file mode 100644 index 000000000..784f85a22 --- /dev/null +++ b/src/assets/svg/dashboard/success.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svg/dashboard/telegram-logo.svg b/src/assets/svg/dashboard/telegram-logo.svg new file mode 100644 index 000000000..6eed16127 --- /dev/null +++ b/src/assets/svg/dashboard/telegram-logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/svg/dashboard/toggle-button-chevron.svg b/src/assets/svg/dashboard/toggle-button-chevron.svg new file mode 100644 index 000000000..99631b26b --- /dev/null +++ b/src/assets/svg/dashboard/toggle-button-chevron.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svg/dashboard/twitter-logo.svg b/src/assets/svg/dashboard/twitter-logo.svg new file mode 100644 index 000000000..6800b3c50 --- /dev/null +++ b/src/assets/svg/dashboard/twitter-logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svg/dashboard/warning-icon.svg b/src/assets/svg/dashboard/warning-icon.svg new file mode 100644 index 000000000..651be7673 --- /dev/null +++ b/src/assets/svg/dashboard/warning-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg/dashboard/white-cross.svg b/src/assets/svg/dashboard/white-cross.svg new file mode 100644 index 000000000..fd1c9c15a --- /dev/null +++ b/src/assets/svg/dashboard/white-cross.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Feature/index.js b/src/components/Feature/index.js new file mode 100644 index 000000000..954e324fa --- /dev/null +++ b/src/components/Feature/index.js @@ -0,0 +1,14 @@ +import { bool, node } from 'prop-types'; + +const propTypes = { + disabled: bool, + children: node.isRequired, +}; + +const Feature = ({ disabled, children }) => { + return disabled ? null : children; +}; + +Feature.propTypes = propTypes; + +export default Feature; diff --git a/src/components/_enhancers/ScrollContext.js b/src/components/_enhancers/ScrollContext.js index 5c01f7740..cf660e810 100644 --- a/src/components/_enhancers/ScrollContext.js +++ b/src/components/_enhancers/ScrollContext.js @@ -2,18 +2,23 @@ import React, { useRef, useEffect, useState, createContext } from 'react'; export const ScrollContext = createContext({}); -export const ScrollProvider = ({ children }) => { +export const ScrollProvider = ({ children, minScrollDeltaThreshold = 0, withScrollInitiallyUp = false }) => { const scrollPositionRef = useRef(0); - const [isScrollUp, setIsScrollUp] = useState(false); + const [isScrollUp, setIsScrollUp] = useState(() => (withScrollInitiallyUp ? true : false)); useEffect(() => { const handleScroll = () => { - setIsScrollUp(scrollPositionRef.current > window.pageYOffset); - scrollPositionRef.current = window.pageYOffset; + const currentScrollY = window.pageYOffset; + const scrollDelta = scrollPositionRef.current - currentScrollY; + + if (Math.abs(scrollDelta) > minScrollDeltaThreshold) { + setIsScrollUp(scrollDelta > 0); + scrollPositionRef.current = currentScrollY; + } }; window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); - }, []); + }, [minScrollDeltaThreshold]); return ( diff --git a/src/components/about-page/Press/index.js b/src/components/about-page/Press/index.js index 7d2e77aec..62780e5d0 100644 --- a/src/components/about-page/Press/index.js +++ b/src/components/about-page/Press/index.js @@ -1,18 +1,21 @@ import React from 'react'; import { useTranslation } from 'gatsby-plugin-react-i18next'; +import cn from 'classnames'; import { ReactComponent as ArrowIcon } from '../../../assets/svg/arrow-down-small.svg'; import CoindeskArticleScreenshot from '../../../assets/images/coindesk-article-screenshot.webp'; +import CoindeskArticleAltScreenshot from '../../../assets/images/dashboard/coindesk-article-alt-screenshot.png'; import CoindeskLogo from '../../../assets/images/coindesk-logo.webp'; import './style.scss'; -const PressStory = ({ t }) => ( +export const PressStory = ({ t, pressStoryLinkCn, withAltArticleScreenshot }) => (
@@ -30,7 +33,7 @@ const PressStory = ({ t }) => (
coindesk article screenshot
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 ( + + ); +}; + +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 ( +
+
{children}
+
+ ); +}; + +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)} + > + {`${username}-avatar`} +

{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}

+
+
+ +

{discordVoice}

+
+
+ {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 ( +
+ + + + +
+
+ +

Telegram

+

+ {parseSocialMediaMemberCount(data, 'telegramMemberCount')} +

+ + +

+2% Last month

+
+
+
+ + +
+
+
+ tweetscout-logo + +
+
+

{parseTweetscoutScore(data)}

+

{parseTweetscoutLevel(data)}

+
+ +
+
+
+
+ ); +}; + +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) => ( + + ))} + + + + {data.map((rowData, index) => ( + + {columns.map((column, index) => { + return ( + + ); + })} + + ))} + +
+ {column.header} +
+ {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 ( +
+
+ + + {loading ? : } +
+
+ ); +}; + +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 ( +
+ + +
+ ); +}; + +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 ( +
+ +

{value}

+
+ ); +}; + +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 ( +
+
+
+ + + + + + + + + +
+
+ + {transitions.map( + ({ item, key, props }) => + item && ( + + + + ) + )} +
+ ); +}; + +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 && ( +
+ dashboard-hero-video-overlay + +
+ )} +
+
+ + + + + ); +}; + +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 =>
  1. , + }, + }, + }); + + 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} + > +
    + joystream-history-stage-img +
    +
    +
    +
    +

    {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} +

    + +
    +
    + +

    + {currentStageIdx + 1} + {` / ${stagesQuantity}`} +

    + +
    +
    + ); +}; + +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}-logo`} +

    {platformName}

    +
      +
    • +
      + +

      {requiresKyc ? 'Requires KYC' : 'Doesn’t require KYC'}

      +
      +
    • +
    • +
      + {acceptsUsersFromPoland ? : } +

      + {acceptsUsersFromPoland ? 'Accepting users from: Poland' : 'Not accepting users from: Poland'} +

      +
      +
    • +
    • +
      + +

      {`Pair: ${exchangePair[0]} <-> ${exchangePair[1]}`}

      +
      +
    • +
    + +
    + ); +}; + +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}`}

    + + + +
    +
    + {deliveryMilestones.map((deliveryMilestone, index) => { + return ( + +
    +
    + delivery-milestone-icon +
    +

    {`${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}

    + +
    +

    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) => ( + + ))} + + + + {data.map((rowData, index) => ( + + {columns.map((column, index) => ( + + ))} + + ))} + +
    + {column.header} +
    + {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 ( + + ); +}; + +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.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 ( +
    +
    + +
    +

    Co-Founder

    +

    {name}

    +
    + +
    + {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}

    +
    + ); + })} +
    + +
    + +

    {timesServed}

    +
    +
    +
    +
    + ); +}; + +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 ( + +
    +
    +
    + +
    +
    +
    + working-group-logo +
    +

    {name}

    +
    + +
    +
    +

    WG Lead:

    +
    +
    +

    {lead?.username}

    +
    +
    + +
    +

    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 ( +
    +
    + +
    + exchange-option-logo +

    {name}

    +
    + +
    Volume (24h)
    +

    ${formatNumberWithCommas(volume)}

    + +
      +
    • +
      +2% Depth
      +

      ${formatNumberWithCommas(depthUp2)}

      +
    • +
    • +
      -2% Depth
      +

      ${formatNumberWithCommas(depthDown2)}

      +
    • +
    +
    + ); +}; + +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 && ( + + )} + + + )} +
    + ); +}; + +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)}

    +
    +
      +
    • +

      Price:

      +

      ${Number(payload[0].payload.price).toFixed(5)}

      +
    • +
    • +

      Vol 24h:

      +

      ${Number(payload[0].payload.volume).toFixed(2)}

      +
    • +
    +
    + ); + } + + 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 ; + })} +
    +
    + +

    Joy tokens does not have max supply

    +
    + + + +
    +
    +
    + ); +}; + +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"