Browse Source

design improvements

Joystream Stats 3 năm trước cách đây
mục cha
commit
7beee9c705

+ 84 - 56
src/App.tsx

@@ -1,7 +1,7 @@
 import React from "react";
 import "bootstrap/dist/css/bootstrap.min.css";
 import "./index.css";
-import { Routes, Loading, Footer, Status } from "./components";
+import { Modals, Routes, Loading, Footer, Status } from "./components";
 
 import * as get from "./lib/getters";
 import { domain, wsLocation } from "./config";
@@ -54,12 +54,14 @@ const initialState = {
   stakes: {},
   stashes: [],
   stars: {},
-  hideFooter: false,
+  hideFooter: true,
+  showStatus: false,
   status: { era: 0, block: { id: 0, era: 0, timestamp: 0, duration: 6 } },
 };
 
 class App extends React.Component<IProps, IState> {
   initializeSocket() {
+    socket.on("disconnect", () => setTimeout(this.initializeSocket, 1000));
     socket.on("connect", () => {
       if (!socket.id) return console.log("no websocket connection");
       console.log("my socketId:", socket.id);
@@ -84,7 +86,7 @@ class App extends React.Component<IProps, IState> {
   }
 
   async handleBlock(api, header: Header) {
-    let { blocks, status } = this.state;
+    let { blocks, status, queue } = this.state;
     const id = header.number.toNumber();
     if (blocks.find((b) => b.id === id)) return;
     const timestamp = (await api.query.timestamp.now()).toNumber();
@@ -93,13 +95,14 @@ class App extends React.Component<IProps, IState> {
     status.block = { id, timestamp, duration };
     this.save("status", status);
 
-    blocks = blocks.concat(status.block);
+    blocks = this.addOrReplace(blocks, status.block);
     this.setState({ blocks });
 
     if (id / 50 === Math.floor(id / 50)) {
       this.updateStatus(api, id);
       this.fetchTokenomics();
     }
+    if (!queue.length) this.findJob(api);
   }
 
   async updateStatus(api: Api, id = 0) {
@@ -214,15 +217,17 @@ class App extends React.Component<IProps, IState> {
       principal,
     };
 
-    //console.debug(data, channel);
-    const channels = this.state.channels.concat(channel);
+    const channels = this.addOrReplace(this.state.channels, channel);
     this.save("channels", channels);
     if (id > 1)
       this.enqueue(`channel ${id - 1}`, () => this.fetchChannel(api, id - 1));
   }
 
   async fetchCategory(api: Api, id: number) {
-    if (this.state.categories.find((c) => c.id === id)) return;
+    if (!id) return;
+    const exists = this.state.categories.find((c) => c.id === id);
+    if (exists) return this.fetchCategory(api, id - 1);
+
     const data = await api.query.forum.categoryById(id);
     const threadId = Number(data.thread_id);
     const title = String(data.title);
@@ -251,13 +256,14 @@ class App extends React.Component<IProps, IState> {
       moderatorId,
     };
 
-    this.save("categories", this.state.categories.concat(category));
-    if (id > 1)
-      this.enqueue(`category ${id - 1}`, () => this.fetchCategory(api, id - 1));
+    this.save("categories", this.addOrReplace(this.state.categories, category));
+    this.enqueue(`category ${id - 1}`, () => this.fetchCategory(api, id - 1));
   }
+
   async fetchPost(api: Api, id: number) {
+    if (!id) return;
     const exists = this.state.posts.find((p) => p.id === id);
-    if (exists) return this.fetchMemberByAccount(api, exists.authorId);
+    if (exists) return this.fetchPost(api, id - 1);
 
     const data = await api.query.forum.postById(id);
     const threadId = Number(data.thread_id);
@@ -269,14 +275,16 @@ class App extends React.Component<IProps, IState> {
     this.fetchMemberByAccount(api, authorId);
 
     const post: Post = { id, threadId, text, authorId, createdAt };
-    const posts = this.state.posts.concat(post);
+    const posts = this.addOrReplace(this.state.posts, post);
     this.save("posts", posts);
-    if (id > 1)
-      this.enqueue(`post ${id - 1}`, () => this.fetchPost(api, id - 1));
+    this.enqueue(`post ${id - 1}`, () => this.fetchPost(api, id - 1));
   }
 
   async fetchThread(api: Api, id: number) {
-    if (this.state.threads.find((t) => t.id === id)) return;
+    if (!id) return;
+    const exists = this.state.threads.find((t) => t.id === id);
+    if (exists) return this.fetchThread(api, id - 1);
+
     const data = await api.query.forum.threadById(id);
 
     const title = String(data.title);
@@ -295,50 +303,58 @@ class App extends React.Component<IProps, IState> {
       createdAt,
       authorId,
     };
-    const threads = this.state.threads.concat(thread);
+    const threads = this.addOrReplace(this.state.threads, thread);
     this.save("threads", threads);
-    if (id > 1)
-      this.enqueue(`thread ${id - 1}`, () => this.fetchThread(api, id - 1));
+    this.enqueue(`thread ${id - 1}`, () => this.fetchThread(api, id - 1));
   }
 
   // council
-  async fetchCouncils(api: Api) {
-    const currentRound = await api.query.councilElection.round();
-    for (let round = Number(currentRound.toJSON()); round > 0; round--)
+  async fetchCouncils(api: Api, currentRound: number) {
+    for (let round = currentRound; round > 0; round--)
       this.enqueue(`council ${round}`, () => this.fetchCouncil(api, round));
   }
 
   async fetchCouncil(api: Api, round: number) {
-    const council = await api.query.council.activeCouncil();
     let { councils } = this.state;
-    councils[round] = council.toJSON();
+    councils[round] = (await api.query.council.activeCouncil()).toJSON();
+    councils[round].map((c) => this.fetchMemberByAccount(api, c.member));
     this.save("councils", councils);
-    council.map((c) => this.fetchMemberByAccount(api, c.member));
   }
 
   async updateCouncil(api: Api, block: number) {
-    const stage = await api.query.councilElection.stage();
-    const json = stage.toJSON();
-    const key = Object.keys(json)[0];
-    const stageEndsAt = json[key];
+    console.debug(`Updating council`);
+    const round = Number((await api.query.councilElection.round()).toJSON());
     const termEndsAt = Number((await api.query.council.termEndsAt()).toJSON());
+    const stage = (await api.query.councilElection.stage()).toJSON();
+    let stageEndsAt = 0;
+    if (stage) {
+      const key = Object.keys(stage)[0];
+      stageEndsAt = stage[key];
+    }
 
-    let { status } = this.state;
-    const { council } = status;
-    if (council)
-      if (block < council.termEndsAt || block < council.stageEndsAt) return;
-
-    const round = Number((await api.query.councilElection.round()).toJSON());
-    status.council = { round, stageEndsAt, termEndsAt, stage: stage.toJSON() };
-    this.save("status", status);
-    this.fetchCouncil(api, round);
+    const stages = [
+      "announcingPeriod",
+      "votingPeriod",
+      "revealingPeriod",
+      "newTermDuration",
+    ];
+    let durations = await Promise.all(
+      stages.map((s) => api.query.councilElection[s]())
+    ).then((stages) => stages.map((stage) => stage.toJSON()));
+    durations.push(durations.reduce((a, b) => a + b, 0));
+    this.fetchCouncils(api, round);
+    return { round, stageEndsAt, termEndsAt, stage, durations };
   }
 
   // proposals
   async fetchProposal(api: Api, id: number) {
+    if (id > 1)
+      this.enqueue(`proposal ${id - 1}`, () => this.fetchProposal(api, id - 1));
+    // find existing
     const { proposals } = this.state;
     const exists = this.state.proposals.find((p) => p && p.id === id);
 
+    // check if complete
     if (
       exists &&
       exists.detail &&
@@ -346,25 +362,25 @@ class App extends React.Component<IProps, IState> {
       exists.executed
     )
       if (exists.votesByAccount && exists.votesByAccount.length) return;
-      else return this.fetchVotesPerProposal(api, exists);
+      else
+        return this.enqueue(`votes for proposal ${id}`, () =>
+          this.fetchProposalVotes(api, exists)
+        );
 
+    // fetch
     const proposal = await get.proposalDetail(api, id);
-    if (proposal.type !== "Text") {
-      const details = await api.query.proposalsCodex.proposalDetailsByProposalId(
-        id
-      );
-      proposal.detail = details.toJSON();
-    }
+    if (proposal.type !== "text")
+      proposal.detail = (
+        await api.query.proposalsCodex.proposalDetailsByProposalId(id)
+      ).toJSON();
     proposals[id] = proposal;
     this.save("proposals", proposals);
     this.enqueue(`votes for proposal ${id}`, () =>
-      this.fetchVotesPerProposal(api, proposal)
+      this.fetchProposalVotes(api, proposal)
     );
-    if (id > 1)
-      this.enqueue(`proposal ${id - 1}`, () => this.fetchProposal(api, id - 1));
   }
 
-  async fetchVotesPerProposal(api: Api, proposal: ProposalDetail) {
+  async fetchProposalVotes(api: Api, proposal: ProposalDetail) {
     const { votesByAccount } = proposal;
     if (votesByAccount && votesByAccount.length) return;
 
@@ -378,7 +394,7 @@ class App extends React.Component<IProps, IState> {
           const member = this.state.members.find(
             (m) => m.account === seat.member
           );
-          member && members.push(member);
+          if (member) members.push(member);
         })
       );
 
@@ -480,9 +496,9 @@ class App extends React.Component<IProps, IState> {
     );
     if (exists) return exists;
 
-    const id = await get.memberIdByAccount(api, account);
+    const id = Number(await get.memberIdByAccount(api, account));
     if (!id) return empty;
-    return await this.fetchMember(api, Number(id));
+    return await this.fetchMember(api, id);
   }
   async fetchMember(api: Api, id: number): Promise<Member> {
     const exists = this.state.members.find((m: Member) => m.id === id);
@@ -497,10 +513,11 @@ class App extends React.Component<IProps, IState> {
     const about = String(membership.about);
     const registeredAt = Number(membership.registered_at_block);
     const member: Member = { id, handle, account, registeredAt, about };
-    const members = this.state.members.concat(member);
+    const members = this.addOrReplace(this.state.members, member);
     this.save(`members`, members);
     this.updateHandles(members);
-    this.enqueue(`member ${id--}`, () => this.fetchMember(api, id--));
+    if (id > 1)
+      this.enqueue(`member ${id - 1}`, () => this.fetchMember(api, id - 1));
     return member;
   }
   updateHandles(members: Member[]) {
@@ -589,8 +606,9 @@ class App extends React.Component<IProps, IState> {
 
   async loadData() {
     const status = this.load("status");
-    if (status && status.version !== version) return this.clearData();
-    if (status) this.setState({ status });
+    if (status)
+      if (status.version !== version) return this.clearData();
+      else this.setState({ status });
     console.debug(`Loading data`);
     this.loadMembers();
     "councils categories channels proposals posts threads handles tokenomics reports validators nominators stakes stars"
@@ -625,6 +643,9 @@ class App extends React.Component<IProps, IState> {
     }
   }
 
+  toggleShowStatus() {
+    this.setState({ showStatus: !this.state.showStatus });
+  }
   toggleFooter() {
     this.setState({ hideFooter: !this.state.hideFooter });
   }
@@ -640,13 +661,19 @@ class App extends React.Component<IProps, IState> {
           {...this.state}
         />
 
+        <Modals toggleShowStatus={this.toggleShowStatus} {...this.state} />
+
         <Footer
           show={!hideFooter}
           toggleHide={this.toggleFooter}
           link={userLink}
         />
 
-        <Status connected={connected} fetching={fetching} />
+        <Status
+          toggleShowStatus={this.toggleShowStatus}
+          connected={connected}
+          fetching={fetching}
+        />
       </>
     );
   }
@@ -677,6 +704,7 @@ class App extends React.Component<IProps, IState> {
     this.load = this.load.bind(this);
     this.toggleStar = this.toggleStar.bind(this);
     this.toggleFooter = this.toggleFooter.bind(this);
+    this.toggleShowStatus = this.toggleShowStatus.bind(this);
   }
 }
 

+ 71 - 44
src/components/Calendar/index.tsx

@@ -8,11 +8,16 @@ import moment from "moment";
 import Back from "../Back";
 import Loading from "../Loading";
 
-import { CalendarItem, CalendarGroup, ProposalDetail } from "../../types";
+import {
+  CalendarItem,
+  CalendarGroup,
+  ProposalDetail,
+  Status,
+} from "../../types";
 
 interface IProps {
   proposals: ProposalDetail[];
-  status: { startTime: number };
+  status: Status;
   history: any;
 }
 interface IState {
@@ -25,14 +30,21 @@ class Calendar extends Component<IProps, IState> {
   constructor(props: IProps) {
     super(props);
     this.state = { items: [], groups: [], hide: [] };
+    this.filterItems = this.filterItems.bind(this);
     this.toggleShowProposalType = this.toggleShowProposalType.bind(this);
-    this.openProposal = this.openProposal.bind(this);
+    this.openItem = this.openItem.bind(this);
+  }
+  componentDidMount() {
+    this.filterItems();
+    setInterval(this.filterItems, 5000);
   }
 
   filterItems() {
-    const { status, proposals } = this.props;
+    const { status, posts, proposals, threads, handles } = this.props;
+    if (!status || !status.council) return [];
     const { hide } = this.state;
-    const { startTime, block } = status;
+    const { startTime, block, council } = status;
+
     let groups: CalendarGroup[] = [
       { id: 1, title: "RuntimeUpgrade" },
       { id: 2, title: "Council Round" },
@@ -48,52 +60,67 @@ class Calendar extends Component<IProps, IState> {
       return group.id;
     };
 
+    const getTime = (block: number) =>
+      moment(startTime + 6000 * block).valueOf();
+
+    // proposals
     proposals.forEach((p) => {
       if (!p) return;
-      const group = selectGroup(p.type);
+      const group = selectGroup(`Proposal:${p.type}`);
       if (hide[group]) return;
-      items.push({
-        id: p.id,
-        group: selectGroup(p.type),
-        title: `${p.id} ${p.title}`,
-        start_time: moment(startTime + p.createdAt * 6000).valueOf(),
-        end_time: p.finalizedAt
-          ? moment(startTime + p.finalizedAt * 6000).valueOf()
-          : moment().valueOf(),
-      });
+      const id = `proposal-${p.id}`;
+      const route = `/proposals/${p.id}`;
+      const title = `${p.id} ${p.title} by ${p.author}`;
+      const start_time = moment(startTime + p.createdAt * 6000).valueOf();
+      const end_time = p.finalizedAt
+        ? moment(startTime + p.finalizedAt * 6000).valueOf()
+        : moment().valueOf();
+      items.push({ id, route, group, title, start_time, end_time });
     });
 
-    const announcing = 28800;
-    const voting = 14400;
-    const revealing = 14400;
-    const termDuration = 144000;
-    const cycle = termDuration + announcing + voting + revealing;
-    this.setState({ groups, items });
+    // posts
+    posts
+      .filter((p) => p.createdAt.block > 1)
+      .forEach((p) => {
+        if (!p) return;
+        const group = selectGroup(`Posts`);
+        if (hide[group]) return;
+        const id = `post-${p.id}`;
+        const route = `/forum/threads/${p.threadId}`;
+        const thread = threads.find((t) => t.id === p.threadId) || {};
+        const handle = handles[p.authorId];
+        const title = `${p.id} in ${thread.title} by ${handle}`;
+        const start_time = getTime(p.createdAt.block);
+        const end_time = getTime(p.createdAt.block);
+        items.push({ id, route, group, title, start_time, end_time });
+      });
 
-    for (let round = 1; round * cycle < block.id + cycle; round++) {
+    // councils
+    const stage = council.durations;
+    const beforeTerm = stage[0] + stage[1] + stage[2];
+
+    for (let round = 1; round * stage[4] < block.id + stage[4]; round++) {
+      const title = `Round ${round}`;
+      const route = `/councils`;
       items.push({
-        id: items.length + 1,
+        id: `round-${round}`,
         group: 2,
-        title: `Round ${round}`,
-        start_time: moment(
-          startTime + 6000 * (57601 + (round - 1) * cycle)
-        ).valueOf(),
-        end_time: moment(
-          startTime + 6000 * (57601 + round * cycle - 1)
-        ).valueOf(),
+        route,
+        title,
+        start_time: getTime(beforeTerm + (round - 1) * stage[4]),
+        end_time: getTime(beforeTerm + round * stage[4] - 1),
       });
+      const startBlock = (round - 1) * stage[4];
       items.push({
-        id: items.length + 1,
+        id: `election-round-${round}`,
         group: 3,
-        title: `Election Round ${round}`,
-        start_time: moment(startTime + 6000 * ((round - 1) * cycle)).valueOf(),
-        end_time: moment(
-          startTime + 6000 * (57601 + (round - 1) * cycle)
-        ).valueOf(),
+        route,
+        title: `Election ${title}`,
+        start_time: getTime(startBlock),
+        end_time: getTime(beforeTerm + startBlock),
       });
     }
-    this.setState({ items });
-    return items;
+    this.setState({ groups, items });
   }
   toggleShowProposalType(id: number) {
     const { hide } = this.state;
@@ -101,17 +128,17 @@ 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}`);
+  openItem(id: number) {
+    const item = this.state.items.find((i) => i.id === id);
+    if (item) this.props.history.push(item.route);
   }
 
   render() {
     const { hide, groups } = this.state;
     const { history, status } = this.props;
 
-    if (!status.block) return <Loading />;
-    const items = this.state.items || this.filterItems();
+    const items = this.state.items;
+    if (!items.length) return <Loading target="items" />;
 
     const filters = (
       <div className="d-flex flew-row">
@@ -144,7 +171,7 @@ class Calendar extends Component<IProps, IState> {
             stackItems={true}
             defaultTimeStart={moment(status.startTime).add(-1, "day")}
             defaultTimeEnd={moment().add(15, "day")}
-            onItemSelect={this.openProposal}
+            onItemSelect={this.openItem}
           />
         </div>
       </>

+ 9 - 11
src/components/Council/ElectionStatus.tsx

@@ -15,18 +15,17 @@ const ElectionStage = (props: {
   domain: string;
 }) => {
   const { block, council, domain } = props;
-  if (!council) return <div>Loading..</div>;
+  if (!council) return <span>Loading..</span>;
   const { stage, termEndsAt } = council;
 
   if (!stage) {
-    if (!block || !termEndsAt) return <div />;
+    if (!block || !termEndsAt) return <span />;
     const left = timeLeft(termEndsAt - block);
-    return <div>election in {left}</div>;
+    return <span>election starts in {left}</span>;
   }
 
   let stageString = Object.keys(stage)[0];
   const left = timeLeft(stage[stageString] - block);
-
   if (stageString === "announcing")
     return <a href={`${domain}/#/council/applicants`}>{left} to apply</a>;
 
@@ -36,20 +35,19 @@ const ElectionStage = (props: {
   if (stageString === "revealing")
     return <a href={`${domain}/#/council/votes`}>{left} to reveal votes</a>;
 
-  return <div>{JSON.stringify(stage)}</div>;
+  return <span>{JSON.stringify(stage)}</span>;
 };
 
-const ElectionStatus = (props: { domain: string; status: Status }) => {
+const ElectionStatus = (props: { block; domain: string; status: Status }) => {
   const { domain, status } = props;
-  const { block } = status;
-  if (!block || !status.council) return <div />;
+  if (!status.block || !status.council) return <div />;
 
   return (
-    <div className="position-absolute text-left text-light">
+    <div className="text-center text-white">
       <ElectionStage
-        domain={domain}
+        block={status.block.id}
         council={status.council}
-        block={block.id}
+        domain={domain}
       />
     </div>
   );

+ 9 - 46
src/components/Council/index.tsx

@@ -21,21 +21,20 @@ const Council = (props: {
   validators: string[];
   handles: Handles;
   status: Status;
+  electionPeriods: number[];
 }) => {
-  const { councils, status, members, handles, posts, proposals } = props;
+  const { councils, handles, members, posts, proposals, status } = props;
   const council = councils[councils.length - 1];
   if (!council) return <Loading target="council" />;
-  const third = Math.floor(council.length / 3);
 
   return (
     <div className="box w-50 p-3 m-3">
-      <ElectionStatus domain={props.domain} status={status} />
-
       <h3>Council</h3>
-      <div className="d-flex flex-row  justify-content-between">
-        <div className="d-flex flex-column">
-          {council.slice(0, third).map((m) => (
-            <div key={m.member} className="box">
+      <div className="d-flex flex-wrap justify-content-between">
+        {council
+          .sort((a, b) => handles[a.member].localeCompare(handles[b.member]))
+          .map((m) => (
+            <div key={m.member} className="col-12 col-md-4">
               <MemberBox
                 id={m.id || 0}
                 account={m.member}
@@ -50,45 +49,9 @@ const Council = (props: {
               />
             </div>
           ))}
-        </div>
-        <div className="d-flex flex-column">
-          {council.slice(third, 2 * third).map((m) => (
-            <div key={m.member} className="box">
-              <MemberBox
-                id={m.id || 0}
-                account={m.member}
-                handle={m.handle || handles[m.member]}
-                members={members}
-                councils={councils}
-                proposals={proposals}
-                placement={"bottom"}
-                posts={posts}
-                startTime={status.startTime}
-                validators={props.validators}
-              />
-            </div>
-          ))}
-        </div>
-        <div className="d-flex flex-column">
-          {council.slice(2 * third).map((m) => (
-            <div key={m.member} className="box">
-              <MemberBox
-                key={m.member}
-                id={m.id || 0}
-                account={m.member}
-                handle={m.handle || handles[m.member]}
-                members={members}
-                councils={councils}
-                proposals={proposals}
-                placement={"bottom"}
-                posts={posts}
-                startTime={status.startTime}
-                validators={props.validators}
-              />
-            </div>
-          ))}
-        </div>
       </div>
+      <hr />
+      <ElectionStatus domain={props.domain} status={status} />
     </div>
   );
 };

+ 2 - 6
src/components/Councils/CouncilVotes.tsx

@@ -83,12 +83,8 @@ class CouncilVotes extends Component<IProps, IState> {
                       );
                       if (!vote) return <td />;
                       return (
-                        <td>
-                          <VoteButton
-                            key={member.handle}
-                            handle={member.handle}
-                            vote={vote.vote}
-                          />
+                        <td key={member.handle}>
+                          <VoteButton handle={member.handle} vote={vote.vote} />
                         </td>
                       );
                     })

+ 46 - 35
src/components/Councils/Leaderboard.tsx

@@ -20,9 +20,9 @@ const LeaderBoard = (props: {
   proposals: ProposalDetail[];
   members: Member[];
   councils: Seat[][];
-  cycle: number;
+  stages: number[];
 }) => {
-  const { cycle, members, proposals } = props;
+  const { members, proposals, stages } = props;
   const councils = props.councils.filter((c) => c);
   const summarizeVotes = (handle: string, propList: ProposalDetail[]) => {
     let votes = 0;
@@ -36,39 +36,50 @@ const LeaderBoard = (props: {
 
   let councilMembers: Member[] = [];
 
-  const councilVotes: CouncilVotes[] = councils.map(
-    (council, i: number): CouncilVotes => {
-      const start = 57601 + i * cycle;
-      const end = 57601 + (i + 1) * cycle;
-      const proposalsRound = proposals.filter(
-        (p) => p && p.createdAt > start && p.createdAt < end
-      );
-      const proposalCount = proposalsRound.length;
-
-      const members: CouncilMember[] = council.map(
-        (seat): CouncilMember => {
-          const member = props.members.find((m) => m.account === seat.member);
-          if (!member)
-            return { handle: ``, seat, votes: 0, proposalCount, percentage: 0 };
-
-          councilMembers.find((m) => m.id === member.id) ||
-            councilMembers.push(member);
-
-          let votes = summarizeVotes(member.handle, proposalsRound);
-          const percentage = Math.round((100 * votes) / proposalCount);
-          return {
-            handle: member.handle,
-            seat,
-            votes,
-            proposalCount,
-            percentage,
-          };
-        }
-      );
-
-      return { proposalCount, members };
-    }
-  );
+  const councilVotes: CouncilVotes[] = councils
+    .map(
+      (council, i: number): CouncilVotes => {
+        const start =
+          stages[0] + stages[1] + stages[2] + stages[3] + i * stages[4];
+        const end =
+          stages[0] + stages[1] + stages[2] + stages[3] + (i + 1) * stages[4];
+        const proposalsRound = proposals.filter(
+          (p) => p && p.createdAt > start && p.createdAt < end
+        );
+        const proposalCount = proposalsRound.length;
+        if (!proposalCount) return null;
+
+        const members: CouncilMember[] = council.map(
+          (seat): CouncilMember => {
+            const member = props.members.find((m) => m.account === seat.member);
+            if (!member)
+              return {
+                handle: ``,
+                seat,
+                votes: 0,
+                proposalCount,
+                percentage: 0,
+              };
+
+            councilMembers.find((m) => m.id === member.id) ||
+              councilMembers.push(member);
+
+            let votes = summarizeVotes(member.handle, proposalsRound);
+            const percentage = Math.round((100 * votes) / proposalCount);
+            return {
+              handle: member.handle,
+              seat,
+              votes,
+              proposalCount,
+              percentage,
+            };
+          }
+        );
+
+        return { proposalCount, members };
+      }
+    )
+    .filter((c) => c);
 
   councilMembers = councilMembers
     .map((m) => {

+ 20 - 20
src/components/Councils/index.tsx

@@ -3,14 +3,7 @@ import { Table } from "react-bootstrap";
 import LeaderBoard from "./Leaderboard";
 import CouncilVotes from "./CouncilVotes";
 import Back from "../Back";
-import { Member, ProposalDetail, Seat } from "../../types";
-
-// TODO fetch from chain
-const announcingPeriod = 28800;
-const votingPeriod = 14400;
-const revealingPeriod = 14400;
-const termDuration = 144000;
-const cycle = termDuration + announcingPeriod + votingPeriod + revealingPeriod; // 201600
+import { Member, ProposalDetail, Seat, Status } from "../../types";
 
 const Rounds = (props: {
   block: number;
@@ -18,8 +11,11 @@ const Rounds = (props: {
   councils: Seat[][];
   proposals: any;
   history: any;
+  status: Status;
 }) => {
-  const { block, councils, members, proposals } = props;
+  const { block, councils, members, proposals, status } = props;
+  if (!status.council) return <div />;
+  const stage = status.council.durations;
   return (
     <div className="w-100">
       <div className="position-fixed" style={{ right: "0", top: "0" }}>
@@ -32,26 +28,28 @@ const Rounds = (props: {
             <th>Announced</th>
             <th>Voted</th>
             <th>Revealed</th>
-            <th>Start</th>
-            <th>End</th>
+            <th>Term start</th>
+            <th>Term end</th>
           </tr>
         </thead>
         <tbody>
           {councils.map((council, i: number) => (
-            <tr key={i} className="">
+            <tr key={i + 1}>
               <td>{i + 1}</td>
-              <td>{1 + i * cycle}</td>
-              <td>{28801 + i * cycle}</td>
-              <td>{43201 + i * cycle}</td>
-              <td>{57601 + i * cycle}</td>
-              <td>{57601 + 201600 + i * cycle}</td>
+              <td>{1 + i * stage[4]}</td>
+              <td>{stage[0] + i * stage[4]}</td>
+              <td>{stage[0] + stage[1] + i * stage[4]}</td>
+              <td>{stage[0] + stage[2] + stage[3] + i * stage[4]}</td>
+              <td>
+                {stage[0] + stage[1] + stage[2] + stage[3] + +i * stage[4]}
+              </td>
             </tr>
           ))}
         </tbody>
       </Table>
 
       <LeaderBoard
-        cycle={cycle}
+        stages={stage}
         councils={councils}
         members={members}
         proposals={proposals}
@@ -71,8 +69,10 @@ const Rounds = (props: {
             proposals={props.proposals.filter(
               (p: ProposalDetail) =>
                 p &&
-                p.createdAt > 57601 + i * cycle &&
-                p.createdAt < 57601 + (i + 1) * cycle
+                p.createdAt >
+                  stage[0] + stage[1] + stage[2] + stage[3] + i * stage[4] &&
+                p.createdAt <
+                  stage[0] + stage[1] + stage[2] + stage[3] + (i + 1) * stage[4]
             )}
           />
         ))}

+ 19 - 2
src/components/Dashboard/Proposals.tsx

@@ -3,8 +3,24 @@ import { Link } from "react-router-dom";
 import ProposalsTable from "../Proposals/ProposalTable";
 import Loading from "../Loading";
 
-const Proposals = (props: { proposals; validators; councils; members }) => {
-  const { proposals, validators, councils, members, posts, startTime } = props;
+const Proposals = (props: {
+  proposals;
+  validators;
+  councils;
+  members;
+  posts;
+  startTime: number;
+  block: number;
+}) => {
+  const {
+    proposals,
+    validators,
+    councils,
+    members,
+    posts,
+    startTime,
+    block,
+  } = props;
 
   const pending = proposals.filter((p) => p && p.result === "Pending");
   if (!proposals.length) return <Loading target="proposals" />;
@@ -31,6 +47,7 @@ const Proposals = (props: { proposals; validators; councils; members }) => {
         </Link>
       </div>
       <ProposalsTable
+        block={block}
         hideNav={true}
         proposals={pending}
         proposalPosts={props.proposalPosts}

+ 17 - 4
src/components/Dashboard/Status.jsx

@@ -1,10 +1,23 @@
 import React from "react";
 
-const Status = (props: { connected: boolean, fetching: string }) => {
-  const { connected, fetching } = props;
-  if (!connected) return <div className="connecting">Connecting ..</div>;
+const Status = (props: {
+  connected: boolean,
+  fetching: string,
+  toggleShowStatus: () => void,
+}) => {
+  const { toggleShowStatus, connected, fetching } = props;
+  if (!connected)
+    return (
+      <div className="connecting" onClick={toggleShowStatus}>
+        Connecting ..
+      </div>
+    );
   if (!fetching.length) return <div />;
-  return <div className="connecting">Fetching {fetching}</div>;
+  return (
+    <div className="connecting" onClick={toggleShowStatus}>
+      Fetching {fetching}
+    </div>
+  );
 };
 
 export default Status;

+ 2 - 0
src/components/Dashboard/index.tsx

@@ -39,6 +39,7 @@ const Dashboard = (props: IProps) => {
           <Link to={`/timeline`}>Timeline</Link>
           <Link to={`/tokenomics`}>Reports</Link>
           <Link to={`/validators`}>Validators</Link>
+          <Link to={`/spending`}>Spending</Link>
           <Link to="/mint">Toolbox</Link>
         </div>
 
@@ -58,6 +59,7 @@ const Dashboard = (props: IProps) => {
         />
 
         <Proposals
+          block={status.block ? status.block.id : 0}
           members={members}
           councils={councils}
           posts={posts}

+ 2 - 2
src/components/Forum/LatestPost.tsx

@@ -16,7 +16,7 @@ const LatestPost = (props: {
   startTime: number;
 }) => {
   const { selectThread, handles = [], thread, post, startTime } = props;
-  const { authorId, createdAt, id, threadId, text, handle } = post;
+  const { authorId, createdAt, id, threadId, text } = post;
 
   return (
     <div
@@ -32,7 +32,7 @@ const LatestPost = (props: {
         <a href={`${domain}/#/forum/threads/${threadId}`}>reply</a>
       </div>
 
-      <div>
+      <div className="overflow-hidden">
         <div className="text-left mb-3 font-weight-bold">
           <Link to={`/forum/threads/${threadId}`}>
             {thread ? thread.title : threadId}

+ 0 - 29
src/components/Forum/Loading.tsx

@@ -1,29 +0,0 @@
-import React from "react";
-import { Button } from "react-bootstrap";
-import { Category, Thread, Post } from "../../types";
-
-const Loading = (props: {
-  getMinimal: (array: { id: number }[]) => any;
-  categories: Category[];
-  threads: Thread[];
-  posts: Post[];
-}) => {
-  const { getMinimal, categories, threads, posts } = props;
-  const categoryId = getMinimal(categories);
-  const threadId = getMinimal(threads);
-  const postId = getMinimal(posts);
-
-  const strCategories = categoryId ? `${categoryId} categories, ` : "";
-  const strThreads = threadId ? `${threadId} threads, ` : "";
-  const strPosts = postId ? `${postId} posts ` : "";
-
-  if (`${strCategories}${strThreads}${strPosts}` === "") return <div />;
-
-  return (
-    <Button variant="secondary" className="btn-sm ml-auto">
-      Fetching {strCategories} {strThreads} {strPosts}
-    </Button>
-  );
-};
-
-export default Loading;

+ 25 - 0
src/components/Forum/Missing.tsx

@@ -0,0 +1,25 @@
+import React from "react";
+import { Loading } from "..";
+import { Category, Thread, Post } from "../../types";
+
+const Missing = (props: {
+  getMinimal: (array: { id: number }[]) => any;
+  categories: Category[];
+  threads: Thread[];
+  posts: Post[];
+}) => {
+  const { getMinimal } = props;
+  const categories = getMinimal("categories");
+  const threads = getMinimal("threads");
+  const posts = getMinimal("posts");
+
+  const strCategories = categories > 0 ? `${categories} categories, ` : "";
+  const strThreads = threads > 0 ? `${threads} threads, ` : "";
+  const strPosts = posts > 0 ? `${posts} posts ` : "";
+  const summary = `${strCategories} ${strThreads} ${strPosts}`;
+  if (summary.length === 2) return <div />;
+
+  return <Loading target={summary} />
+};
+
+export default Missing;

+ 8 - 7
src/components/Forum/NavBar.tsx

@@ -5,7 +5,7 @@ import { Link } from "react-router-dom";
 import { ChevronRight } from "react-feather";
 import { Category, Post, Thread } from "../../types";
 
-import Loading from "./Loading";
+import Missing from "./Missing";
 
 const CategoryNav = (props: {
   selectThread: (id: number) => void;
@@ -71,13 +71,14 @@ const NavBar = (props: {
       <CategoryNav selectThread={selectThread} category={category} />
       <ThreadNav thread={thread} />
 
-      <Loading
-        getMinimal={getMinimal}
-        categories={categories}
-        threads={threads}
-        posts={posts}
-      />
       <Nav className="ml-auto">
+        <Missing
+          getMinimal={getMinimal}
+          categories={categories}
+          threads={threads}
+          posts={posts}
+        />
+
         <Form>
           <input
             type="search"

+ 7 - 9
src/components/Forum/PostBox.tsx

@@ -15,20 +15,18 @@ const PostBox = (props: {
   text: string;
   threadId: number;
 }) => {
-  const { createdAt, startTime, id, authorId, handles, threadId } = props;
+  const { createdAt, startTime, id, authorId, handles, threadId, text } = props;
   const created = moment(startTime + createdAt.block * 6000).fromNow();
-  const text = props.text
-    .split("\n")
-    .map((line) => line.replace(/>/g, "&gt;"))
-    .join("\n\n");
 
   return (
     <div className="box" key={id}>
-      <div className="float-right">
-        <a href={`${domain}/#/forum/threads/${threadId}`}>reply</a>
+      <div>
+        <div className="float-right">
+          <a href={`${domain}/#/forum/threads/${threadId}`}>reply</a>
+        </div>
+        <div className="float-left">{created}</div>
+        <User key={authorId} id={authorId} handle={handles[authorId]} />
       </div>
-      <div className="float-left">{created}</div>
-      <User key={authorId} id={authorId} handle={handles[authorId]} />
       <Markdown
         plugins={[gfm]}
         className="mt-1 overflow-auto text-left"

+ 6 - 10
src/components/Forum/index.tsx

@@ -23,6 +23,7 @@ class Forum extends React.Component<IProps, IState> {
   constructor(props: IProps) {
     super(props);
     this.state = { categoryId: 0, threadId: 0, postId: 0, searchTerm: "" };
+    this.getMissing = this.getMissing.bind(this);
     this.selectCategory = this.selectCategory.bind(this);
     this.selectThread = this.selectThread.bind(this);
     this.handleKeyDown = this.handleKeyDown.bind(this);
@@ -56,15 +57,6 @@ class Forum extends React.Component<IProps, IState> {
     this.setState({ threadId });
   }
 
-  getMinimal(array: { id: number }[]) {
-    if (!array.length) return " ";
-    let id = array[0].id;
-    array.forEach((p) => {
-      if (p.id < id) id = p.id;
-    });
-    if (id > 1) return id;
-  }
-
   filterPosts(posts: Post[], s: string) {
     return s === ""
       ? posts
@@ -96,6 +88,10 @@ class Forum extends React.Component<IProps, IState> {
     return { posts, threads, categories };
   }
 
+  getMissing(target: string) {
+    return this.props.status[target] - this.props[target].length;
+  }
+
   render() {
     const { handles, categories, posts, threads, status } = this.props;
     const { categoryId, threadId, searchTerm } = this.state;
@@ -108,7 +104,7 @@ class Forum extends React.Component<IProps, IState> {
         <NavBar
           selectCategory={this.selectCategory}
           selectThread={this.selectThread}
-          getMinimal={this.getMinimal}
+          getMinimal={this.getMissing}
           categories={categories}
           category={category}
           threads={threads}

+ 1 - 1
src/components/Loading.tsx

@@ -5,7 +5,7 @@ const Loading = (props: { target?: string }) => {
   const { target } = props;
   const title = target ? `Fetching ${target}` : "Connecting to Websocket";
   return (
-    <Button variant="warning" className="m-1">
+    <Button variant="warning" className="m-1 py-0 mr-2">
       <Spinner animation="border" variant="dark" size="sm" className="mr-1" />
       {title}
     </Button>

+ 3 - 3
src/components/Members/MemberPosts.tsx

@@ -2,14 +2,14 @@ import React from "react";
 import { Link } from "react-router-dom";
 import Markdown from "react-markdown";
 import gfm from "remark-gfm";
-import moment from "moment"
+import moment from "moment";
 
 const Posts = (props: {
   posts: Post[];
   threadTitle: (id: number) => string;
   startTime: number;
 }) => {
-  const { posts, startTime, threadTitle } = props;
+  const { domain, posts, startTime, threadTitle } = props;
 
   if (!posts.length) return <div />;
 
@@ -47,4 +47,4 @@ const Posts = (props: {
   );
 };
 
-export default Posts
+export default Posts;

+ 18 - 10
src/components/Mint/ValidatorRewards.tsx

@@ -1,5 +1,6 @@
 import React from "react";
 import { Table } from "react-bootstrap";
+import { Loading } from "..";
 
 const ValidatorRewards = (props: {
   handleChange: (e: any) => void;
@@ -8,22 +9,29 @@ const ValidatorRewards = (props: {
   price: number;
 }) => {
   const { validators, payout, price } = props;
+  if (!payout) return <Loading target="validator reward" />;
+
+  let counts = [45, 100, 200, 300, 500];
+  if (!counts.includes(validators)) counts.unshift(validators);
+
   return (
     <div>
       <h2 className="mt-5 text-center">Validator Rewards</h2>
-      <label>Payment / h</label>
-      <input
-        className="form-control col-4"
-        onChange={props.handleChange}
-        name="payout"
-        type="number"
-        value={payout}
-      />
+      <div className="form-group">
+        <label>Payment / h</label>
+        <input
+          className="form-control"
+          onChange={props.handleChange}
+          name="payout"
+          type="number"
+          value={payout}
+        />
+      </div>
       <h3 className="mt-3">Reward for increasing Validator counts</h3>
       <Table className="bg-light">
         <Thead />
         <tbody>
-          {[validators, 45, 100, 200, 300, 500].map((count) => (
+          {counts.map((count) => (
             <ValidatorRow
               key={`vcount-${count}`}
               price={price}
@@ -37,7 +45,7 @@ const ValidatorRewards = (props: {
       <Table className="bg-light">
         <Thead />
         <tbody>
-          {[validators, 45, 100, 200, 300, 500].map((count) => (
+          {counts.map((count) => (
             <ValidatorRow
               key={`vcount-${count}`}
               price={price}

+ 30 - 48
src/components/Mint/index.tsx

@@ -35,21 +35,12 @@ class Mint extends React.Component<IProps, IState> {
   constructor(props: IProps) {
     super(props);
 
-    this.state = {
-      start,
-      end,
-      role: "",
-      salary: 0,
-      payout: 0,
-    };
+    this.state = { start, end, role: "", salary: 0 };
     this.setRole = this.setRole.bind(this);
     this.handleChange = this.handleChange.bind(this);
   }
 
   componentDidMount() {
-    const payout = this.props.status.lastReward;
-    console.log(`payout`, payout, this.props.status);
-    this.setState({ payout });
     this.setRole({ target: { value: "consul" } });
   }
 
@@ -66,11 +57,10 @@ class Mint extends React.Component<IProps, IState> {
   }
 
   render() {
-    const { tokenomics, validators } = this.props;
+    const { tokenomics, validators, payout } = this.props;
     if (!tokenomics) return <Loading />;
 
-    const { role, start, salary, end, payout } = this.state;
-
+    const { role, start, salary, end } = this.state;
     const { price } = tokenomics;
     const rate = Math.floor(+price * 100000000) / 100;
     const blocks = end - start;
@@ -78,30 +68,30 @@ class Mint extends React.Component<IProps, IState> {
     return (
       <div className="p-3 text-light">
         <h2>Mint</h2>
-        <div className="d-flex flex-row form-group">
-          <label className="col-2">Token value</label>
+        <div className="form-group">
+          <label>Token value</label>
           <input
-            className="form-control col-4"
+            className="form-control"
             disabled={true}
             name="rate"
             type="text"
             value={`${rate} $ / 1 M JOY`}
           />
         </div>
-        <div className="d-flex flex-row form-group">
-          <label className="col-2">Start block</label>
+        <div className="form-group">
+          <label>Start block</label>
           <input
-            className="form-control col-4"
+            className="form-control"
             onChange={this.handleChange}
             name="start"
             type="number"
             value={start}
           />
         </div>
-        <div className="d-flex flex-row form-group">
-          <label className="col-2">End block</label>
+        <div className="form-group">
+          <label>End block</label>
           <input
-            className="form-control col-4"
+            className="form-control"
             onChange={this.handleChange}
             name="end"
             type="number"
@@ -109,66 +99,58 @@ class Mint extends React.Component<IProps, IState> {
           />
         </div>
 
-        <div className="d-flex flex-row form-group">
-          <label className="col-2">Blocks</label>
+        <div className="form-group">
+          <label>Blocks</label>
           <input
-            className="form-control col-4"
+            className="form-control"
             disabled={true}
             name="blocks"
             type="number"
             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.setRole}
-          >
+        <div className="form-group">
+          <label>Role</label>
+          <select name="role" className="form-control" onChange={this.setRole}>
             {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 / {payoutInterval} blocks)
-          </label>
+        <div className="form-group">
+          <label>Reward (JOY / {payoutInterval} blocks)</label>
           <input
-            className="form-control col-4"
+            className="form-control"
             name="baseReward"
             type="number"
             onChange={this.handleChange}
             value={salary}
           />
         </div>
-        <div className="d-flex flex-row form-group">
-          <label className="col-2">
-            Reward (USD / {payoutInterval} blocks)
-          </label>
+        <div className="form-group">
+          <label>Reward (USD / {payoutInterval} blocks)</label>
           <input
-            className="form-control col-4"
+            className="form-control"
             disabled={true}
             name="baseRewardUSD"
             type="number"
             value={price * salary}
           />
         </div>
-        <div className="d-flex flex-row form-group">
-          <label className="col-2">Reward (JOY) / {blocks} blocks</label>
+        <div className="form-group">
+          <label>Reward (JOY) / {blocks} blocks</label>
           <input
-            className="form-control col-4"
+            className="form-control"
             disabled={true}
             name="reward"
             type="number"
             value={(blocks / payoutInterval) * salary}
           />
         </div>
-        <div className="d-flex flex-row form-group">
-          <label className="col-2">Reward (USD) / {blocks} blocks</label>
+        <div className="form-group">
+          <label>Reward (USD) / {blocks} blocks</label>
           <input
-            className="form-control col-4"
+            className="form-control"
             disabled={true}
             name="joy"
             type="number"

+ 16 - 13
src/components/Proposals/Bar.tsx

@@ -1,34 +1,37 @@
 import React from "react";
-import { OverlayTrigger, Tooltip,  } from "react-bootstrap";
+import { OverlayTrigger, Tooltip } from "react-bootstrap";
 
 const Bar = (props: {
   id: number;
-  blocks: number ;
+  blocks: number;
   duration: string;
   period: number;
 }) => {
-  const { blocks, duration, id, period } = props;
-  const percent = 100 * (blocks / period) 
-  if (percent <0) return <div>updating ..</div>
-  
+  const { blocks, created, duration, id, period } = props;
+  const percent = 100 - 100 * (blocks / period);
+  if (percent < 0) return <div>updating ..</div>;
+  const bg = percent < 25 ? `danger` : percent < 50 ? `warning` : `success`;
+
   return (
     <OverlayTrigger
       key={id}
-      placement="right"
+      placement="bottom"
       overlay={
         <Tooltip id={String(id)}>
-          {Math.floor(percent)}% of {period} blocks
-          <br />
-          {duration}
+          <div>created: {created}</div>
+          <div>age: {duration}</div>
+          <div>
+            {Math.floor(percent)}% of {period} blocks left
+          </div>
         </Tooltip>
       }
     >
       <div
-        className="bg-dark mr-2"
-        style={{ height: `30px`, width: `${percent}%` }}
+        className={`bg-${bg} mr-2`}
+        style={{ height: `5px`, width: `${percent}%` }}
       ></div>
     </OverlayTrigger>
   );
 };
 
-export default Bar
+export default Bar;

+ 4 - 6
src/components/Proposals/Detail.tsx

@@ -12,17 +12,15 @@ const Detail = (props: { detail?: any; type: string }) => {
 
   if (type === "spending")
     return (
-      <>
-        <b>Spending</b>
-        <p title={`to ${data[1]}`}>{amount(data[0])} M tJOY</p>
-      </>
+      <p title={`to ${data[1]}`}>
+        <b>Spending</b>: {amount(data[0])} M tJOY
+      </p>
     );
 
   if (type === "setWorkingGroupMintCapacity")
     return (
       <p>
-        Fill {data[1]} working group mint
-        <br />({amount(data[0])} M tJOY)
+        Fill {data[1]} working group mint: ({amount(data[0])} M tJOY)
       </p>
     );
 

+ 5 - 9
src/components/Proposals/ProposalTable.tsx

@@ -92,14 +92,8 @@ class ProposalTable extends React.Component<IProps, IState> {
   }
 
   render() {
-    const {
-      hideNav,
-      block,
-      councils,
-      members,
-      posts,
-      proposalPosts,
-    } = this.props;
+    const { hideNav, block, councils, members, posts } = this.props;
+
     const { page, author, selectedTypes } = this.state;
 
     // proposal types
@@ -164,7 +158,9 @@ class ProposalTable extends React.Component<IProps, IState> {
               block={block}
               members={members}
               startTime={this.props.startTime}
-              posts={proposalPosts.filter((post) => post.threadId === p.id)}
+              posts={this.props.proposalPosts.filter(
+                (post) => post.threadId === p.id
+              )}
               councils={councils}
               forumPosts={posts}
               proposals={this.props.proposals}

+ 57 - 54
src/components/Proposals/Row.tsx

@@ -73,7 +73,7 @@ const ProposalRow = (props: {
 
   const url = `https://pioneer.joystreamstats.live/#/proposals/${id}`;
   let result: string = props.result ? props.result : props.stage;
-  if (executed) result = Object.keys(props.executed)[0]
+  if (executed) result = Object.keys(props.executed)[0];
   const color = colors[result];
 
   const created = formatTime(props.startTime + createdAt * 6000);
@@ -90,72 +90,75 @@ const ProposalRow = (props: {
   const duration = blocks ? `${daysStr} ${hoursStr} / ${blocks} blocks` : "";
 
   return (
-    <div className="d-flex flex-row justify-content-between text-left px-2 mt-3">
-      <div className="col-3">
+    <div className="d-flex flex-column">
+      <div className="d-flex flex-wrap justify-content-left text-left mt-3">
         <OverlayTrigger
-          key={id}
           placement="right"
-          overlay={<Tooltip id={String(id)}>{description}</Tooltip>}
-        >
-          <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 id={`votes-${id}`}>
+              <VotesTooltip
+                votesByAccount={props.votesByAccount}
+                votes={votes}
               />
             </Tooltip>
           }
         >
-          <div>
-            by
-            <Link to={`/members/${author}`}> {author}</Link>
+          <div
+            className={`col-3 col-md-1 text-center p-2 border border-${color}`}
+          >
+            <b>{result}</b>
+            <div className="d-flex flex-row justify-content-center">
+              <VotesBubbles votes={votes} />
+            </div>
           </div>
         </OverlayTrigger>
-      </div>
-      
-      <div className="col-2 text-left">
-        <Detail detail={detail} type={type} />
-      </div>
 
-      <OverlayTrigger
-        placement="left"
-        overlay={
-          <Tooltip id={`votes-${id}`}>
-            <VotesTooltip votesByAccount={props.votesByAccount} votes={votes} />
-          </Tooltip>
-        }
-      >
-        <div className={`col-1 p-2 border border-${color}`}>
-          <b>{result}</b>
-          <div className="d-flex flex-row">
-            <VotesBubbles votes={votes} />
-          </div>
-        </div>
-      </OverlayTrigger>
+        <div className="col-9 col-md-3 text-left">
+          <OverlayTrigger
+            placement={"bottom"}
+            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>
+              <Link to={`/members/${author}`}> {author}</Link>
+            </div>
+          </OverlayTrigger>
 
-      <div className="col-2 text-left">
-        <Bar id={id} blocks={blocks} period={period} duration={duration} />
-      </div>
+          <OverlayTrigger
+            key={id}
+            placement="bottom"
+            overlay={<Tooltip id={String(id)}>{description}</Tooltip>}
+          >
+            <b>
+              <Link to={`/proposals/${id}`}>{title}</Link>
+              <Posts posts={props.posts} />
+            </b>
+          </OverlayTrigger>
+          <Detail detail={detail} type={type} />
+        </div>
 
-      <div className="col-1">{created}</div>
-      <div className="col-1">
-        {finalized ? finalized : <VoteNowButton show={true} url={url} />}
+        <div className="d-none d-md-block ml-auto">
+          {finalized ? finalized : <VoteNowButton show={true} url={url} />}
+        </div>
       </div>
+      <Bar
+        id={id}
+        blocks={blocks}
+        created={created}
+        period={period}
+        duration={duration}
+      />
     </div>
   );
 };

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

@@ -11,7 +11,7 @@ const getRound = (block: number): number =>
 const executionFailed = (result: string, executed: any) => {
   if (result !== "Approved") return result;
   if (!executed || !Object.keys(executed)) return;
-  if (executed.Approved.ExecutionFailed)
+  if (executed.Approved && executed.Approved.ExecutionFailed)
     return executed.Approved.ExecutionFailed.error;
   return false;
 };
@@ -69,7 +69,7 @@ const ProposalLine = (props: any) => {
       <span
         className={`bg-${failed ? "danger" : "warning"} text-body p-1 mr-2`}
       >
-        {detail ? amount(detail.Spending[0]) : `?`} M
+        {detail ? amount(detail.spending[0]) : `?`} M
       </span>
       <Link to={`/proposals/${id}`}>{title}</Link> (
       <Link to={`/members/${author}`}>{author}</Link>)

+ 7 - 1
src/components/Timeline/Item.tsx

@@ -2,6 +2,8 @@ import React from "react";
 import { Link } from "react-router-dom";
 import moment from "moment";
 import { Event } from "../../types";
+import Markdown from "react-markdown";
+import gfm from "remark-gfm";
 
 const TimelineItem = (props: { event: Event; startTime: number }) => {
   const { category, date, text, link } = props.event;
@@ -20,7 +22,11 @@ const TimelineItem = (props: { event: Event; startTime: number }) => {
           {category.tag}
         </span>
         <time>{created}</time>
-        <p>{text}</p>
+        <Markdown
+          plugins={[gfm]}
+          className="mt-1 overflow-auto text-left"
+          children={text}
+        />
 
         <Link to={link.url}>{link.text}</Link>
 

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

@@ -9,14 +9,14 @@ const Timeline = (props: {
   proposals: ProposalDetail[];
   status: { startTime: number };
 }) => {
-  const { posts, proposals, status } = props;
+  const { handles, posts, proposals, status } = props;
   let events: Event[] = [];
 
   proposals.forEach(
     (p) =>
       p &&
       events.push({
-        text: p.title,
+        text: p.description,
         date: p.createdAt,
         category: {
           tag: "Proposal",
@@ -24,7 +24,7 @@ const Timeline = (props: {
         },
         link: {
           url: `/proposals/${p.id}`,
-          text: `Proposal ${p.id}`,
+          text: `${p.title} by ${p.author}`,
         },
       })
   );
@@ -33,15 +33,12 @@ const Timeline = (props: {
     (p) =>
       p &&
       events.push({
-        text: p.text.slice(0, 100),
+        text: p.text,
         date: p.createdAt.block,
-        category: {
-          tag: "Forum Post",
-          color: `blue`,
-        },
+        category: { tag: "Forum Post", color: `blue` },
         link: {
           url: `/forum/threads/${p.threadId}`,
-          text: `Post ${p.id}`,
+          text: `Post ${p.id} by ${handles[p.authorId]}`,
         },
       })
   );

+ 1 - 1
src/components/Validators/MinMax.tsx

@@ -37,7 +37,7 @@ const MinMax = (props: {
   const validatorReward = reward ? reward / validators.length : 0;
 
   return (
-    <Table className="bg-secondary w-50">
+    <Table className="bg-secondary">
       <tbody>
         <tr>
           <td {...name}>Validators</td>

+ 4 - 2
src/components/Votes.tsx

@@ -35,7 +35,7 @@ export const VoteNowButton = (props: { show: boolean; url: string }) => {
 
   return (
     <Button variant="danger">
-      <a href={url}>Vote!</a>
+      <a href={url}>Vote</a>
     </Button>
   );
 };
@@ -49,6 +49,7 @@ const VoteBubble = (props: {
 }) => {
   const { count, detailed, vote } = props;
   if (!count) return <span />;
+
   return (
     <Button className="btn-sm m-0" variant={voteStyles[voteKeys[vote]]}>
       {count} {detailed && vote}
@@ -62,6 +63,7 @@ export const VotesBubbles = (props: {
 }) => {
   const { detailed } = props;
   const votes = JSON.parse(JSON.stringify(props.votes)); // TODO
+
   return (
     <div>
       {Object.keys(votes).map((vote: string) => (
@@ -98,7 +100,7 @@ export const VotesTooltip = (props: any) => {
   const votes = votesByAccount.filter((v: Vote) =>
     v.vote === `` ? false : true
   );
-  if (!votes.length) return <div>No votes</div>;
+  if (!votes.length) return <div>No votes were cast yet.</div>;
 
   return (
     <div className="text-left text-light">

+ 1 - 0
src/components/index.ts

@@ -20,6 +20,7 @@ export { default as Validators } from "./Validators";
 export { default as Timeline } from "./Timeline";
 export { default as TableFromObject } from "./TableFromObject";
 
+export { default as Modals } from "./Modals";
 export { default as Footer } from "./Dashboard/Footer";
 export { default as Status } from "./Dashboard/Status";
 

+ 1 - 1
src/index.css

@@ -189,7 +189,7 @@ table td {
 .timeline-item-content p {
   font-size: 0.8.em;
   margin: 15px 0;
-  max-width: 250px;
+  max-width: 350px;
 }
 
 .timeline-item-content a {

+ 7 - 3
src/types.ts

@@ -23,6 +23,7 @@ export interface Status {
   connecting: boolean;
   loading: string;
   council?: { stage: any; round: number; termEndsAt: number };
+  durations: number[];
   issued: number;
   price: number;
   proposals: number;
@@ -35,14 +36,16 @@ export interface Status {
 }
 
 export interface IState {
-  //gethandle: (account: AccountId | string)  => string;
+  connecting: boolean;
+  loading: string;
+  processingTasks: number;
+  fetching: string;
+  queue: { key: string; action: any }[];
   status: Status;
   blocks: Block[];
   nominators: string[];
   validators: string[];
   stashes: string[];
-  queue: { key: string; action: any }[];
-  loading: string;
   councils: Seat[][];
   channels: Channel[];
   categories: Category[];
@@ -60,6 +63,7 @@ export interface IState {
   stakes?: { [key: string]: Stakes };
   rewardPoints?: RewardPoints;
   hideFooter: boolean;
+  showStatus: boolean;
 }
 
 export interface RewardPoints {