Переглянути джерело

refactor Votes Proposals Dashboard Validators

Joystream Stats 3 роки тому
батько
коміт
fe0fca7d21
35 змінених файлів з 986 додано та 563 видалено
  1. 34 5
      src/App.tsx
  2. 5 7
      src/components/Back.tsx
  3. 13 1
      src/components/Calendar/index.tsx
  4. 0 3
      src/components/Council/index.tsx
  5. 26 17
      src/components/Councils/CouncilVotes.tsx
  6. 0 27
      src/components/Councils/VoteDiv.tsx
  7. 2 1
      src/components/Councils/index.tsx
  8. 31 0
      src/components/Dashboard/Footer.tsx
  9. 96 44
      src/components/Dashboard/index.tsx
  10. 9 6
      src/components/Forum/LatestPost.tsx
  11. 2 1
      src/components/Members/Member.tsx
  12. 0 39
      src/components/Members/Votes.tsx
  13. 2 1
      src/components/Members/index.tsx
  14. 55 37
      src/components/Mint/index.tsx
  15. 69 0
      src/components/Proposals/Detail.tsx
  16. 2 2
      src/components/Proposals/NavBar.tsx
  17. 34 0
      src/components/Proposals/NavButtons.tsx
  18. 7 10
      src/components/Proposals/Proposal.tsx
  19. 37 46
      src/components/Proposals/ProposalTable.tsx
  20. 45 40
      src/components/Proposals/Row.tsx
  21. 33 0
      src/components/Proposals/TableHead.tsx
  22. 0 15
      src/components/Proposals/VoteButton.tsx
  23. 0 25
      src/components/Proposals/VotesTooltip.tsx
  24. 34 6
      src/components/Routes/index.tsx
  25. 35 0
      src/components/TableFromObject.tsx
  26. 6 0
      src/components/Timeline/index.tsx
  27. 2 2
      src/components/Tokenomics/Navigation.tsx
  28. 2 1
      src/components/Tokenomics/index.tsx
  29. 58 52
      src/components/Validators/MinMax.tsx
  30. 14 16
      src/components/Validators/Validator.tsx
  31. 75 45
      src/components/Validators/index.tsx
  32. 110 0
      src/components/Votes.tsx
  33. 10 0
      src/components/index.ts
  34. 133 114
      src/index.css
  35. 5 0
      src/types.ts

+ 34 - 5
src/App.tsx

