View on GitHub
File Changes
import '@storybook/addon-actions/register';
import '@storybook/addon-links/register';
+
import '@storybook/addon-knobs/register';
-
import { configure } from '@storybook/react';
+
import { configure, addDecorator } from '@storybook/react';
+
import { ThemeDecorator } from '../stories/support/ThemeDecorator';

                      
-
// automatically import all files ending in *.stories.js
const req = require.context('../stories', true, /\.stories\.tsx$/);
function loadStories() {
  req.keys().forEach(filename => req(filename));
}

                      
+
addDecorator(story => <ThemeDecorator>{story()}</ThemeDecorator>);
+

                      
configure(loadStories, module);
m
+1/-1
const Dotenv = require('dotenv-webpack');

                      
// TODO: get these variables
-
let themeResource = 'testnet';
+
let themeResource = 'incentivized-testnet';
const resourcesDir = path.join(__dirname, 'source/styles/resources');
const resources = [
  `${resourcesDir}/mixins/**/*.scss`,
m
+3/-0
  },
  "dependencies": {
    "@svgr/webpack": "^4.3.2",
+
    "@types/chroma-js": "^1.4.3",
    "@types/classnames": "2.2.9",
    "@types/color": "3.0.0",
    "@types/qrcode.react": "^0.9.0",
    "apollo-link-http": "1.5.15",
    "apollo-link-ws": "1.0.17",
    "array-sync": "4.0.0",
+
    "chroma-js": "^2.0.6",
    "classnames": "2.2.6",
    "color": "3.1.2",
    "debug": "4.1.1",
    "@graphql-codegen/typescript-operations": "1.2.0",
    "@storybook/addon-actions": "5.1.11",
    "@storybook/addon-info": "5.1.11",
+
    "@storybook/addon-knobs": "^5.2.3",
    "@storybook/addon-links": "5.1.11",
    "@storybook/addons": "5.1.11",
    "@storybook/react": "5.1.11",
.epochInfoContainer {
  margin: 61.5px 0;
+
  font-family: ProximaNova;

                      
  .header {
    margin-bottom: 41px;
+
// import ApolloClient from 'apollo-client';
+

                      
+
// import {
+
//   GetBlocksQuery,
+
//   GetBlocksQueryVariables,
+
// } from '../../../generated/typings/graphql-schema';
+
// import { GraphQLRequest } from '../../utils/graphql/GraphQLRequest';
+
// import getBlocksQuery from './graphql/getBlocks.graphql';
+

                      
+
export class StakePoolsApi {
+
  // public getBlocksByIdsQuery: GraphQLRequest<
+
  //   GetBlocksQuery,
+
  //   GetBlocksQueryVariables
+
  // >;
+
  // constructor(client: ApolloClient<any>) {
+
  //   // this.getBlocksByIdsQuery = new GraphQLRequest<
+
  //   //   GetBlocksQuery,
+
  //   //   GetBlocksQueryVariables
+
  //   // >(client, getBlocksQuery);
+
  // }
+
}
+
.stakePoolThumbnailContainer {
+
  background-color: #2a2b3c;
+
  border-radius: 4px;
+
  height: 71px;
+
  position: relative;
+
  width: 80px;
+

                      
+
  &:hover {
+
    @extend .isHighlighted;
+
  }
+

                      
+
  > button {
+
    background: transparent;
+
    border: none;
+
    cursor: pointer;
+
    height: 100%;
+
    position: absolute;
+
    width: 100%;
+
    z-index: 1;
+
    &:focus {
+
      outline: none;
+
    }
+
  }
+
}
+

                      
+
.content {
+
  cursor: pointer;
+
  height: 100%;
+
  padding: 12px 0 0;
+
  width: 100%;
+
}
+

                      
+
.isHighlighted {
+
  background-color: $popup-bg;
+
}
+

                      
+
.ticker {
+
  color: $solid-text;
+
  font-size: 14px;
+
  font-weight: 600;
+
  letter-spacing: -0.5px;
+
  line-height: 1;
+
  margin: 0 0 4px;
+
  text-align: center;
+
}
+

                      
+
.ranking {
+
  font-size: 20px;
+
  font-weight: bold;
+
  text-align: center;
+
}
+

                      
+
.clock {
+
  border-radius: 0 2px 0 4px;
+
  height: 18px;
+
  margin: 1px 0 0;
+
  position: absolute;
+
  right: 0;
+
  top: 0;
+
  width: 18px;
+
  svg {
+
    height: 18px;
+
    width: 18px;
+
    > g > path {
+
      fill: $secondary-half-color;
+
    }
+
  }
+
}
+

                      
+
.colorBand {
+
  border-radius: 0 0 4px 4px;
+
  bottom: 0;
+
  display: block;
+
  height: 5px;
+
  left: 0;
+
  position: absolute;
+
  width: 100%;
+
}
+
import classnames from 'classnames';
+
import { observer } from 'mobx-react-lite';
+
import React from 'react';
+
import { IStakePoolThumbnailProps } from '../types';
+
import styles from './StakePoolThumbnail.scss';
+

                      
+
const ClockIcon = require('../../../static/assets/images/stake-pools/clock-icon.svg');
+

                      
+
const StakePoolThumbnail = ({
+
  isSelected,
+
  onSelect,
+
  stakePool,
+
  color,
+
  children,
+
}: IStakePoolThumbnailProps) => {
+
  const { ranking, ticker, retiring, id } = stakePool;
+
  const containerStyles = classnames([
+
    styles.stakePoolThumbnailContainer,
+
    isSelected ? styles.isHighlighted : null,
+
  ]);
+
  return (
+
    <div className={containerStyles}>
+
      <button onClick={onSelect} />
+
      <div className={styles.content}>
+
        <div className={styles.ticker}>{ticker}</div>
+
        <div className={styles.ranking} style={{ color }}>
+
          {ranking}
+
        </div>
+
        {retiring && (
+
          <div className={styles.clock}>
+
            <ClockIcon className={styles.clockIcon} />
+
          </div>
+
        )}
+
        <div
+
          className={styles.colorBand}
+
          style={{
+
            background: color,
+
          }}
+
        />
+
      </div>
+
      {children}
+
    </div>
+
  );
+
};
+
export default observer(StakePoolThumbnail);
+
$vertical: -38px;
+
$horizontal: 77px;
+

                      
+
.stakePoolTooltipContainer {
+
  background: $popup-bg;
+
  border-radius: 5px;
+
  box-shadow: 0 5px 20px 0 $shadow-color;
+
  padding: 10px;
+
  position: absolute;
+
  user-select: text;
+
  width: 240px;
+
  z-index: 2;
+

                      
+
  &:before {
+
    border-color: transparent;
+
    border-style: solid;
+
    content: '';
+
    display: block;
+
    height: 0;
+
    position: absolute;
+
    width: 0;
+
  }
+

                      
+
  &.top {
+
    top: $vertical;
+
    &:before {
+
      top: 61px;
+
    }
+
  }
+
  &.right {
+
    right: $horizontal;
+
    &:before {
+
      border-left-color: $popup-bg;
+
      border-width: 10px 0px 10px 10px;
+
      right: -10px;
+
    }
+
  }
+
  &.bottom {
+
    bottom: $vertical;
+
    &:before {
+
      bottom: 61px;
+
    }
+
  }
+
  &.left {
+
    left: $horizontal;
+
    &:before {
+
      border-right-color: $popup-bg;
+
      border-width: 10px 10px 10px 0px;
+
      left: -10px;
+
    }
+
  }
+
}
+

                      
+
.container {
+
  padding: 10px 10px 0;
+
}
+

                      
+
.colorBand {
+
  border-radius: 4px 4px 0 0;
+
  display: block;
+
  height: 5px;
+
  left: 0;
+
  position: absolute;
+
  top: 0;
+
  width: 100%;
+
}
+

                      
+
.name {
+
  color: $solid-text;
+
  font-family: ProximaNova;
+
  font-size: 16px;
+
  letter-spacing: 0.5px;
+
  line-height: 1.38;
+
  margin: 0 0 3px;
+
  max-height: 44px;
+
  overflow: hidden;
+
  padding-right: 25px;
+
  text-overflow: ellipsis;
+
}
+

                      
+
.closeButton {
+
  background: none;
+
  border: none;
+
  cursor: pointer;
+
  padding: 0;
+
  position: absolute;
+
  right: 20px;
+
  top: 25px;
+

                      
+
  &:focus {
+
    outline: 0;
+
  }
+

                      
+
  svg {
+
    height: 8px;
+
    width: 8px;
+

                      
+
    path {
+
      fill: $solid-text;
+
    }
+
  }
+
}
+

                      
+
.ticker {
+
  color: $solid-text;
+
  display: inline-block;
+
  font-size: 14px;
+
  line-height: 1.36;
+
  margin-bottom: 9px;
+
  opacity: 0.6;
+
  vertical-align: middle;
+
}
+

                      
+
.retirement {
+
  background: $secondary-half-color;
+
  border-radius: 3px;
+
  color: $solid-text;
+
  display: inline-block;
+
  font-size: 10px;
+
  margin-bottom: 9px;
+
  margin-left: 8px;
+
  padding: 3px 9px;
+
  vertical-align: middle;
+
}
+

                      
+
.description {
+
  color: $solid-text;
+
  font-size: 14px;
+
  line-height: 1.36;
+
  margin-bottom: 4px;
+
  max-height: 79px;
+
  overflow: hidden;
+
  text-overflow: ellipsis;
+
}
+

                      
+
.url {
+
  color: $secondary-highlight-color;
+
  border-bottom: 1px solid $secondary-highlight-color;
+
  font-family: ProximaNova;
+
  font-size: 12px;
+
  line-height: 1.36;
+
  max-width: 100%;
+

                      
+
  .urlContent {
+
    display: inline-block;
+
    max-width: calc(100% - 14px);
+
    overflow: hidden;
+
    text-overflow: ellipsis;
+
    vertical-align: middle;
+
  }
+

                      
+
  .urlIcon {
+
    height: 13px;
+
    width: 13px;
+
    margin-left: 2px;
+
    vertical-align: middle;
+

                      
+
    path {
+
      stroke: $secondary-highlight-color;
+
    }
+
  }
+
}
+

                      
+
.table {
+
  // @extend %regularText;
+
  display: flex;
+
  flex-wrap: wrap;
+
  margin-bottom: 14px;
+

                      
+
  dt,
+
  dd {
+
    height: 19px;
+
    margin-bottom: 6px;
+

                      
+
    &:last-of-type {
+
      margin-bottom: 0;
+
    }
+
  }
+

                      
+
  dt {
+
    color: $solid-text;
+
    font-size: 14px;
+
    line-height: 1.36;
+
    opacity: 0.8;
+
    width: 60%;
+
  }
+

                      
+
  dd {
+
    line-height: 0;
+
    margin: 0;
+
    padding-top: 1px;
+
    text-align: right;
+
    width: 40%;
+
  }
+

                      
+
  span {
+
    border-radius: 3px;
+
    color: $solid-text;
+
    display: inline-block;
+
import classnames from 'classnames';
+
import moment from 'moment';
+
import React, { FC, useCallback, useEffect, useRef } from 'react';
+
import { MouseEvent } from 'react';
+
import { getColorFromRange } from '../../../utils/colors';
+
import { IStakePoolTooltipProps } from '../types';
+
import styles from './StakePoolTooltip.scss';
+

                      
+
const CloseCrossIcon = require('../../../static/assets/images/stake-pools/close-cross.svg');
+
const ExternalLinkIcon = require('../../../static/assets/images/stake-pools/link-ic.svg');
+

                      
+
const StakePoolTooltip: FC<IStakePoolTooltipProps> = ({
+
  stakePool,
+
  onClose,
+
  position,
+
  color,
+
}) => {
+
  const {
+
    name,
+
    description,
+
    ticker,
+
    url,
+
    ranking,
+
    controlledStake,
+
    profitMargin,
+
    performance,
+
    retiring,
+
  } = stakePool;
+

                      
+
  const darken = 1;
+
  const alpha = 0.3;
+
  const reverse = true;
+
  const retirementFromNow = retiring ? moment(retiring).fromNow(true) : '';
+
  const colorBand = getColorFromRange(ranking);
+

                      
+
  const colorBandStyle = {
+
    backgroundColor: colorBand,
+
  };
+

                      
+
  const stakePoolTooltipStyles = classnames([
+
    styles.stakePoolTooltipContainer,
+
    styles[position.vertical],
+
    styles[position.horizontal],
+
  ]);
+

                      
+
  const handleOuterClick = useCallback((event: Event) => {
+
    const target = event.target as HTMLElement;
+
    if (tooltipNode.current && !tooltipNode.current.contains(target)) {
+
      onClose();
+
    }
+
  }, []);
+

                      
+
  useEffect(() => {
+
    document.body.addEventListener('click', handleOuterClick);
+
    return () => {
+
      document.body.removeEventListener('click', handleOuterClick);
+
    };
+
  }, [handleOuterClick]);
+

                      
+
  const tooltipNode = useRef<HTMLDivElement>(null);
+

                      
+
  return (
+
    <div className={stakePoolTooltipStyles} ref={tooltipNode}>
+
      <div className={styles.colorBand} style={colorBandStyle} />
+
      <div className={styles.container}>
+
        <h3 className={styles.name}>{name}</h3>
+
        <button className={styles.closeButton} onClick={() => onClose()}>
+
          <CloseCrossIcon />
+
        </button>
+
        <div className={styles.ticker}>{ticker}</div>
+
        {retiring && (
+
          <div className={styles.retirement}>
+
            Retiring in {retirementFromNow}
+
          </div>
+
        )}
+
        <div className={styles.description}>{description}</div>
+
        <a className={styles.url} href={url} target="_blank">
+
          <span className={styles.urlContent}>{url}</span>
+
          <ExternalLinkIcon className={styles.urlIcon} />
+
        </a>
+
        <dl className={styles.table}>
+
          <dt>Ranking</dt>
+
          <dd className={styles.ranking}>
+
            <span
+
              style={{
+
                background: getColorFromRange(ranking, { darken, alpha }),
+
              }}
+
            >
+
              {ranking}
+
            </span>
+
          </dd>
+
          <dt>ControlledStake</dt>
+
          <dd className={styles.controlledStake}>
+
            <span
+
              style={{
+
                background: getColorFromRange(controlledStake, {
+
                  alpha,
+
                  darken,
+
                }),
+
              }}
+
            >
+
              {controlledStake}%
+
            </span>
+
          </dd>
+
          <dt>ProfitMargin</dt>
+
          <dd className={styles.profitMargin}>
+
            <span
+
              style={{
+
                background: getColorFromRange(profitMargin, {
+
                  alpha,
+
                  darken,
+
                  reverse,
+
                }),
+
              }}
+
            >
+
              {profitMargin}%
+
            </span>
+
          </dd>
+
          <dt>Performance</dt>
+
          <dd className={styles.performance}>
+
            <span
+
              style={{
+
                background: getColorFromRange(performance, {
+
                  alpha,
+
                  darken,
+
                  reverse,
+
                }),
+
              }}
+
            >
+
              {performance}%
+
            </span>
+
          </dd>
+
        </dl>
+
      </div>
+
    </div>
+
  );
+
};
+
export default StakePoolTooltip;
+
.stakePoolsContainer {
+
  margin-top: 70px;
+
}
+
import { debounce } from 'lodash';
+
import { observer } from 'mobx-react-lite';
+
import React, { Component } from 'react';
+
import DividerWithTitle from '../../widgets/divider-with-title/components/DividerWithTitle';
+
import { getFilteredStakePoolsList } from '../helpers';
+
import {
+
  IStakePoolProps,
+
  IStakePoolsListProps,
+
  IStakePoolsProps,
+
} from '../types';
+
import styles from './StakePools.scss';
+
import StakePoolsList from './StakePoolsList';
+
import StakePoolsSearch from './StakePoolsSearch';
+

                      
+
interface IState {
+
  selectedPoolId: string;
+
  search: string;
+
}
+
const initialState = {
+
  search: '',
+
  selectedPoolId: '',
+
};
+
export default class StakePools extends Component<IStakePoolsProps, IState> {
+
  public state = {
+
    ...initialState,
+
  };
+
  public componentDidMount() {
+
    window.addEventListener('resize', this.handleResize);
+
  }
+
  public componentWillUnmount() {
+
    window.removeEventListener('resize', this.handleClose);
+
  }
+
  public handleResize = () =>
+
    debounce(this.handleClose, 200, { leading: true, trailing: false });
+
  public handleSelect = (selectedPoolId: string) => {
+
    return this.setState({
+
      selectedPoolId,
+
    });
+
  };
+
  public handleClose = () => {
+
    this.setState({
+
      ...initialState,
+
    });
+
  };
+
  public handleSearch = (search: string) => {
+
    this.setState({ search });
+
  };
+
  public render() {
+
    const { selectedPoolId, search } = this.state;
+
    const { stakePoolsList } = this.props;
+
    const filteredStakePoolsList: Array<
+
      IStakePoolProps
+
    > = getFilteredStakePoolsList(stakePoolsList, search);
+
    return (
+
      <div className={styles.stakePoolsContainer}>
+
        <DividerWithTitle title="Stake pools" />
+
        <StakePoolsSearch search={search} onSearch={this.handleSearch} />
+
        <StakePoolsList
+
          stakePoolsList={filteredStakePoolsList}
+
          selectedPoolId={selectedPoolId}
+
          onSelect={this.handleSelect}
+
          onClose={this.handleClose}
+
        />
+
      </div>
+
    );
+
  }
+
}
+
.stakePoolsListContainer {
+
  display: grid;
+
  font-family: ProximaNova;
+
  grid-gap: 10px;
+
  grid-template-columns: repeat(auto-fill, 80px);
+
  justify-content: space-between;
+
}
+
import classnames from 'classnames';
+
import { observer } from 'mobx-react-lite';
+
import React, { FC, MouseEvent, useState } from 'react';
+
import { getColorFromRange } from '../../../utils/colors';
+
import { getTooltipPosition } from '../helpers';
+
import {
+
  IStakePoolProps,
+
  IStakePoolsListProps,
+
  IStakePoolTooltipPositionProps,
+
} from '../types';
+
import styles from './StakePoolsList.scss';
+
import StakePoolThumbnail from './StakePoolThumbnail';
+
import StakePoolTooltip from './StakePoolTooltip';
+

                      
+
const StakePoolsList: FC<IStakePoolsListProps> = ({
+
  stakePoolsList,
+
  onSelect,
+
  selectedPoolId,
+
  onClose,
+
}) => {
+
  const [position, setTooltipState] = useState<
+
    IStakePoolTooltipPositionProps | any
+
  >({});
+
  const handleSelect = (id: string, event: MouseEvent<HTMLElement>) => {
+
    setTooltipState(getTooltipPosition(event));
+
    onSelect(id);
+
  };
+
  const colorOptions = { domain: [0, stakePoolsList.length] };
+
  return (
+
    <div className={styles.stakePoolsListContainer}>
+
      {stakePoolsList.map((stakePool: IStakePoolProps) => {
+
        const { id, ranking } = stakePool;
+
        const color = getColorFromRange(stakePool.ranking, colorOptions);
+
        const isSelected = id === selectedPoolId;
+
        return (
+
          <StakePoolThumbnail
+
            color={color}
+
            isSelected={isSelected}
+
            key={id}
+
            onSelect={handleSelect.bind(null, stakePool.id)}
+
            stakePool={stakePool}
+
          >
+
            {isSelected && (
+
              <StakePoolTooltip
+
                stakePool={stakePool}
+
                color={color}
+
                position={position}
+
                onClose={onClose}
+
              />
+
            )}
+
          </StakePoolThumbnail>
+
        );
+
      })}
+
    </div>
+
  );
+
};
+
export default observer(StakePoolsList);
+
.stakePoolsSearchContainer {
+
  margin-bottom: 70px;
+
}
+
import { observer } from 'mobx-react-lite';
+
import { Input } from 'react-polymorph/lib/components/Input';
+
import { IStakePoolsSearchProps } from '../types';
+
import styles from './StakePoolsSearch.scss';
+

                      
+
const StakePoolsSearch = ({ search, onSearch }: IStakePoolsSearchProps) => {
+
  return (
+
    <div className={styles.stakePoolsSearchContainer}>
+
      <Input
+
        placeholder="Search for a specific stake pool"
+
        value={search}
+
        onChange={(v: string) => onSearch(v)}
+
      />
+
    </div>
+
  );
+
};
+
export default observer(StakePoolsSearch);
+
import { observer } from 'mobx-react-lite';
+
import React from 'react';
+
import StakePools from '../components/StakePools';
+
import { useStakePools } from '../hooks';
+

                      
+
export const StakePoolsComponentContainerRaw = () => {
+
  const { store } = useStakePools();
+
  const { stakePoolsList } = store;
+

                      
+
  return <StakePools stakePoolsList={stakePoolsList} />;
+
};
+
export const StakePoolsComponentContainer = observer(
+
  StakePoolsComponentContainerRaw
+
);
+
import React from 'react';
+

                      
+
import { stakePoolsContext } from '../contexts';
+
import { blocksContextDefault } from '../index';
+
import { StakePoolsComponentContainer } from './StakePoolsComponentContainer';
+

                      
+
const StakePoolsContainer = () => (
+
  <stakePoolsContext.Provider value={blocksContextDefault}>
+
    <StakePoolsComponentContainer />
+
  </stakePoolsContext.Provider>
+
);
+

                      
+
export default StakePoolsContainer;
+
import React from 'react';
+
import { IStakePoolsContext } from './types';
+

                      
+
export const stakePoolsContext = React.createContext<IStakePoolsContext | null>(
+
  null
+
);
+
import { MouseEvent } from 'react';
+
import { IStakePoolProps, IStakePoolTooltipPositionProps } from './types';
+

                      
+
const SEARCH_FIELDS = ['ticker', 'name'];
+
const stakePoolsListSearch = (
+
  stakePool: IStakePoolProps,
+
  rawSearch: string
+
) => {
+
  const search = rawSearch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').trim();
+
  let pass = !search;
+
  SEARCH_FIELDS.forEach((field: string) => {
+
    if (!pass) {
+
      pass = RegExp(search, 'i').test((window as any)[field]);
+
    }
+
  });
+
  return pass;
+
};
+
export const getFilteredStakePoolsList = (
+
  stakePoolsList: Array<IStakePoolProps>,
+
  search: string
+
): Array<IStakePoolProps> =>
+
  stakePoolsList.filter((stakePool: IStakePoolProps) =>
+
    stakePoolsListSearch(stakePool, search)
+
  );
+
export const getTooltipPosition = (
+
  event: MouseEvent<HTMLElement>
+
): IStakePoolTooltipPositionProps => {
+
  const button = event.target as HTMLButtonElement;
+
  const target = button.parentNode as HTMLElement;
+
  const { top: thumbTop, left: thumbLeft } = target.getBoundingClientRect();
+
  const vertical = thumbTop > window.innerHeight / 2 ? 'bottom' : 'top';
+
  const horizontal = thumbLeft > window.innerWidth / 2 ? 'right' : 'left';
+
  return { vertical, horizontal };
+
};
Diff too large – View on GitHub