Ver código fonte

KPI + Leaderboard

Joystream Stats 2 anos atrás
pai
commit
5e7c8f815f

+ 141 - 0
src/components/KPI/Leaderboard.tsx

@@ -0,0 +1,141 @@
+import { Link } from "react-router-dom";
+import {
+  Accordion,
+  AccordionSummary,
+  createStyles,
+  Grid,
+  makeStyles,
+  Theme,
+  Typography,
+} from "@material-ui/core";
+import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
+import { Tokenomics } from "../../types";
+import { Loading } from "..";
+
+interface LeaderboardMember {
+  id: number;
+  handle: string;
+  totalEarnedUsd: number;
+  totalEarnedTjoy: number;
+  timesElected: number;
+  usdPerElection: number;
+  tjoyPerElection: number;
+}
+interface CouncilMember {
+  id: number;
+  handle: string;
+  rewardUsd: 51;
+  rewardTjoy: number;
+}
+interface Kpi {
+  kpi: number;
+  totalPossibleRewardsUsd: 3525;
+  councilMembers: CouncilMember[];
+}
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {
+      textAlign: "center",
+      backgroundColor: "#000",
+      color: "#fff",
+    },
+    acc: {
+      color: "#fff",
+      backgroundColor: "#4138ff",
+    },
+    heading: {
+      fontSize: theme.typography.pxToRem(25),
+      fontWeight: theme.typography.fontWeightRegular,
+    },
+  })
+);
+
+const Leaderboard = (props: { tokenomics: Tokenomics }) => {
+  const { tokenomics, leaderboard } = props;
+  const price = tokenomics ? tokenomics.price : 30 / 1000000;
+  const classes = useStyles();
+  if (!leaderboard.length) return <Loading target="data" />;
+
+  return (
+    <Grid className={classes.root} item lg={12}>
+      <Typography variant="h2" className="mb-3">
+        KPI Grading
+      </Typography>
+
+      {leaderboard ? (
+        <div>
+          <div className="d-flex flex-row text-right font-weight-bold">
+            <div className="col-2">Member</div>
+            <div className="col-1">Times Elected</div>
+            <div className="col-2">Total Earned USD</div>
+            <div className="col-2">Total Earned M tJOY</div>
+            <div className="col-2">USD per Election</div>
+            <div className="col-2">M tJOY per Election</div>
+          </div>
+
+          {leaderboard
+            .sort((a, b) => b.totalEarnedUsd - a.totalEarnedUsd)
+            .slice(0, 20)
+            .map((m: LeaderboardMember) => (
+              <div className="d-flex flex-row text-right mt-1">
+                <div className="col-2">
+                  <Link className="text-light" to={`/members/${m.handle}`}>
+                    {m.handle}
+                  </Link>
+                </div>
+                <div className="col-1">{m.timesElected}</div>
+                <div className="col-2">$ {m.totalEarnedUsd}</div>
+                <div className="col-2">
+                  {(m.totalEarnedUsd / price / 1000000).toFixed(1)}
+                </div>
+                <div className="col-2">{m.usdPerElection}</div>
+                <div className="col-2">
+                  {(m.usdPerElection / price / 1000000).toFixed(1)}
+                </div>
+              </div>
+            ))}
+        </div>
+      ) : (
+        <div />
+      )}
+
+      {kpi.map((round: Kpi) => (
+        <Accordion className={classes.acc} key={round.kpi}>
+          <AccordionSummary
+            expandIcon={<ExpandMoreIcon style={{ color: "#fff" }} />}
+            aria-controls={`${round.kpi}-content`}
+            id={`${round.kpi}-header`}
+          >
+            <h3>Round {round.kpi}</h3>
+            <div className="ml-2">(max: $ {round.totalPossibleRewardsUsd})</div>
+          </AccordionSummary>
+
+          <div className="d-flex flex-column">
+            <div className="d-flex flex-row text-right font-weight-bold">
+              <div className="col-2">Member</div>
+              <div className="col-2">Reward USD</div>
+              <div className="col-2">Reward M tJOY</div>
+            </div>
+
+            {round.councilMembers
+              .sort((a, b) => b.rewardUsd - a.rewardUsd)
+              .map((m) => (
+                <div className="d-flex flex-row text-right">
+                  <div className="col-2">
+                    <Link to={`/members/${m.handle}`}>{m.handle}</Link>
+                  </div>
+                  <div className="col-2">$ {m.rewardUsd}</div>
+                  <div className="col-2">
+                    {(m.rewardUsd / price / 1000000).toFixed(1)}
+                  </div>
+                </div>
+              ))}
+          </div>
+        </Accordion>
+      ))}
+    </Grid>
+  );
+};
+
+export default Leaderboard;