@@ -35,6 +35,9 @@ const initialState = {
   blocks: [],
   now: 0,
   block: 0,
+  era: 0,
+  issued: 0,
+  price: 0,
   nominators: [],
   validators: [],
   channels: [],
@@ -55,6 +58,7 @@ const initialState = {
   stashes: [],
   stars: {},
   lastReward: 0,
+  hideFooter: false,
 };
 
 class App extends React.Component<IProps, IState> {
@@ -337,15 +341,21 @@ class App extends React.Component<IProps, IState> {
     for (let i = proposalCount; i > 0; i--) this.fetchProposal(api, i);
   }
   async fetchProposal(api: Api, id: number) {
-    let { proposals } = this.state;
-    const exists = proposals.find((p) => p && p.id === id);
+    const { proposals } = this.state;
+    const exists = this.state.proposals.find((p) => p && p.id === id);
 
-    if (exists && exists.stage === "Finalized")
+    if (exists && exists.detail && exists.stage === "Finalized")
       if (exists.votesByAccount && exists.votesByAccount.length) return;
       else return this.fetchVotesPerProposal(api, exists);
 
     console.debug(`Fetching proposal ${id}`);
     const proposal = await get.proposalDetail(api, id);
+    if (proposal.type !== "Text") {
+      const details = await api.query.proposalsCodex.proposalDetailsByProposalId(
+        id
+      );
+      proposal.detail = details.toJSON();
+    }
     proposals[id] = proposal;
     this.save("proposals", proposals);
     this.fetchVotesPerProposal(api, proposal);
@@ -501,6 +511,13 @@ class App extends React.Component<IProps, IState> {
     this.save(`handles`, handles);
   }
 
+  // Validators
+  toggleStar(account: string) {
+    let { stars } = this.state;
+    stars[account] = !stars[account];
+    this.save("stars", stars);
+  }
+
   // Reports
   async fetchReports() {
     const domain = `https://raw.githubusercontent.com/Joystream/community-repo/master/council-reports`;
@@ -678,9 +695,20 @@ class App extends React.Component<IProps, IState> {
     }
   }
 
+  toggleFooter() {
+    console.log(this.state.hideFooter);
+    this.setState({ hideFooter: !this.state.hideFooter });
+  }
+
   render() {
     if (this.state.loading) return <Loading />;
-    return <Routes load={this.load} save={this.save} {...this.state} />;
+    return (
+      <Routes
+        toggleFooter={this.toggleFooter}
+        toggleStar={this.toggleStar}
+        {...this.state}
+      />
+    );
   }
 
   componentDidMount() {
@@ -698,7 +726,8 @@ class App extends React.Component<IProps, IState> {
     this.fetchTokenomics = this.fetchTokenomics.bind(this);
     this.fetchProposal = this.fetchProposal.bind(this);
     this.load = this.load.bind(this);
-    this.save = this.save.bind(this);
+    this.toggleStar = this.toggleStar.bind(this);
+    this.toggleFooter = this.toggleFooter.bind(this);
   }
 }
 

+ 5 - 7
src/components/Back.tsx

@@ -1,14 +1,12 @@
 import React from "react";
 import { Button } from "react-bootstrap";
-import { Link } from "react-router-dom";
 
-const Back = (props: { target?: string }) => {
+const Back = (props: { target?: string; history: any }) => {
+  const goBack = () => props.history.goBack();
   return (
-    <Link to={props.target || `/`}>
-      <Button variant="secondary" className="p-1 m-1">
-        Back
-      </Button>
-    </Link>
+    <Button variant="secondary" className="p-1 m-1" onClick={goBack}>
+      Back
+    </Button>
   );
 };
 

+ 13 - 1
src/components/Calendar/index.tsx

@@ -1,4 +1,5 @@
 import React, { Component } from "react";
+import { Link } from "react-router-dom";
 import { Button } from "react-bootstrap";
 import Timeline from "react-calendar-timeline";
 import "react-calendar-timeline/lib/Timeline.css";
@@ -13,6 +14,7 @@ interface IProps {
   proposals: ProposalDetail[];
   now: number;
   block: number;
+  history: any;
 }
 interface IState {
   items: CalendarItem[];
@@ -25,6 +27,7 @@ class Calendar extends Component<IProps, IState> {
     super(props);
     this.state = { items: [], groups: [], hide: [] };
     this.toggleShowProposalType = this.toggleShowProposalType.bind(this);
+    this.openProposal = this.openProposal.bind(this);
   }
 
   componentDidMount() {
@@ -102,6 +105,10 @@ class Calendar extends Component<IProps, IState> {
     this.setState({ hide });
     this.filterItems();
   }
+  openProposal(id: number) {
+    console.log(`want to see`, id);
+    this.props.history.push(`/proposals/${id}`);
+  }
 
   render() {
     const { hide, items, groups } = this.state;
@@ -128,6 +135,10 @@ class Calendar extends Component<IProps, IState> {
 
     return (
       <div>
+        <Link className="back left" to={"/"}>
+          <Button variant="secondary">Back</Button>
+        </Link>
+
         <Timeline
           groups={groups.filter((g) => !hide[g.id])}
           items={items}
@@ -136,9 +147,10 @@ class Calendar extends Component<IProps, IState> {
           stackItems={true}
           defaultTimeStart={moment(first.start_time).add(-1, "day")}
           defaultTimeEnd={moment().add(15, "day")}
+          onItemSelect={this.openProposal}
         />
         <div className="position-fixed" style={{ left: "0", bottom: "0" }}>
-          <Back />
+          <Back history={this.props.history} />
         </div>
       </div>
     );

+ 0 - 3
src/components/Council/index.tsx

@@ -79,9 +79,6 @@ const Council = (props: {
           </div>
         </div>
       )) || <Loading />}
-      <hr />
-
-      <Link to={`/tokenomics`}>Reports</Link>
     </div>
   );
 };

+ 26 - 17
src/components/Councils/CouncilVotes.tsx

@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import { Table } from "react-bootstrap";
 import ProposalOverlay from "../Proposals/ProposalOverlay";
-import VoteDiv from "./VoteDiv";
+import { VoteButton } from "..";
 import { Member, ProposalDetail, Seat } from "../../types";
 
 interface IProps {
@@ -56,14 +56,7 @@ class CouncilVotes extends Component<IProps, IState> {
       <Table className="text-light text-center">
         <thead onClick={this.toggleExpand}>
           <tr>
-            <th className="text-right" style={{ width: "40%" }}>
-              Round {round}
-            </th>
-            {councilMembers.map((member) => (
-              <th key={member.handle} style={{ width: "10%" }}>
-                {member.handle}
-              </th>
-            ))}
+            <th colSpan={councilMembers.length + 1}>Round {round}</th>
           </tr>
         </thead>
         <tbody>
@@ -78,14 +71,30 @@ class CouncilVotes extends Component<IProps, IState> {
                     <ProposalOverlay block={block} {...p} />
                   </td>
 
-                  {p.votesByAccount &&
-                    council.map((seat) => (
-                      <VoteDiv
-                        key={seat.member}
-                        votes={p.votesByAccount}
-                        member={members.find((m) => m.account === seat.member)}
-                      />
-                    ))}
+                  {p.votesByAccount ? (
+                    council.map((seat) => {
+                      if (!p.votesByAccount || !members) return <td />;
+                      const member = members.find(
+                        (m) => m.account === seat.member
+                      );
+                      if (!member) return <td />;
+                      const vote = p.votesByAccount.find(
+                        (v) => v.handle === member.handle
+                      );
+                      if (!vote) return <td />;
+                      return (
+                        <td>
+                          <VoteButton
+                            key={member.handle}
+                            handle={member.handle}
+                            vote={vote.vote}
+                          />
+                        </td>
+                      );
+                    })
+                  ) : (
+                    <td>Loading ..</td>
+                  )}
                 </tr>
               ))}
         </tbody>

+ 0 - 27
src/components/Councils/VoteDiv.tsx

@@ -1,27 +0,0 @@
-import React from "react";
-import { Button } from "react-bootstrap";
-import { Member, Vote } from "../../types";
-
-const VoteDiv = (props: { votes?: Vote[]; member?: Member }) => {
-  const { votes, member } = props;
-  if (!votes || !member) return <td />;
-
-  const v = votes.find((v) => v.handle === member.handle);
-  if (!v || v.vote === "") return <td />;
-
-  const styles: { [key: string]: string } = {
-    Approve: "success",
-    Reject: "danger",
-    Abstain: "dark",
-  };
-
-  return (
-    <td className="text-center p-0">
-      <Button className="p-0" variant={styles[v.vote]}>
-        {v.vote}
-      </Button>
-    </td>
-  );
-};
-
-export default VoteDiv;

+ 2 - 1
src/components/Councils/index.tsx

@@ -17,12 +17,13 @@ const Rounds = (props: {
   members: Member[];
   councils: Seat[][];
   proposals: any;
+  history: any;
 }) => {
   const { block, councils, members, proposals } = props;
   return (
     <div className="w-100">
       <div className="position-fixed" style={{ right: "0", top: "0" }}>
-        <Back />
+        <Back history={props.history} />
       </div>
       <Table className="w-100 text-light">
         <thead>

+ 31 - 0
src/components/Dashboard/Footer.tsx

@@ -0,0 +1,31 @@
+import React from "react";
+import { X, Info } from "react-feather";
+
+const Footer = (props: {
+  connecting: boolean;
+  show: boolean;
+  toggleHide: () => void;
+  link: string;
+}) => {
+  const { show, link } = props;
+  if (!show)
+    return (
+      <Info className="footer-hidden" onClick={() => props.toggleHide()} />
+    );
+  return (
+    <div className="w-100 footer">
+      <X className="footer-hidden" onClick={() => props.toggleHide()} />
+      If you find this place useful,{" "}
+      <a className="mx-1" href={link}>
+        <u>send some tokens</u>
+      </a>
+      and a
+      <a className="mx-1" href="/forum/threads/257">
+        <u>message with ideas</u>
+      </a>{" "}
+      to make it even better.
+    </div>
+  );
+};
+
+export default Footer;

+ 96 - 44
src/components/Dashboard/index.tsx

@@ -1,11 +1,18 @@
 import React from "react";
 import { Link } from "react-router-dom";
-import { ActiveProposals, Council } from "..";
+import { Council } from "..";
+import Proposals from "../Proposals/ProposalTable";
+import Post from "../Forum/LatestPost";
 import Footer from "./Footer";
 import Validators from "../Validators";
 import { IState } from "../../types";
 
-const Dashboard = (props: IState) => {
+interface IProps extends IState {
+  toggleStar: (a: string) => void;
+  toggleFooter: () => void;
+}
+
+const Dashboard = (props: IProps) => {
   const {
     connecting,
     block,
@@ -13,35 +20,28 @@ const Dashboard = (props: IState) => {
     domain,
     handles,
     members,
+    posts,
     proposals,
+    threads,
     tokenomics,
   } = props;
   const userLink = `${domain}/#/members/joystreamstats`;
   return (
     <div className="w-100 flex-grow-1 d-flex align-items-center justify-content-center d-flex flex-column">
-      <div className="box position-abasolute" style={{ top: "0", right: "0" }}>
-        <Link to="/mint">Tools</Link>
-      </div>
-
-      <div className="title">
-        <h1>
-          <a href={domain}>Joystream</a>
-        </h1>
-      </div>
-
-      <div className="box">
-        <h3>Forum</h3>
-        <Link to="/forum">
-          {props.posts.length} posts in {props.threads.length} threads
-        </Link>
+      <div
+        className="box position-fixed bg-warning d-flex flex-column"
+        style={{ top: "0px", right: "0px" }}
+      >
+        <Link to={`/calendar`}>Calendar</Link>
+        <Link to={`/timeline`}>Timeline</Link>
+        <Link to={`/tokenomics`}>Reports</Link>
+        <Link to={`/validators`}>Validators</Link>
+        <Link to="/mint">Toolbox</Link>
       </div>
 
-      <div className="box">
-        <h3>Active Proposals</h3>
-        <ActiveProposals block={block} proposals={proposals} />
-        <hr />
-        <Link to={`/proposals`}>Show all</Link>
-      </div>
+      <h1 className="title">
+        <a href={domain}>Joystream</a>
+      </h1>
 
       <Council
         councils={props.councils}
@@ -58,28 +58,80 @@ const Dashboard = (props: IState) => {
         validators={props.validators}
       />
 
-      <Validators
-        block={block}
-        era={props.era}
-        now={now}
-        lastReward={props.lastReward}
-        councils={props.councils}
-        handles={handles}
-        members={members}
-        posts={props.posts}
-        proposals={props.proposals}
-        nominators={props.nominators}
-        validators={props.validators}
-        stashes={props.stashes}
-        stars={props.stars}
-        stakes={props.stakes}
-        save={props.save}
-        rewardPoints={props.rewardPoints}
-        issued={tokenomics ? Number(tokenomics.totalIssuance) : 0}
-        price={tokenomics ? Number(tokenomics.price) : 0}
-      />
+      <div className="w-100 p-3 m-3">
+        <div className="d-flex flex-row">
+          <h3 className="ml-1 text-light">Active Proposals</h3>
+          <Link className="m-3 text-light" to={"/proposals"}>
+            All
+          </Link>
+          <Link className="m-3 text-light" to={"/councils"}>
+            Votes
+          </Link>
+        </div>
+        <Proposals
+          hideNav={true}
+          startTime={now - block * 6000}
+          block={block}
+          proposals={proposals.filter((p) => p && p.result === "Pending")}
+          proposalPosts={props.proposalPosts}
+          members={members}
+          councils={props.councils}
+          posts={posts}
+          validators={props.validators}
+        />
+      </div>
 
-      <Footer show={!connecting} link={userLink} />
+      <div className="w-100 p-3 m-3 d-flex flex-column">
+        <h3>
+          <Link className="text-light" to={"/forum"}>
+            Forum
+          </Link>
+        </h3>
+        {posts
+          .sort((a, b) => b.id - a.id)
+          .slice(0, 10)
+          .map((post) => (
+            <Post
+              key={post.id}
+              selectThread={() => {}}
+              handles={handles}
+              post={post}
+              thread={threads.find((t) => t.id === post.threadId)}
+              startTime={now - block * 6000}
+            />
+          ))}
+      </div>
+
+      <div className="w-100 p-3 m-3">
+        <Validators
+          block={block}
+          era={props.era}
+          now={now}
+          lastReward={props.lastReward}
+          councils={props.councils}
+          handles={handles}
+          members={members}
+          posts={props.posts}
+          proposals={props.proposals}
+          nominators={props.nominators}
+          validators={props.validators}
+          stashes={props.stashes}
+          stars={props.stars}
+          stakes={props.stakes}
+          toggleStar={props.toggleStar}
+          rewardPoints={props.rewardPoints}
+          tokenomics={tokenomics}
+          hideBackButton={true}
+        />
+      </div>
+
+      <Footer
+        toggleHide={props.toggleFooter}
+        show={!props.hideFooter}
+        connecting={connecting}
+        link={userLink}
+      />
+      {connecting ? <div className="connecting">Connecting ..</div> : ""}
     </div>
   );
 };

+ 9 - 6
src/components/Forum/LatestPost.tsx

@@ -15,14 +15,16 @@ const LatestPost = (props: {
   thread?: Thread;
   startTime: number;
 }) => {
-  const { handles, thread, post, startTime } = props;
+  const { selectThread, handles, thread, post, startTime } = props;
   const { authorId, createdAt, id, threadId, text } = post;
 
   return (
     <div
       key={id}
       className="box d-flex flex-row"
-      onClick={() => thread && props.selectThread(thread.id)}
+      onClick={
+        thread && selectThread ? () => props.selectThread(thread.id) : () => {}
+      }
     >
       <div className="col-2 mr-3">
         <User key={authorId} id={authorId} handle={handles[authorId]} />
@@ -31,11 +33,12 @@ const LatestPost = (props: {
       </div>
 
       <div>
-        <Link to={`/forum/threads/${threadId}`}>
-          <div className="text-left mb-3 font-weight-bold">
+        <div className="text-left mb-3 font-weight-bold">
+          <Link to={`/forum/threads/${threadId}`}>
             {thread ? thread.title : threadId}
-          </div>
-        </Link>
+          </Link>
+        </div>
+
         <Markdown
           plugins={[gfm]}
           className="overflow-auto text-left"

+ 2 - 1
src/components/Members/Member.tsx

@@ -21,6 +21,7 @@ const MemberBox = (props: {
   block: number;
   now: number;
   validators: string[];
+  history:any
 }) => {
   const { block, now, councils, members, posts, proposals } = props;
   const h = props.match.params.handle;
@@ -43,7 +44,7 @@ const MemberBox = (props: {
 
   return (
     <div>
-      <Back />
+        <Back history={props.history} />
       <div className="box">
         {isCouncilMember && <div>council member</div>}
         <a href={`${domain}/#/members/${member.handle}`}>

+ 0 - 39
src/components/Members/Votes.tsx

@@ -1,39 +0,0 @@
-import React from "react";
-import { ProposalDetail } from "../../types";
-
-interface Vote {
-  proposal: ProposalDetail;
-  vote: string;
-}
-
-const Votes = (props: { votes: Vote[] }) => {
-  const { votes } = props;
-  if (!votes.length) return <div />;
-
-  return (
-    <div>
-      <h2>Votes</h2>
-      <div className="">
-        {votes.map((v) => (
-          <VoteDiv key={v.proposal.id} vote={v} />
-        ))}
-      </div>
-    </div>
-  );
-};
-
-const VoteDiv = (props: { vote: Vote }) => {
-  const { proposal, vote } = props.vote;
-  if (vote === "") return <div />;
-
-  return (
-    <div
-      key={proposal.id}
-      className={vote === `Approve` ? `bg-success` : `bg-danger`}
-    >
-      {proposal.title}
-    </div>
-  );
-};
-
-export default Votes

+ 2 - 1
src/components/Members/index.tsx

@@ -13,6 +13,7 @@ interface IProps {
   now: number;
   block: number;
   validators: string[];
+  history:any
 }
 
 interface IState {
@@ -49,7 +50,7 @@ class Members extends React.Component<IProps, IState> {
 
     return (
       <div>
-        <Back />
+        <Back history={this.props.history} />
         <h1 className="text-center text-white">Joystream Members</h1>
         <div className="d-flex flew-row justify-content-between">
           {cols.map((col, index: number) => (

+ 55 - 37
src/components/Mint/index.tsx

@@ -7,56 +7,76 @@ interface IProps {
   tokenomics?: any;
   validators: string[];
   lastReward: number;
+  history: any;
 }
 interface IState {
-  [key: string]: number;
+  start: number;
+  end: number;
+  role: string;
+  salary: number;
+  payout: number; // for validators
+  [key: string]: number | string;
 }
 
 const round = 8;
 const start = 57601 + round * 201600;
 const end = 57601 + (round + 1) * 201600;
 
+const payoutInterval = 3600;
+const salaries: { [key: string]: number } = {
+  storageLead: 21000,
+  storageProvider: 10500,
+  curatorLead: 20678,
+  curator: 13500,
+  consul: 8571,
+};
+
 class Mint extends React.Component<IProps, IState> {
   constructor(props: IProps) {
     super(props);
 
-    this.state = { start, end, reward: 10, role: 0, payout: 0 };
+    this.state = {
+      start,
+      end,
+      role: "",
+      salary: 0,
+      payout: 0,
+    };
+    this.setRole = this.setRole.bind(this);
     this.handleChange = this.handleChange.bind(this);
   }
 
   componentDidMount() {
     this.setState({ payout: this.props.lastReward });
+    this.setRole({ target: { value: "consul" } });
   }
 
+  setRole(e: any) {
+    const role = e.target.value;
+    this.setState({ role, salary: salaries[role] });
+  }
   handleChange(e: {
     preventDefault: () => void;
-    target: { name: string; value: string };
+    target: { name: string; value: any };
   }) {
     e.preventDefault();
-    const value = parseInt(e.target.value);
-    this.setState({ [e.target.name]: value > 0 ? value : 0 });
+    this.setState({ [e.target.name]: parseInt(e.target.value) });
   }
 
   render() {
     const { tokenomics } = this.props;
     if (!tokenomics) return <Loading />;
-    const { payout } = this.state;
+    const { role, start, salary, end, payout } = this.state;
 
     const { price } = tokenomics;
     const rate = Math.floor(+price * 100000000) / 100;
-
-    const rewards: { [key: string]: number } = { storageLead: 23146 };
-    const blocks = this.state.end - this.state.start;
-    const baseReward = rewards[Object.keys(rewards)[this.state.role]];
-    const rewardTime = 3600;
-    const reward = (blocks / rewardTime) * baseReward;
+    const blocks = end - start;
 
     return (
       <div className="p-3 text-light">
         <h2>Mint</h2>
-
         <div className="d-flex flex-row form-group">
-          <label className="col-2">Rate</label>
+          <label className="col-2">Token value</label>
           <input
             className="form-control col-4"
             disabled={true}
@@ -65,7 +85,6 @@ class Mint extends React.Component<IProps, IState> {
             value={`${rate} $ / 1 M JOY`}
           />
         </div>
-
         <div className="d-flex flex-row form-group">
           <label className="col-2">Start block</label>
           <input
@@ -73,10 +92,9 @@ class Mint extends React.Component<IProps, IState> {
             onChange={this.handleChange}
             name="start"
             type="number"
-            value={this.state.start}
+            value={start}
           />
         </div>
-
         <div className="d-flex flex-row form-group">
           <label className="col-2">End block</label>
           <input
@@ -84,7 +102,7 @@ class Mint extends React.Component<IProps, IState> {
             onChange={this.handleChange}
             name="end"
             type="number"
-            value={this.state.end}
+            value={end}
           />
         </div>
 
@@ -98,61 +116,60 @@ class Mint extends React.Component<IProps, IState> {
             value={blocks}
           />
         </div>
-
         <div className="d-flex flex-row form-group">
           <label className="col-2">Role</label>
           <select
             name="role"
             className="form-control col-4"
-            onChange={this.handleChange}
+            onChange={this.setRole}
           >
-            {Object.keys(rewards).map((role, index: number) => (
-              <option key={index}>{role}</option>
+            {Object.keys(salaries).map((r: string) => (
+              <option selected={role === r}>{r}</option>
             ))}
           </select>
         </div>
-
         <div className="d-flex flex-row form-group">
-          <label className="col-2">Reward (JOY / {rewardTime} blocks)</label>
+          <label className="col-2">
+            Reward (JOY / {payoutInterval} blocks)
+          </label>
           <input
             className="form-control col-4"
-            disabled={true}
             name="baseReward"
             type="number"
-            value={baseReward}
+            onChange={this.handleChange}
+            value={salary}
           />
         </div>
-
         <div className="d-flex flex-row form-group">
-          <label className="col-2">Reward (USD / {rewardTime} blocks)</label>
+          <label className="col-2">
+            Reward (USD / {payoutInterval} blocks)
+          </label>
           <input
             className="form-control col-4"
             disabled={true}
             name="baseRewardUSD"
             type="number"
-            value={price * baseReward}
+            value={price * salary}
           />
         </div>
-
         <div className="d-flex flex-row form-group">
-          <label className="col-2">Reward (JOY)</label>
+          <label className="col-2">Reward (JOY) / {blocks} blocks</label>
           <input
             className="form-control col-4"
             disabled={true}
             name="reward"
             type="number"
-            value={reward}
+            value={(blocks / payoutInterval) * salary}
           />
         </div>
-
         <div className="d-flex flex-row form-group">
-          <label className="col-2">Reward (USD)</label>
+          <label className="col-2">Reward (USD) / {blocks} blocks</label>
           <input
             className="form-control col-4"
             disabled={true}
             name="joy"
             type="number"
-            value={price * reward}
+            value={(blocks / payoutInterval) * salary * price}
           />
         </div>
 
@@ -162,8 +179,9 @@ class Mint extends React.Component<IProps, IState> {
           payout={payout}
           price={this.props.tokenomics ? this.props.tokenomics.price : 0}
         />
-
-        <Back />
+        <div className="position-fixed" style={{ right: "0px", top: "0px" }}>
+          <Back history={this.props.history} />
+        </div>
       </div>
     );
   }

+ 69 - 0
src/components/Proposals/Detail.tsx

@@ -0,0 +1,69 @@
+import React from "react";
+import { OverlayTrigger, Tooltip } from "react-bootstrap";
+import { TableFromObject } from "..";
+
+const amount = (amount: number) => (amount / 1000000).toFixed(2);
+const Detail = (props: { detail?: any; type: string }) => {
+  const { detail, type } = props;
+  if (!detail) return <p>{type}</p>;
+
+  if (type === "Text") return <p>Text</p>;
+
+  if (type === "Spending")
+    return (
+      <p>
+        <b>Spending</b>
+        <p>{amount(detail.Spending[0])} M tJOY</p>
+      </p>
+    );
+
+  if (type === "SetWorkingGroupMintCapacity")
+    return (
+      <p>
+        Fill {detail.SetWorkingGroupMintCapacity[1]} working group mint
+        <br />({amount(detail.SetWorkingGroupMintCapacity[0])} M tJOY)
+      </p>
+    );
+
+  if (type === "SetWorkingGroupLeaderReward")
+    return (
+      <p>
+        Set {detail.SetWorkingGroupLeaderReward[2]} working group reward (
+        {amount(detail.SetWorkingGroupLeaderReward[1])} M tJOY,
+        {detail.SetWorkingGroupLeaderReward[0]})
+      </p>
+    );
+
+  if (type === "SetContentWorkingGroupMintCapacity")
+    return (
+      <p>
+        SetContentWorkingGroupMintCapacity (
+        {amount(detail.SetContentWorkingGroupMintCapacity)} M tJOY)
+      </p>
+    );
+
+  if (type === "BeginReviewWorkingGroupLeaderApplication")
+    return (
+      <p>
+        Hire {detail[type][1]} working group leader ({detail[type][0]})
+      </p>
+    );
+
+  if (type === "SetValidatorCount")
+    return <p>SetValidatorCount ({detail.SetValidatorCount})</p>;
+
+  return (
+    <OverlayTrigger
+      placement={"right"}
+      overlay={
+        <Tooltip id={`${type}`} className="tooltip">
+          <TableFromObject data={detail[type]} />
+        </Tooltip>
+      }
+    >
+      <div>{type}</div>
+    </OverlayTrigger>
+  );
+};
+
+export default Detail;

+ 2 - 2
src/components/Proposals/NavBar.tsx

@@ -4,8 +4,8 @@ import { Link } from "react-router-dom";
 import { Sliders } from "react-feather";
 
 const NavBar = (props: any) => {
-  const { authors } = props;
-
+  const { authors, show } = props;
+if (!show) return <div/>
   return (
     <Navbar bg="dark" variant="dark">
       <Link to="/">

+ 34 - 0
src/components/Proposals/NavButtons.tsx

@@ -0,0 +1,34 @@
+import React from "react";
+import { Button } from "react-bootstrap";
+import { ChevronLeft, ChevronRight } from "react-feather";
+
+const NavButtons = (props: {
+  setPage: (page: number) => void;
+  page: number;
+  limit: number;
+  proposals: number;
+}) => {
+  const { setPage, limit, page, proposals } = props;
+  if (proposals < limit) return <div/>
+  return (
+    <div className="text-center">
+      <Button
+        variant="secondary"
+        className="btn btn-sm"
+        disabled={page === 1}
+        onClick={() => setPage(page - 1)}
+      >
+        <ChevronLeft />
+      </Button>
+      <Button
+        variant="secondary"
+        className="btn btn-sm ml-1"
+        disabled={page * limit > proposals}
+        onClick={() => setPage(page + 1)}
+      >
+        <ChevronRight />
+      </Button>
+    </div>
+  );
+};
+export default NavButtons;

+ 7 - 10
src/components/Proposals/Proposal.tsx

@@ -1,25 +1,24 @@
 import React from "react";
 import htmr from "htmr";
 import { ProposalDetail } from "../../types";
-import Votes from "./VotesTooltip";
-import Back from "../Back";
+import { Back, VotesTooltip } from "..";
 import Markdown from "react-markdown";
 import gfm from "remark-gfm";
+import { domain } from "../../config";
 
 const Proposal = (props: {
   match: { params: { id: string } };
   proposals: ProposalDetail[];
+  history: any;
 }) => {
   const { match, proposals } = props;
   const id = parseInt(match.params.id);
-
   const proposal = proposals.find((p) => p && p.id === id);
   if (!proposal) return <div>Proposal not found</div>;
-  const { description, title, message, votesByAccount } = proposal;
-
+  const { description, title, message, votes, votesByAccount } = proposal;
   return (
     <div>
-      <Back target="/proposals" />
+      <Back history={props.history} />
       <div className="d-flex flex-row">
         <div className="box col-6 ml-3">
           <h3>{title}</h3>
@@ -34,15 +33,13 @@ const Proposal = (props: {
           <div className="box text-left">
             <div>{htmr(message.replaceAll(`\n`, "<br/>"))}</div>
             <div>
-              <a href={`https://pioneer.joystreamstats.live/#/proposals/${id}`}>
-                more
-              </a>
+              <a href={`${domain}/#/proposals/${id}`}>more</a>
             </div>
           </div>
 
           <div className="box">
             <h3>Votes</h3>
-            <Votes votes={votesByAccount} />
+            <VotesTooltip votes={votes} voteByAccounts={votesByAccount} />
           </div>
         </div>
       </div>

+ 37 - 46
src/components/Proposals/ProposalTable.tsx

@@ -1,11 +1,15 @@
 import React from "react";
-import { OverlayTrigger, Tooltip } from "react-bootstrap";
+import Head from "./TableHead";
 import Row from "./Row";
 import NavBar from "./NavBar";
+import NavButtons from "./NavButtons";
 import Types from "./Types";
 import { Member, Post, ProposalDetail, ProposalPost, Seat } from "../../types";
 
+const LIMIT = 10;
+
 interface IProps {
+  hideNav?: boolean;
   block: number;
   members: Member[];
   proposals: ProposalDetail[];
@@ -23,6 +27,7 @@ interface IState {
   asc: boolean;
   hidden: string[];
   showTypes: boolean;
+  page: number;
 }
 
 class ProposalTable extends React.Component<IProps, IState> {
@@ -34,10 +39,13 @@ class ProposalTable extends React.Component<IProps, IState> {
       hidden: [],
       author: "All",
       showTypes: false,
+      page: 1,
     };
     this.selectAuthor = this.selectAuthor.bind(this);
     this.toggleHide = this.toggleHide.bind(this);
     this.toggleShowTypes = this.toggleShowTypes.bind(this);
+    this.setPage = this.setPage.bind(this);
+    this.setKey = this.setKey.bind(this);
   }
 
   setKey(key: string) {
@@ -45,6 +53,9 @@ class ProposalTable extends React.Component<IProps, IState> {
     else this.setState({ key });
   }
 
+  setPage(page: number) {
+    this.setState({ page });
+  }
   toggleShowTypes() {
     this.setState({ showTypes: !this.state.showTypes });
   }
@@ -79,8 +90,15 @@ class ProposalTable extends React.Component<IProps, IState> {
   }
 
   render() {
-    const { block, councils, members, posts, proposalPosts } = this.props;
-    const { author, hidden } = this.state;
+    const {
+      hideNav,
+      block,
+      councils,
+      members,
+      posts,
+      proposalPosts,
+    } = this.props;
+    const { page, author, hidden } = this.state;
 
     // proposal types
     let types: { [key: string]: number } = {};
@@ -109,6 +127,7 @@ class ProposalTable extends React.Component<IProps, IState> {
     return (
       <div className="h-100 overflow-hidden bg-light">
         <NavBar
+          show={!hideNav}
           author={author}
           authors={authors}
           selectAuthor={this.selectAuthor}
@@ -122,49 +141,15 @@ class ProposalTable extends React.Component<IProps, IState> {
           types={types}
         />
 
-        <div className="d-flex flex-row justify-content-between p-2 bg-dark text-light text-left font-weight-bold">
-          <div onClick={() => this.setKey("id")}>ID</div>
-          <div className="col-2" onClick={() => this.setKey("author")}>
-            Author
-          </div>
-          <div className="col-3" onClick={() => this.setKey("description")}>
-            Description
-          </div>
-          <div className="col-2" onClick={() => this.setKey("type")}>
-            Type
-          </div>
-          <div className="col-1 text-center">
-            Result
-            <br />
-            <OverlayTrigger
-              placement="bottom"
-              overlay={
-                <Tooltip id={`approved`}>
-                  {approved} of {proposals.length} approved
-                </Tooltip>
-              }
-            >
-              <div>{Math.floor(100 * (approved / proposals.length))}%</div>
-            </OverlayTrigger>
-          </div>
-          <div className="col-2">
-            Voting Duration
-            <br />∅ {avgDays ? `${avgDays}d` : ""}
-            {avgHours ? `${avgHours}h` : ""}
-          </div>
-          <div className="col-1" onClick={() => this.setKey("createdAt")}>
-            Created
-          </div>
-          <div className="col-1" onClick={() => this.setKey("finalizedAt")}>
-            Finalized
-          </div>
-        </div>
-
-        <div
-          className="d-flex flex-column overflow-auto p-2"
-          style={{ height: `85%` }}
-        >
-          {proposals.map((p) => (
+        <Head
+          setKey={this.setKey}
+          approved={approved}
+          proposals={proposals.length}
+          avgDays={avgDays}
+          avgHours={avgHours}
+        />
+        <div className="d-flex flex-column overflow-auto p-2">
+          {proposals.slice((page - 1) * LIMIT, page * LIMIT).map((p) => (
             <Row
               key={p.id}
               {...p}
@@ -179,6 +164,12 @@ class ProposalTable extends React.Component<IProps, IState> {
             />
           ))}
         </div>
+        <NavButtons
+          setPage={this.setPage}
+          page={page}
+          limit={LIMIT}
+          proposals={proposals.length}
+        />
       </div>
     );
   }

+ 45 - 40
src/components/Proposals/Row.tsx

@@ -5,8 +5,8 @@ import { OverlayTrigger, Tooltip } from "react-bootstrap";
 import MemberOverlay from "../Members/MemberOverlay";
 import Bar from "./Bar";
 import Posts from "./Posts";
-import Votes from "./VotesTooltip";
-import VoteButton from "./VoteButton";
+import Detail from "./Detail";
+import { VoteNowButton, VotesTooltip, VotesBubbles } from "..";
 import moment from "moment";
 
 import {
@@ -24,10 +24,10 @@ const formatTime = (time: number) => {
 };
 
 const colors: { [key: string]: string } = {
-  Approved: "bg-success text-light",
-  Rejected: "bg-danger text-light",
-  Canceled: "bg-danger text-light",
-  Expired: "bg-warning text-dark",
+  Approved: "success",
+  Rejected: "danger ",
+  Canceled: "danger ",
+  Expired: "warning ",
   Pending: "",
 };
 
@@ -49,7 +49,7 @@ const ProposalRow = (props: {
   members: Member[];
   posts: ProposalPost[];
   votesByAccount?: Vote[];
-
+  detail?: any;
   // author overlay
   councils: Seat[][];
   forumPosts: Post[];
@@ -66,6 +66,7 @@ const ProposalRow = (props: {
     title,
     type,
     votes,
+    detail,
     members,
   } = props;
 
@@ -89,55 +90,59 @@ const ProposalRow = (props: {
 
   return (
     <div className="d-flex flex-row justify-content-between text-left p-2">
-      <div className="text-right">{id}</div>
-
-      <OverlayTrigger
-        placement={"right"}
-        overlay={
-          <Tooltip id={`overlay-${author}`} className="member-tooltip">
-            <MemberOverlay
-              handle={author}
-              members={members}
-              councils={props.councils}
-              proposals={props.proposals}
-              posts={props.forumPosts}
-              startTime={props.startTime}
-              validators={props.validators}
-            />
-          </Tooltip>
-        }
-      >
-        <div className="col-2">
-          <Link to={`/members/${author}`}>{author}</Link>
-        </div>
-      </OverlayTrigger>
-
       <div className="col-3">
-        <div className="float-right">
-          <Posts posts={props.posts} />
-        </div>
         <OverlayTrigger
           key={id}
           placement="right"
           overlay={<Tooltip id={String(id)}>{description}</Tooltip>}
         >
-          <Link to={`/proposals/${id}`}>{title}</Link>
+          <b>
+            <Link to={`/proposals/${id}`}>{title}</Link>
+          </b>
+        </OverlayTrigger>
+        <div>
+          <Posts posts={props.posts} />
+        </div>
+
+        <OverlayTrigger
+          placement={"right"}
+          overlay={
+            <Tooltip id={`overlay-${author}`} className="member-tooltip">
+              <MemberOverlay
+                handle={author}
+                members={members}
+                councils={props.councils}
+                proposals={props.proposals}
+                posts={props.forumPosts}
+                startTime={props.startTime}
+                validators={props.validators}
+              />
+            </Tooltip>
+          }
+        >
+          <div>
+            by
+            <Link to={`/members/${author}`}> {author}</Link>
+          </div>
         </OverlayTrigger>
       </div>
-      <div className="col-2">{type}</div>
+      <div className="col-2 text-left">
+        <Detail detail={detail} type={type} />
+      </div>
 
       <OverlayTrigger
         placement="left"
         overlay={
           <Tooltip id={`votes-${id}`}>
-            <Votes votes={props.votesByAccount} />
+            <VotesTooltip votesByAccount={props.votesByAccount} votes={votes} />
           </Tooltip>
         }
       >
-        <div className={`col-1 p-2 ${color}`}>
+        <div className={`col-1 p-2 border border-${color}`}>
           <b>{result}</b>
-          <br />
-          {JSON.stringify(Object.values(votes))}
+          <div className="d-flex flex-row">
+            <VotesBubbles votes={votes} />
+          </div>
         </div>
       </OverlayTrigger>
 
@@ -147,7 +152,7 @@ const ProposalRow = (props: {
 
       <div className="col-1">{created}</div>
       <div className="col-1">
-        {finalized ? finalized : <VoteButton show={true} url={url} />}
+        {finalized ? finalized : <VoteNowButton show={true} url={url} />}
       </div>
     </div>
   );

+ 33 - 0
src/components/Proposals/TableHead.tsx

@@ -0,0 +1,33 @@
+import React from "react";
+import { OverlayTrigger, Tooltip } from "react-bootstrap";
+
+const TableHead = (props: any) => {
+  const { setKey, approved, proposals, avgDays, avgHours } = props;
+  return (
+    <div className="d-flex flex-row justify-content-between p-2 bg-dark text-light text-left font-weight-bold">
+      <div className="col-3">
+        <span onClick={() => setKey("description")}>Description </span>/
+        <span onClick={() => setKey("author")}> Author</span>
+      </div>
+      <div className="col-2 text-left" onClick={() => setKey("type")}>
+        Type
+      </div>
+      <div className="col-2 text-center">
+        approved: {approved} of {proposals} (
+        {Math.floor(100 * (approved / proposals))}%)
+      </div>
+      <div className="col-2">
+        Voting Duration
+        <br />∅ {avgDays ? `${avgDays}d` : ""}
+        {avgHours ? `${avgHours}h` : ""}
+      </div>
+      <div className="col-1" onClick={() => setKey("createdAt")}>
+        Created
+      </div>
+      <div className="col-1" onClick={() => setKey("finalizedAt")}>
+        Finalized
+      </div>
+    </div>
+  );
+};
+export default TableHead;

+ 0 - 15
src/components/Proposals/VoteButton.tsx

@@ -1,15 +0,0 @@
-import React from "react";
-import { Button } from "react-bootstrap";
-
-const VoteButton = (props: { show: boolean; url: string }) => {
-  const { show, url } = props;
-  if (!show) return <div />;
-
-  return (
-    <Button variant="danger">
-      <a href={url}>Vote!</a>
-    </Button>
-  );
-};
-
-export default VoteButton;

+ 0 - 25
src/components/Proposals/VotesTooltip.tsx

@@ -1,25 +0,0 @@
-import React from "react";
-import { Table } from "react-bootstrap";
-import { Vote } from "../../types";
-
-const VotesTooltip = (props: { votes?: Vote[] }) => {
-  if (!props.votes) return <div>Fetching votes..</div>;
-
-  const votes = props.votes.filter((v) => (v.vote === `` ? false : true));
-  if (!votes.length) return <div>No votes</div>;
-
-  return (
-    <Table className="text-left text-light">
-      <tbody>
-        {votes.map((v) => (
-          <tr key={v.handle}>
-            <td>{v.handle}:</td>
-            <td>{v.vote}</td>
-          </tr>
-        ))}
-      </tbody>
-    </Table>
-  );
-};
-
-export default VotesTooltip;

+ 34 - 6
src/components/Routes/index.tsx

@@ -11,36 +11,64 @@ import {
   Proposal,
   Timeline,
   Tokenomics,
+  Validators,
 } from "..";
 import { IState } from "../../types";
 
-const Routes = (props: IState) => {
+interface IProps extends IState {
+  toggleStar: (a: string) => void;
+  toggleFooter: () => void;
+}
+
+const Routes = (props: IProps) => {
   const { reports, tokenomics } = props;
   return (
     <Switch>
       <Route
         path="/tokenomics"
-        render={() => <Tokenomics reports={reports} tokenomics={tokenomics} />}
+        render={(routeprops) => (
+          <Tokenomics
+            {...routeprops}
+            reports={reports}
+            tokenomics={tokenomics}
+          />
+        )}
       />
       <Route
         path="/proposals/:id"
         render={(routeprops) => <Proposal {...routeprops} {...props} />}
       />
       <Route path="/proposals" render={() => <Proposals {...props} />} />
-      <Route path="/councils" render={() => <Councils {...props} />} />
+      <Route
+        path="/councils"
+        render={(routeprops) => <Councils {...routeprops} {...props} />}
+      />
       <Route
         path="/forum/threads/:thread"
         render={(routeprops) => <Forum {...routeprops} {...props} />}
       />
       <Route path="/forum" render={() => <Forum {...props} />} />
-      <Route path="/mint" render={() => <Mint {...props} />} />
+      <Route
+        path="/mint"
+        render={(routeprops) => <Mint {...routeprops} {...props} />}
+      />
       <Route
         path="/members/:handle"
         render={(routeprops) => <Member {...routeprops} {...props} />}
       />
-      <Route path="/members" render={() => <Members {...props} />} />
-      <Route path="/calendar" render={() => <Calendar {...props} />} />
+      <Route
+        path="/members"
+        render={(routeprops) => <Members {...routeprops} {...props} />}
+      />
+      <Route
+        path="/calendar"
+        render={(routeprops) => <Calendar {...routeprops} {...props} />}
+      />
       <Route path="/timeline" render={() => <Timeline {...props} />} />
+      <Route
+        path="/validators"
+        render={(routeprops) => <Validators {...routeprops} {...props} />}
+      />
       <Route path="/" render={() => <Dashboard {...props} />} />
     </Switch>
   );

+ 35 - 0
src/components/TableFromObject.tsx

@@ -0,0 +1,35 @@
+import React from "react";
+import { Table } from "react-bootstrap";
+
+const TableFromObject = (props: {
+  data: string | { [key: string]: string };
+}) => {
+  const { data } = props;
+  if (!data) return <span />;
+  if (typeof data === "string") return <span>{data}</span>;
+  const keys = Object.keys(data);
+  return (
+    <Table className="text-light">
+      <tbody>
+        {keys.map((key: string) =>
+          typeof data[key] === "object" ? (
+            <tr key={key}>
+              <td colSpan={2}>
+                <TableFromObject data={data[key]} />
+              </td>
+            </tr>
+          ) : (
+            <tr key={key}>
+              <td className="text-left text-bold">
+                <b>{key}</b>
+              </td>
+              <td className="text-right">{data[key]}</td>
+            </tr>
+          )
+        )}
+      </tbody>
+    </Table>
+  );
+};
+
+export default TableFromObject;

+ 6 - 0
src/components/Timeline/index.tsx

@@ -1,4 +1,6 @@
 import React from "react";
+import { Link } from "react-router-dom";
+import { Button } from "react-bootstrap";
 import TimelineItem from "./Item";
 import { Event, Post, ProposalDetail } from "../../types";
 
@@ -50,6 +52,10 @@ const Timeline = (props: {
 
   return (
     <div className="timeline-container">
+      <Link className="back left" to={"/"}>
+        <Button variant="secondary">Back</Button>
+      </Link>
+
       {events
         .sort((a, b) => b.date - a.date)
         .map((event: Event, idx) => (

+ 2 - 2
src/components/Tokenomics/Navigation.tsx

@@ -3,10 +3,10 @@ import { Button } from "react-bootstrap";
 import { Link } from "react-router-dom";
 import Back from "../Back";
 
-const Navigation = () => {
+const Navigation = (props:{history:any}) => {
   return (
     <div className="d-flex flex-row justify-content-center">
-      <Back />
+      <Back history={props.history} />
 
       <Link to={`/councils`}>
         <Button variant="secondary" className="p-1 m-1">

+ 2 - 1
src/components/Tokenomics/index.tsx

@@ -10,6 +10,7 @@ import { Tokenomics } from "../../types";
 interface IProps {
   reports: { [key: string]: string };
   tokenomics?: Tokenomics;
+  history:any
 }
 
 const CouncilReports = (props: IProps) => {
@@ -30,7 +31,7 @@ const CouncilReports = (props: IProps) => {
           extecutedBurnsAmount={extecutedBurnsAmount}
         />
 
-        <Navigation />
+        <Navigation history={props.history} />
       </div>
 
       <div className="box col-8">

+ 58 - 52
src/components/Validators/MinMax.tsx

@@ -1,10 +1,14 @@
 import React from "react";
+import { Table } from "react-bootstrap";
+import { Link } from "react-router-dom";
 import { Stakes } from "../../types";
 
 const dollar = (d: number) => (d > 0 ? `$ ${d.toFixed(2)}` : "");
 
 const MinMax = (props: {
   stakes?: { [key: string]: Stakes };
+  block: number;
+  era: number;
   issued: number;
   validators: string[];
   nominators: number;
@@ -28,60 +32,62 @@ const MinMax = (props: {
     if (total < minStake) minStake = total;
   });
 
-  return (
-    <div className="float-right text-right d-none d-md-block">
-      <div className="mb-2">
-        <div>
-          <div className="float-left mr-1">nominators:</div>
-          {props.nominators}
-        </div>
-        <div>
-          <div className="float-left mr-1">validators:</div>
-          {validators.length}
-        </div>
-        <div>
-          <div className="float-left mr-1">waiting:</div>
-          {waiting}
-        </div>
-      </div>
-
-      <b>total stake</b>
-      <div className="mb-2">
-        <div>{(sum / 1000000).toFixed(1)} M JOY</div>/{" "}
-        {(issued / 1000000).toFixed(1)} M JOY
-        <div>({((sum / issued) * 100).toFixed(1)}%)</div>
-      </div>
-      <div>
-        <div className="float-left mr-1">min:</div> {minStake} JOY
-      </div>
-      <div>
-        <div className="float-left mr-1">max:</div> {maxStake} JOY
-      </div>
-
-      <Reward reward={reward} price={price} validators={validators.length} />
-    </div>
-  );
-};
-
-const Reward = (props: {
-  reward: number;
-  price: number;
-  validators: number;
-}) => {
-  const { reward, price, validators } = props;
-  if (!reward) return <div />;
+  const name = { className: "text-right" };
+  const value = { className: "text-left" };
 
   return (
-    <div className="mt-2">
-      <b>total reward per hour</b>
-      <div>{reward} JOY</div>
-      <div>{dollar(price * reward)}</div>
-      <div className="mt-2">per validator:</div>
-      <div className="text-warning">
-        {(reward / validators).toFixed(0)} JOY
-        <div>{dollar((price * reward) / validators)}</div>
-      </div>
-    </div>
+    <Table className="bg-secondary w-50">
+      <tbody>
+        <tr>
+          <td {...name}>Validators</td>
+          <td {...value}>{validators.length}</td>
+        </tr>
+        <tr>
+          <td {...name}>Waiting</td>
+          <td {...value}>{waiting}</td>
+        </tr>
+        <tr>
+          <td {...name}>Nominators</td>
+          <td {...value}>{props.nominators}</td>
+        </tr>
+
+        <tr>
+          <td {...name}>Issued</td>
+          <td {...value}> {(issued / 1000000).toFixed(1)} M JOY</td>
+        </tr>
+        <tr>
+          <td {...name}>Staked</td>
+          <td {...value}>
+            {(sum / 1000000).toFixed(1)} M JOY (
+            <b>{((sum / issued) * 100).toFixed(1)}%</b>)
+          </td>
+        </tr>
+        <tr>
+          <td {...name}>Min stake</td>
+          <td {...value}>{minStake} JOY</td>
+        </tr>
+        <tr>
+          <td {...name}>Max stake</td>
+          <td {...value}>{maxStake} JOY </td>
+        </tr>
+        <tr>
+          <td {...name}>Total payed per hour</td>
+          <td {...value}>
+            {reward} JOY ({dollar(price * reward)}){" "}
+          </td>
+        </tr>
+        <tr>
+          <td {...name}>Reward per validator per hour</td>
+          <td className="text-left text-warning">
+            {(reward / validators.length).toFixed(0)} JOY (
+            {dollar((price * reward) / validators.length)})
+            <Link className="ml-1" to={"/mint"}>
+              Details
+            </Link>
+          </td>
+        </tr>
+      </tbody>
+    </Table>
   );
 };
 

+ 14 - 16
src/components/Validators/Validator.tsx

@@ -96,28 +96,19 @@ class Validator extends Component<IProps, IState> {
     }
 
     return (
-      <div className="d-flex flex-row justify-content-around">
-        <div className="col-2 col-md-1">
-          <Minus width={15} color={"black"} onClick={this.hide} />
-          <Star
-            width={15}
-            color={"black"}
-            fill={starred ? "black" : "teal"}
-            onClick={() => toggleStar(validator)}
-          />
-          <a
-            href={`${domain}/#/staking/query/${validator}`}
-            title="Show Stats (External)"
-          >
-            <Activity width={15} />
-          </a>
-        </div>
+      <div className="mt-2 d-flex flex-row justify-content-around">
         <div
           className="col-1 text-right"
           onClick={() => sortBy("points")}
           title="era points"
         >
           {points}
+          <a
+            href={`${domain}/#/staking/query/${validator}`}
+            title="Show Stats (External)"
+          >
+            <Activity width={15} />
+          </a>
         </div>
         <div className="col-2 text-right">
           <MemberBox
@@ -132,6 +123,13 @@ class Validator extends Component<IProps, IState> {
             startTime={startTime}
             validators={this.props.validators}
           />
+          <Star
+            width={15}
+            color={"black"}
+            fill={starred ? "black" : "teal"}
+            className="ml-2 mb-2"
+            onClick={() => toggleStar(validator)}
+          />
         </div>
 
         <div

+ 75 - 45
src/components/Validators/index.tsx

@@ -1,6 +1,7 @@
 import React, { Component } from "react";
-import { ListGroup } from "react-bootstrap";
-import MinMax from "./MinMax";
+import { Link } from "react-router-dom";
+import { Button, ListGroup } from "react-bootstrap";
+import Stats from "./MinMax";
 import Validator from "./Validator";
 import MemberBox from "../Members/MemberBox";
 import {
@@ -11,6 +12,7 @@ import {
   Seat,
   Stakes,
   RewardPoints,
+  Tokenomics,
 } from "../../types";
 
 interface IProps {
@@ -26,33 +28,37 @@ interface IProps {
   stashes: string[];
   nominators: string[];
   stars: { [key: string]: boolean };
-  save: (target: string, data: any) => void;
+  toggleStar: (account: string) => void;
   stakes?: { [key: string]: Stakes };
   rewardPoints?: RewardPoints;
   lastReward: number;
-
-  issued: number;
-  price: number;
+  tokenomics?: Tokenomics;
+  hideBackButton?: boolean;
 }
 
 interface IState {
   sortBy: string;
+  showValidators: boolean;
+  showWaiting: boolean;
 }
 
 class Validators extends Component<IProps, IState> {
   constructor(props: IProps) {
     super(props);
-    this.state = { sortBy: "totalStake" };
-    this.toggleStar = this.toggleStar.bind(this);
+    this.state = {
+      sortBy: "totalStake",
+      showWaiting: false,
+      showValidators: false,
+    };
     this.setSortBy = this.setSortBy.bind(this);
   }
 
-  toggleStar(account: string) {
-    let { stars } = this.props;
-    stars[account] = !stars[account];
-    this.props.save("stars", stars);
+  toggleValidators() {
+    this.setState({ showValidators: !this.state.showValidators });
+  }
+  toggleWaiting() {
+    this.setState({ showWaiting: !this.state.showWaiting });
   }
-
   setSortBy(sortBy: string) {
     this.setState({ sortBy });
   }
@@ -105,6 +111,7 @@ class Validators extends Component<IProps, IState> {
 
   render() {
     const {
+      hideBackButton,
       block,
       era,
       now,
@@ -120,10 +127,12 @@ class Validators extends Component<IProps, IState> {
       lastReward,
       rewardPoints,
       stakes,
-      issued,
-      price,
+      tokenomics,
     } = this.props;
-    const { sortBy } = this.state;
+    const issued = tokenomics ? Number(tokenomics.totalIssuance) : 0;
+    const price = tokenomics ? Number(tokenomics.price) : 0;
+
+    const { sortBy, showWaiting, showValidators } = this.state;
     const startTime = now - block * 6000;
 
     const starred = stashes.filter((v) => stars[v]);
@@ -131,11 +140,19 @@ class Validators extends Component<IProps, IState> {
     const waiting = stashes.filter((s) => !stars[s] && !validators.includes(s));
     if (!unstarred.length) return <div />;
     return (
-      <div className="box w-100 m-0 px-5 mb-5">
-        <div className="float-left">
-          last block: {block}, era {era}
-        </div>
-        <MinMax
+      <div className="box w-100 !mx-5">
+        {hideBackButton ? (
+          ""
+        ) : (
+          <Link className="back" to={"/"}>
+            <Button variant="secondary">Back</Button>
+          </Link>
+        )}
+        <h3 onClick={() => this.toggleValidators()}>Validator Stats</h3>
+
+        <Stats
+          block={block}
+          era={era}
           stakes={stakes}
           issued={issued}
           price={price}
@@ -145,34 +162,13 @@ class Validators extends Component<IProps, IState> {
           reward={lastReward}
         />
 
-        <h3>Validators</h3>
-
         <div className="d-flex flex-column">
           {this.sortBy(sortBy, starred).map((v) => (
             <Validator
               key={v}
               sortBy={this.setSortBy}
               starred={stars[v] ? `teal` : undefined}
-              toggleStar={this.toggleStar}
-              startTime={startTime}
-              validator={v}
-              reward={lastReward / validators.length}
-              councils={councils}
-              handles={handles}
-              members={members}
-              posts={posts}
-              proposals={proposals}
-              validators={validators}
-              stakes={stakes}
-              rewardPoints={rewardPoints}
-            />
-          ))}
-          {this.sortBy(sortBy, unstarred).map((v) => (
-            <Validator
-              key={v}
-              sortBy={this.setSortBy}
-              starred={stars[v] ? `teal` : undefined}
-              toggleStar={this.toggleStar}
+              toggleStar={this.props.toggleStar}
               startTime={startTime}
               validator={v}
               reward={lastReward / validators.length}
@@ -186,10 +182,44 @@ class Validators extends Component<IProps, IState> {
               rewardPoints={rewardPoints}
             />
           ))}
-          {waiting.length ? (
+
+          <Button variant="secondary" onClick={() => this.toggleValidators()}>
+            Toggle {unstarred.length} validators
+          </Button>
+
+          {showValidators &&
+            this.sortBy(sortBy, unstarred).map((v) => (
+              <Validator
+                key={v}
+                sortBy={this.setSortBy}
+                starred={stars[v] ? `teal` : undefined}
+                toggleStar={this.props.toggleStar}
+                startTime={startTime}
+                validator={v}
+                reward={lastReward / validators.length}
+                councils={councils}
+                handles={handles}
+                members={members}
+                posts={posts}
+                proposals={proposals}
+                validators={validators}
+                stakes={stakes}
+                rewardPoints={rewardPoints}
+              />
+            ))}
+
+          <Button
+            variant="secondary"
+            className="mb-5"
+            onClick={() => this.toggleWaiting()}
+          >
+            Toggle {waiting.length} waiting nodes
+          </Button>
+
+          {waiting.length && showWaiting ? (
             <ListGroup className="waiting-validators">
               <hr />
-              <h4>Waiting</h4>
+              <h4 onClick={() => this.toggleWaiting()}>Waiting</h4>
               {waiting.map((v) => (
                 <ListGroup.Item key={v}>
                   <MemberBox

+ 110 - 0
src/components/Votes.tsx

@@ -0,0 +1,110 @@
+import React from "react";
+import { Button } from "react-bootstrap";
+import { Vote } from "../types";
+import { VotingResults } from "@joystream/types/proposals";
+
+export const voteKeys: { [key: string]: string } = {
+  abstensions: "Abstain",
+  approvals: "Approve",
+  rejections: "Reject",
+  slashes: "Slash",
+};
+
+export const voteStyles: { [key: string]: string } = {
+  Abstain: "secondary",
+  Approve: "success",
+  Reject: "danger",
+  Slash: "warning",
+  "": "body",
+};
+
+export const VoteButton = (props: { handle: string; vote: string }) => {
+  const { handle, vote } = props;
+  return (
+    <Button title={vote} className="m-1" variant={voteStyles[vote]}>
+      {handle}
+    </Button>
+  );
+};
+
+// Vote!
+
+export const VoteNowButton = (props: { show: boolean; url: string }) => {
+  const { show, url } = props;
+  if (!show) return <div />;
+
+  return (
+    <Button variant="danger">
+      <a href={url}>Vote!</a>
+    </Button>
+  );
+};
+
+// Bubbles
+
+const VoteBubble = (props: {
+  detailed?: boolean;
+  vote: string;
+  count: number;
+}) => {
+  const { count, detailed, vote } = props;
+  if (!count) return <span />;
+  return (
+    <Button className="btn-sm m-0" variant={voteStyles[voteKeys[vote]]}>
+      {count} {detailed && vote}
+    </Button>
+  );
+};
+
+export const VotesBubbles = (props: {
+  detailed?: boolean;
+  votes: VotingResults | { [key: string]: number }; // VotingResults refuses string keys
+}) => {
+  const { detailed } = props;
+  const votes = JSON.parse(JSON.stringify(props.votes)); // TODO
+  return (
+    <div>
+      {Object.keys(votes).map((vote: string) => (
+        <VoteBubble
+          key={vote}
+          detailed={detailed}
+          vote={vote}
+          count={votes[vote]}
+        />
+      ))}
+    </div>
+  );
+};
+
+// Tooltip
+
+interface IProps {
+  votes: VotingResults;
+  votesByAccount?: Vote[];
+}
+// TODO Property 'votes' does not exist on type 'IntrinsicAttributes & IProps'
+// https://stackoverflow.com/questions/59969756/not-assignable-to-type-intrinsicattributes-intrinsicclassattributes-react-js
+
+export const VotesTooltip = (props: any) => {
+  const { votesByAccount } = props;
+  if (!votesByAccount)
+    return (
+      <div>
+        Fetching votes..
+        <VotesBubbles detailed={true} votes={props.votes} />
+      </div>
+    );
+
+  const votes = votesByAccount.filter((v: Vote) =>
+    v.vote === `` ? false : true
+  );
+  if (!votes.length) return <div>No votes</div>;
+
+  return (
+    <div className="text-left text-light">
+      {votes.map((vote: Vote) => (
+        <VoteButton {...vote} />
+      ))}
+    </div>
+  );
+};

+ 10 - 0
src/components/index.ts

@@ -17,3 +17,13 @@ export { default as Members } from "./Members";
 export { default as Tokenomics } from "./Tokenomics";
 export { default as Validators } from "./Validators";
 export { default as Timeline } from "./Timeline";
+export { default as TableFromObject } from "./TableFromObject";
+
+export {
+  voteStyles,
+  voteKeys,
+  VoteButton,
+  VoteNowButton,
+  VotesBubbles,
+  VotesTooltip,
+} from "./Votes";

+ 133 - 114
src/index.css

@@ -68,199 +68,218 @@ table td {
 
 .title {
   position: fixed;
-  right: 10px;
-  bottom: 0px;
+  top: 0px;
+  left: 0px;
 }
 .user {
   min-width: 75px;
 }
 
-
 /* calendar */
 
 .rct-sidebar-row {
-    color: #fff;
+  color: #fff;
 }
 .rct-item-content {
-    font-size: 0.8em;
+  font-size: 0.8em;
 }
 
-
 /* timeline */
 
 .member-tooltip .tooltip-inner div {
-    width: 350px !important;
+  width: 350px !important;
 }
 .member-tooltip .tooltip-inner {
-    padding: 0 !important;
+  padding: 0 !important;
 }
 
 .timeline-container {
-    display: flex;
-    flex-direction: column;
-    position: relative;
-    margin: 40px 0;
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  margin: 40px 0;
 }
 
 .timeline-container::after {
-    background-color: #e17b77;
-    content: '';
-    position: absolute;
-    left: calc(50% - 2px);
-    width: 4px;
-    height: 100%;
+  background-color: #e17b77;
+  content: "";
+  position: absolute;
+  left: calc(50% - 2px);
+  width: 4px;
+  height: 100%;
 }
 
 .timeline-item {
-    display: flex;
-    justify-content: flex-end;
-    padding-right: 30px;
-    position: relative;
-    margin: 10px 0;
-    width: 50%;
+  display: flex;
+  justify-content: flex-end;
+  padding-right: 30px;
+  position: relative;
+  margin: 10px 0;
+  width: 50%;
 }
 
 .timeline-item:nth-child(odd) {
-    align-self: flex-end;
-    justify-content: flex-start;
-    padding-left: 30px;
-    padding-right: 0;
+  align-self: flex-end;
+  justify-content: flex-start;
+  padding-left: 30px;
+  padding-right: 0;
 }
 
 .timeline-item-content {
-    box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
-    border-radius: 5px;
-    background-color: #fff;
-    display: flex;
-    flex-direction: column;
-    align-items: flex-end;
-    padding: 15px;
-    position: relative;
-    width: 400px;
-    max-width: 70%;
-    text-align: right;
-    overflow:hidden;
+  box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
+  border-radius: 5px;
+  background-color: #fff;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  padding: 15px;
+  position: relative;
+  width: 400px;
+  max-width: 70%;
+  text-align: right;
+  overflow: hidden;
 }
 
 .timeline-item-content::after {
-    content: ' ';
-    background-color: #fff;
-    box-shadow: 1px -1px 1px rgba(0, 0, 0, 0.2);
-    position: absolute;
-    right: -7.5px;
-    top: calc(50% - 7.5px);
-    transform: rotate(45deg);
-    width: 15px;
-    height: 15px;
+  content: " ";
+  background-color: #fff;
+  box-shadow: 1px -1px 1px rgba(0, 0, 0, 0.2);
+  position: absolute;
+  right: -7.5px;
+  top: calc(50% - 7.5px);
+  transform: rotate(45deg);
+  width: 15px;
+  height: 15px;
 }
 
 .timeline-item:nth-child(odd) .timeline-item-content {
-    text-align: left;
-    align-items: flex-start;
+  text-align: left;
+  align-items: flex-start;
 }
 
 .timeline-item:nth-child(odd) .timeline-item-content::after {
-    right: auto;
-    left: -7.5px;
-    box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.2);
+  right: auto;
+  left: -7.5px;
+  box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.2);
 }
 
 .timeline-item-content .tag {
-    color: #fff;
-    font-size: 0.8em;
-    font-weight: bold;
-    top: 5px;
-    left: 5px;
-    letter-spacing: 1px;
-    padding: 5px;
-    position: absolute;
-    text-transform: uppercase;
+  color: #fff;
+  font-size: 0.8em;
+  font-weight: bold;
+  top: 5px;
+  left: 5px;
+  letter-spacing: 1px;
+  padding: 5px;
+  position: absolute;
+  text-transform: uppercase;
 }
 
 .timeline-item:nth-child(odd) .timeline-item-content .tag {
-    left: auto;
-    right: 5px;
+  left: auto;
+  right: 5px;
 }
 
 .timeline-item-content time {
-    color: #777;
-    font-size: 0.8em;
-    font-weight: bold;
+  color: #777;
+  font-size: 0.8em;
+  font-weight: bold;
 }
 
 .timeline-item-content p {
-    font-size: 0.8.em;
-    margin: 15px 0;
-    max-width: 250px;
+  font-size: 0.8.em;
+  margin: 15px 0;
+  max-width: 250px;
 }
 
 .timeline-item-content a {
-    font-size: 1em;
-    font-weight: bold;
+  font-size: 1em;
+  font-weight: bold;
 }
 
 .timeline-item-content a::after {
-    content: ' ►';
-    font-size: 1em;
+  content: " ►";
+  font-size: 1em;
 }
 
 .timeline-item-content .circle {
-    background-color: #fff;
-    border: 3px solid #e17b77;
-    border-radius: 50%;
-    position: absolute;
-    top: calc(50% - 10px);
-    right: -40px;
-    width: 20px;
-    height: 20px;
-    z-index: 100;
+  background-color: #fff;
+  border: 3px solid #e17b77;
+  border-radius: 50%;
+  position: absolute;
+  top: calc(50% - 10px);
+  right: -40px;
+  width: 20px;
+  height: 20px;
+  z-index: 100;
 }
 
 .timeline-item:nth-child(odd) .timeline-item-content .circle {
-    right: auto;
-    left: -40px;
+  right: auto;
+  left: -40px;
 }
 
 @media only screen and (max-width: 1023px) {
-    .timeline-item-content {
-        max-width: 100%;
-    }
+  .timeline-item-content {
+    max-width: 100%;
+  }
 }
 
 @media only screen and (max-width: 767px) {
-    .timeline-item-content,
-    .timeline-item:nth-child(odd) .timeline-item-content {
-        padding: 15px 10px;
-        text-align: center;
-        align-items: center;
-    }
+  .timeline-item-content,
+  .timeline-item:nth-child(odd) .timeline-item-content {
+    padding: 15px 10px;
+    text-align: center;
+    align-items: center;
+  }
 
-    .timeline-item-content .tag {
-        width: calc(100% - 10px);
-        text-align: center;
-    }
+  .timeline-item-content .tag {
+    width: calc(100% - 10px);
+    text-align: center;
+  }
 
-    .timeline-item-content time {
-        margin-top: 20px;
-    }
+  .timeline-item-content time {
+    margin-top: 20px;
+  }
 
-    .timeline-item-content a {
-        text-decoration: underline;
-    }
+  .timeline-item-content a {
+    text-decoration: underline;
+  }
 
-    .timeline-item-content a::after {
-        display: none;
-    }
+  .timeline-item-content a::after {
+    display: none;
+  }
 }
 
 .waiting-validators .list-group-item {
-    background-color: teal !important;
+  background-color: teal !important;
 }
 
+.connecting {
+  background: orange;
+  position: fixed;
+  left: 0px;
+  bottom: 0px;
+  padding: 5px;
+}
+.back {
+  position: fixed;
+  right: 0px;
+  top: 0px;
+}
+.left {
+  left: 0px;
+}
 .footer {
-    background:  teal;
-    position:    fixed;
-    bottom:      0px;
-    padding:     10px;
-    text-align:  center;
+  background: teal;
+  position: fixed;
+  bottom: 0px;
+  padding: 10px;
+  width: 100%;
+  text-align: center;
+}
+.footer-hidden {
+  position: fixed;
+  right: 0px;
+  bottom: 0px;
 }

+ 5 - 0
src/types.ts

@@ -19,6 +19,7 @@ export interface IState {
   //gethandle: (account: AccountId | string)  => string;
   connecting: boolean;
   now: number;
+  era:number;
   block: number;
   blocks: Block[];
   nominators: string[];
@@ -29,6 +30,8 @@ export interface IState {
   councilElection?: { stage: any; round: number; termEndsAt: number };
   channels: Channel[];
   categories: Category[];
+  issued: number;
+  price: number;
   proposals: ProposalDetail[];
   posts: Post[];
   threads: Thread[];
@@ -44,6 +47,7 @@ export interface IState {
   stakes?: { [key: string]: Stakes };
   rewardPoints?: RewardPoints;
   lastReward: number;
+  hideFooter: boolean;
 }
 
 export interface RewardPoints {
@@ -105,6 +109,7 @@ export interface ProposalDetail {
   votesByAccount?: Vote[];
   author: string;
   authorId: number;
+  detail? : any
 }
 
 export interface Vote {