+ 116 - 0
src/components/KPI/Round/KpiBox.tsx

@@ -0,0 +1,116 @@
+import { useState } from "react";
+import { Button } from "react-bootstrap";
+import { Edit } from "react-feather";
+
+import Markdown from "react-markdown";
+import gfm from "remark-gfm";
+
+const KPI = (props: { kpi: any; id: string; edit: string }) => {
+  const { kpi, edit, id, start, end, toggleEdit } = props;
+  const [showDetails, toggleShowDetails] = useState(false);
+  const {
+    title,
+    reward,
+    fiatPool,
+    distribution,
+    process,
+    active,
+    purpose,
+    scope,
+    rewardDistribution,
+  } = kpi;
+
+  if (edit === id) return <div id={id}>Form here</div>;
+
+  return (
+    <div
+      id={id}
+      className="m-2 col-12 col-md-5 col-lg-3"
+      onClick={() => toggleShowDetails(!showDetails)}
+    >
+      <Button variant="secondary" className="p-0 btn-sm float-right">
+        <Edit onClick={() => toggleEdit(id)} />
+      </Button>
+
+      <b>{title}</b>
+
+      <ul>
+        <li>
+          <b>Reward:</b> {reward}
+        </li>
+        <li>
+          <b>Fiat Pool Factor:</b> {fiatPool}
+        </li>
+        <li>
+          <b>Reward Distribution:</b> {distribution}
+        </li>
+        <li>
+          <b>Grading Process:</b> {process}
+        </li>
+        <li>
+          <b>Active:</b> {active}
+          <ul>
+            <li>
+              <b>Start Block:</b> {start}
+            </li>
+            <li>
+              <b>End Block:</b> {end}
+            </li>
+          </ul>
+        </li>
+      </ul>
+
+      {showDetails ? (
+        <>
+          {purpose ? (
+            <div>
+              <h6>Purpose</h6>
+              <Markdown
+                plugins={[gfm]}
+                className="overflow-auto text-left"
+                children={purpose}
+              />
+            </div>
+          ) : (
+            ""
+          )}
+          {scope ? (
+            <div>
+              <h6>Scope of Work</h6>
+              {scope.length ? (
+                scope.map((text) => (
+                  <Markdown
+                    plugins={[gfm]}
+                    className="overflow-auto text-left"
+                    children={text}
+                  />
+                ))
+              ) : (
+                <Markdown
+                  plugins={[gfm]}
+                  className="overflow-auto text-left"
+                  children={scope}
+                />
+              )}
+            </div>
+          ) : (
+            ""
+          )}
+          {rewardDistribution ? (
+            <Markdown
+              plugins={[gfm]}
+              className="overflow-auto text-left"
+              children={rewardDistribution}
+            />
+          ) : (
+            ""
+          )}
+        </>
+      ) : (
+        ""
+      )}
+    </div>
+  );
+};
+
+export default KPI;

+ 54 - 0
src/components/KPI/Round/Stats.tsx

@@ -0,0 +1,54 @@
+import moment from "moment";
+const Stats = (props: { round }) => {
+  const {
+    electionRound,
+    councilMembers,
+    fiatDiff,
+    reward,
+    startBlock,
+    startDate,
+    endBlock,
+    endDate,
+    deadline,
+    deadlineBlock,
+  } = props.round;
+  const length = moment(endDate, "DD.MM.YY").diff(
+    moment(startDate, "DD.MM.YY"),
+    "days"
+  );
+
+  return (
+    <div>
+      <ul>
+        <li>
+          <b>Total Possible Rewards:</b> ${reward}
+        </li>
+        <li>
+          <b>Max Fiat Pool Difference:</b> ${fiatDiff}
+        </li>
+        <li>
+          <b>Council Elected in Round:</b> {electionRound}
+        </li>
+        <li>
+          <b>Council Members:</b> {councilMembers}
+        </li>
+        <li>
+          <b>KPI Length:</b> ${endBlock - startBlock} ({length} days)
+          <ul>
+            <li>
+              <b>Start Block/Date:</b> #{startBlock} ({startDate})
+            </li>
+            <li>
+              <b>End Block/Date:</b> #{endBlock} ({endDate})
+            </li>
+          </ul>
+        </li>
+        <li>
+          <b>Deadline to Submit Summary:</b> {deadlineBlock} ({deadline})
+        </li>
+      </ul>
+    </div>
+  );
+};
+
+export default Stats;

+ 39 - 0
src/components/KPI/Round/TOC.tsx

@@ -0,0 +1,39 @@
+const TOC = (props) => {
+  const { focus } = props;
+  const { kpis, round } = props.round;
+
+  return (
+    <div className="my-2">
+      <ul>
+        <li>
+          KPI {round}
+          <ul>
+            {kpis.sections.map(({ name, kpis }, section: number) => (
+              <li>
+                <div
+                  onClick={() => focus(`#${round}-${section + 1}`)}
+                  href={`#${round}-${section + 1}`}
+                >
+                  {name}
+                </div>
+                <ul>
+                  {kpis.map(({ title }, kpi: number) => (
+                    <li
+                      onClick={() =>
+                        focus(`#${round}-${section + 1}-${kpi + 1}`)
+                      }
+                    >
+                      {title}
+                    </li>
+                  ))}
+                </ul>
+              </li>
+            ))}
+          </ul>
+        </li>
+      </ul>
+    </div>
+  );
+};
+
+export default TOC;

+ 51 - 0
src/components/KPI/Round/index.tsx

@@ -0,0 +1,51 @@
+import { Button } from "react-bootstrap";
+import { RefreshCw } from "react-feather";
+import Markdown from "react-markdown";
+import gfm from "remark-gfm";
+
+import TOC from "../TOC";
+import Stats from "./Stats";
+import KPI from "../KpiBox";
+
+const Round = (props) => {
+  const { edit, toggleEditKpi, focus, fetchKpi } = props;
+  const { round, notes, kpis, startBlock, endBlock } = props.round;
+  return (
+    <div>
+      <Button variant="secondary" className="p-1 btn-sm float-right">
+        <RefreshCw onClick={fetchKpi} />
+      </Button>
+
+      <TOC focus={focus} round={props.round} />
+      <h3>Round {round}</h3>
+      <Stats round={round} />
+
+      <Markdown
+        plugins={[gfm]}
+        className="overflow-auto text-left"
+        children={notes}
+      />
+
+      {kpis.sections.map((section, index: number) => (
+        <div key={index} id={`#${round}-${index}`}>
+          <h4>{section.name}</h4>
+          <div className="d-flex flex-wrap">
+            {section.kpis.map((kpi, i: number) => (
+              <KPI
+                key={i}
+                toggleEdit={toggleEditKpi}
+                id={`#${round}-${index + 1}-${i + 1}`}
+                kpi={kpi}
+                edit={edit}
+                start={startBlock}
+                end={endBlock}
+              />
+            ))}
+          </div>
+        </div>
+      ))}
+    </div>
+  );
+};
+
+export default Round;

+ 78 - 138
src/components/KPI/index.tsx

@@ -1,157 +1,97 @@
-import { useState, useEffect } from "react";
-import { Link } from "react-router-dom";
-import {
-  Accordion,
-  //  AccordionDetails,
-  AccordionSummary,
-  createStyles,
-  Grid,
-  makeStyles,
-  Theme,
-  Typography,
-} from "@material-ui/core";
-import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
-import { Tokenomics } from "../../types";
+import { Component } from "react";
+import { Button } from "react-bootstrap";
+import { Typography } from "@material-ui/core";
 import { Loading } from "..";
 import axios from "axios";
 
-interface LeaderboardMember {
-  id: number;
-  handle: string;
-  totalEarnedUsd: number;
-  totalEarnedTjoy: number;
-  timesElected: number;
-  usdPerElection: number;
-  tjoyPerElection: number;
-}
-interface CouncilMember {
-  id: number;
-  handle: string;
-  rewardUsd: 51;
-  rewardTjoy: number;
-}
-interface Kpi {
-  kpi: number;
-  totalPossibleRewardsUsd: 3525;
-  councilMembers: CouncilMember[];
-}
+import Leaderboard from "./Leaderboard";
+import Round from "./Round";
 
-const useStyles = makeStyles((theme: Theme) =>
-  createStyles({
-    root: {
-      textAlign: "center",
-      backgroundColor: "#000",
-      color: "#fff",
-    },
-    acc: {
-      color: "#fff",
-      backgroundColor: "#4138ff",
-    },
-    heading: {
-      fontSize: theme.typography.pxToRem(25),
-      fontWeight: theme.typography.fontWeightRegular,
-    },
-  })
-);
+const baseUrl = `https://joystreamstats.live/static`;
 
-const KPI = (props: { tokenomics: Tokenomics }) => {
-  const { tokenomics } = props;
-  const price = tokenomics ? tokenomics.price : 30 / 1000000;
-  const classes = useStyles();
-  const [kpi, setKpi] = useState([]);
-  const [leaderboard, setLeaderboard] = useState([]);
-  useEffect(() => {
-    const baseUrl = `https://joystreamstats.live/static/kpi`;
+class KPI extends Component {
+  constructor(props: { tokenomics: Tokenomics }) {
+    super(props);
+    this.state = { rounds: [], leaderboard: [] };
+    this.fetchKpi = this.fetchKpi.bind(this);
+    this.fetchLeaderboard = this.fetchLeaderboard.bind(this);
+    this.toggleEditKpi = this.toggleEditKpi.bind(this);
+    this.toggleShowLeaderboard = this.toggleShowLeaderboard.bind(this);
+  }
+  componentDidMount() {
+    this.fetchKpi();
+    //this.fetchLeaderboard()
+  }
+  fetchKpi() {
     axios
-      .get(`${baseUrl}/data.json`)
-      .then((res) => setKpi(res.data))
+      .get(`${baseUrl}/kpi.json`)
+      .then(({ data }) => data.rounds && this.setState({ rounds: data.rounds }))
       .catch((e) => console.error(`Failed to fetch KPI data.`, e));
+  }
+  fetchLeaderboard() {
     axios
       .get(`${baseUrl}/leaderboard.json`)
-      .then((res) => setLeaderboard(res.data))
+      .then((res) => this.setState({ leaderboard: res.data }))
       .catch((e) => console.error(`Failed to fetch Leadboard data.`, e));
-  });
-  if (!kpi.length) return <Loading target="KPI" />;
-
-  return (
-    <Grid className={classes.root} item lg={12}>
-      <Typography variant="h2" className="mb-3">
-        KPI Grading
-      </Typography>
+  }
 
-      {leaderboard ? (
-        <div>
-          <div className="d-flex flex-row text-right font-weight-bold">
-            <div className="col-2">Member</div>
-            <div className="col-1">Times Elected</div>
-            <div className="col-2">Total Earned USD</div>
-            <div className="col-2">Total Earned M tJOY</div>
-            <div className="col-2">USD per Election</div>
-            <div className="col-2">M tJOY per Election</div>
-          </div>
+  toggleShowLeaderboard() {
+    this.setState({ showLeaderboard: !this.state.showLeaderboard });
+  }
+  toggleEditKpi(edit) {
+    this.setState({ edit });
+  }
+  focus(id) {
+    document.getElementById(id)?.scrollIntoView();
+  }
 
-          {leaderboard
-            .sort((a, b) => b.totalEarnedUsd - a.totalEarnedUsd)
-            .slice(0, 20)
-            .map((m: LeaderboardMember) => (
-              <div className="d-flex flex-row text-right mt-1">
-                <div className="col-2">
-                  <Link className="text-light" to={`/members/${m.handle}`}>
-                    {m.handle}
-                  </Link>
-                </div>
-                <div className="col-1">{m.timesElected}</div>
-                <div className="col-2">$ {m.totalEarnedUsd}</div>
-                <div className="col-2">
-                  {(m.totalEarnedUsd / price / 1000000).toFixed(1)}
-                </div>
-                <div className="col-2">{m.usdPerElection}</div>
-                <div className="col-2">
-                  {(m.usdPerElection / price / 1000000).toFixed(1)}
-                </div>
-              </div>
-            ))}
-        </div>
-      ) : (
-        <div />
-      )}
+  render() {
+    const { edit, rounds, leaderboard, showLeaderboard } = this.state;
+    if (!rounds.length) return <Loading target="KPI" />;
+    return (
+      <div className="m-3 p-2 text-light">
+        <Typography variant="h2" className="mb-3">
+          KPI
+        </Typography>
 
-      {kpi.map((round: Kpi) => (
-        <Accordion className={classes.acc} key={round.kpi}>
-          <AccordionSummary
-            expandIcon={<ExpandMoreIcon style={{ color: "#fff" }} />}
-            aria-controls={`${round.kpi}-content`}
-            id={`${round.kpi}-header`}
-          >
-            <h3>Round {round.kpi}</h3>
-            <div className="ml-2">(max: $ {round.totalPossibleRewardsUsd})</div>
-          </AccordionSummary>
+        <Button
+          variant="outline-secondary"
+          className="p-1 btn-sm"
+          onClick={this.toggleShowLeaderboard}
+        >
+          Leaderboard
+        </Button>
 
-          <div className="d-flex flex-column">
-            <div className="d-flex flex-row text-right font-weight-bold">
-              <div className="col-2">Member</div>
-              <div className="col-2">Reward USD</div>
-              <div className="col-2">Reward M tJOY</div>
+        {showLeaderboard ? (
+          <Leaderboard
+            leaderboard={leaderboard}
+            tokenomics={this.props.tokenomics}
+          />
+        ) : (
+          <div className="d-flex flex-row py-2">
+            <div className="justify-content-start mr-3">
+              {rounds.map(({ round }) => (
+                <Button
+                  variant="outline-secondary"
+                  className="mb-1 p-1 flex-grow-0"
+                >
+                  {round}
+                </Button>
+              ))}
             </div>
 
-            {round.councilMembers
-              .sort((a, b) => b.rewardUsd - a.rewardUsd)
-              .map((m) => (
-                <div className="d-flex flex-row text-right">
-                  <div className="col-2">
-                    <Link to={`/members/${m.handle}`}>{m.handle}</Link>
-                  </div>
-                  <div className="col-2">$ {m.rewardUsd}</div>
-                  <div className="col-2">
-                    {(m.rewardUsd / price / 1000000).toFixed(1)}
-                  </div>
-                </div>
-              ))}
+            <Round
+              round={rounds[0]}
+              edit={edit}
+              fetchKpi={this.fetchKpi}
+              toggleEdit={this.toggleEditKpi}
+              focus={this.focus}
+            />
           </div>
-        </Accordion>
-      ))}
-    </Grid>
-  );
-};
+        )}
+      </div>
+    );
+  }
+}
 
 export default KPI;