Explorar el Código

v4: major overhaul

Joystream Stats hace 3 años
padre
commit
19ad02a066

+ 241 - 264
src/App.tsx

@@ -1,10 +1,11 @@
 import React from "react";
 import "bootstrap/dist/css/bootstrap.min.css";
 import "./index.css";
-import { Routes, Loading } from "./components";
+import { Routes, Loading, Footer, Status } from "./components";
+
 import * as get from "./lib/getters";
 import { domain, wsLocation } from "./config";
-import proposalPosts from "./proposalPosts";
+//import proposalPosts from "./proposalPosts";
 import axios from "axios";
 import { ProposalDetail } from "./types";
 //import socket from "./socket";
@@ -19,7 +20,7 @@ import {
   Post,
   Seat,
   Thread,
-  Status,
+  //  Status,
 } from "./types";
 import { types } from "@joystream/types";
 import { ApiPromise, WsProvider } from "@polkadot/api";
@@ -28,9 +29,13 @@ import { VoteKind } from "@joystream/types/proposals";
 
 interface IProps {}
 
-const version = 0.4;
+const version = 5;
+const userLink = `${domain}/#/members/joystreamstats`;
 
 const initialState = {
+  blocksPerCycle: 201600, // TODO calculate
+  connected: false,
+  fetching: "",
   queue: [],
   blocks: [],
   nominators: [],
@@ -44,13 +49,13 @@ const initialState = {
   domain,
   handles: {},
   members: [],
-  proposalPosts,
+  proposalPosts: [],
   reports: {},
   stakes: {},
   stashes: [],
   stars: {},
   hideFooter: false,
-  status: { connecting: true, loading: "" },
+  status: { era: 0, block: { id: 0, era: 0, timestamp: 0, duration: 6 } },
 };
 
 class App extends React.Component<IProps, IState> {
@@ -67,119 +72,102 @@ class App extends React.Component<IProps, IState> {
   }
 
   async handleApi(api: Api) {
-    let blockHash = await api.rpc.chain.getBlockHash(1);
-    let startTime = (await api.query.timestamp.now.at(blockHash)).toNumber();
-    let stage: any = await api.query.councilElection.stage();
-    let termEndsAt = Number((await api.query.council.termEndsAt()).toJSON());
-    let round: number = Number(
-      (await api.query.councilElection.round()).toJSON()
-    );
-
-    let status: Status = {
-      block: { id: 0, era: 0, timestamp: 0, duration: 6 },
-      council: { termEndsAt, stage: stage.toJSON(), round },
-      category: await get.currentCategoryId(api),
-      channel: await get.currentChannelId(api),
-      post: await get.currentPostId(api),
-      thread: await get.currentThreadId(api),
-      member: await api.query.members.nextMemberId(),
-      proposals: 0,
-      startTime,
-    };
-    this.save("status", status);
-
     api.rpc.chain.subscribeNewHeads((header: Header) =>
       this.handleBlock(api, header)
     );
-    //this.enqueue("members", () =>      this.fetchMembers(api, Number(status.member)) );
-    //    this.enqueue("councils", () => this.fetchCouncils(api, round));
-    //    this.enqueue("proposals", () => this.fetchProposals(api));
-    //    this.enqueue("validators", () => this.fetchValidators(api));
-    //    this.enqueue("nominators", () => this.fetchNominators(api));
-    //    this.enqueue("categories", () => this.fetchCategories(api));
-    //    this.enqueue("threads", () => this.fetchThreads(api, status.thread));
-    //    this.enqueue("posts", () => this.fetchPosts(api, status.post));
-    //this.enqueue("channels", () => this.fetchChannels(api, status.channel));
+    this.updateStatus(api);
+
+    let { status } = this.state;
+    let blockHash = await api.rpc.chain.getBlockHash(1);
+    status.startTime = (await api.query.timestamp.now.at(blockHash)).toNumber();
+    this.save("status", status);
   }
 
   async handleBlock(api, header: Header) {
     let { blocks, status } = this.state;
-
-    // current block
     const id = header.number.toNumber();
     if (blocks.find((b) => b.id === id)) return;
     const timestamp = (await api.query.timestamp.now()).toNumber();
     const duration = status.block ? timestamp - status.block.timestamp : 6000;
+
     status.block = { id, timestamp, duration };
+    this.save("status", status);
+
     blocks = blocks.concat(status.block);
     this.setState({ blocks });
 
-    // validators
-    const era = Number(await api.query.staking.currentEra());
-    this.fetchEraRewardPoints(api, era);
-    if (era > status.era) {
-      status.era = era;
-      this.fetchStakes(api, era, this.state.validators);
-      this.fetchLastReward(api, era - 1);
-    } else if (!status.lastReward) this.fetchLastReward(api, era);
-
-    if (id / 10 === Math.floor(id / 10)) {
-      console.debug(`Updating cache`); // every minute (10 blocks)
-      status.loading = "data";
-      this.updateCouncil(api, id);
-      status.proposals = await this.fetchProposals(api, status.proposals);
-      status.posts = await this.fetchPosts(api, status.posts);
-      status.channels = await this.fetchChannels(api, status.currentChannel);
-      status.categories = await this.fetchCategories(api);
-      status.threads = await this.fetchThreads(api, status.threads);
-      status.proposalPosts = await api.query.proposalsDiscussion.postCount();
+    if (id / 50 === Math.floor(id / 50)) {
+      this.updateStatus(api, id);
+      this.fetchTokenomics();
     }
-
-    this.save("status", status);
-    this.nextTask();
   }
 
-  async updateCouncil(api, id) {
+  async updateStatus(api: Api, id = 0) {
+    console.debug(`Updating status for block ${id}`);
+
     let { status } = this.state;
-    if (!status.council) return;
-    if (id < status.council.termEndsAt || id < status.council.stageEndsAt)
-      return;
-    const round = Number((await api.query.councilElection.round()).toJSON());
-    const stage = await api.query.councilElection.stage();
-    const json = stage.toJSON();
-    const key = Object.keys(json)[0];
-    const stageEndsAt = json[key];
-    const termEndsAt = Number((await api.query.council.termEndsAt()).toJSON());
-    status.council = { round, stageEndsAt, termEndsAt, stage: stage.toJSON() };
+    status.era = await this.updateEra(api);
+    status.council = await this.updateCouncil(api);
+    await this.fetchCouncils(api);
+
+    const nextMemberId = await await api.query.members.nextMemberId();
+    status.members = nextMemberId - 1;
+    status.proposals = await get.proposalCount(api);
+    status.posts = await get.currentPostId(api);
+    status.threads = await get.currentThreadId(api);
+    status.categories = await get.currentCategoryId(api);
+    status.channels = await get.currentChannelId(api);
+    status.proposalPosts = await api.query.proposalsDiscussion.postCount();
     this.save("status", status);
+
+    this.fetchProposal(api, status.proposals);
+    this.fetchPost(api, status.posts);
+    this.fetchThread(api, status.threads);
+    this.fetchCategory(api, status.categories);
+    this.fetchMember(api, status.members);
+    this.fetchChannel(api, status.channels);
+  }
+
+  async updateEra(api: Api) {
+    const era = Number(await api.query.staking.currentEra());
+    this.fetchEraRewardPoints(api, era);
+
+    const { status } = this.state;
+    if (era > status.era) {
+      console.debug(`Updating validators`);
+      this.fetchLastReward(api, era - 1);
+      const validators = await this.fetchValidators(api);
+      this.enqueue("stakes", () => this.fetchStakes(api, era, validators));
+    } else if (!status.lastReward) this.fetchLastReward(api, era - 1);
+    return era;
   }
 
+  // queue management
   enqueue(key: string, action: () => void) {
     this.setState({ queue: this.state.queue.concat({ key, action }) });
+    this.processTask();
   }
-  async nextTask() {
-    let { queue, status } = this.state;
-    if (status.loading === "") return;
-    const task = queue.shift();
-    if (!task) return;
-    status.loading = task.key;
-    this.setState({ status, queue });
-    console.debug(`processing: ${status.loading}`);
 
+  async processTask() {
+    if (this.state.processingTask) return;
+    let { queue } = this.state;
+    const task = queue.shift();
+    if (!task) return this.setState({ fetching: "" });
+    this.setState({ fetching: task.key, queue, processingTask: true });
+    //console.debug(`Fetching ${task.key}`);
     await task.action();
-    status.loading = "";
-    this.setState({ status });
-    setTimeout(() => this.nextTask(), 0);
+    this.setState({ processingTask: false });
+    setTimeout(() => this.processTask(), 0);
   }
 
   async fetchLastReward(api: Api, era: number) {
     const lastReward = Number(await api.query.staking.erasValidatorReward(era));
-    console.debug(`last reward`, era, lastReward);
-    if (lastReward) {
-      let { status } = this.state;
-      status.lastReward = lastReward;
-      this.save("status", status);
-    } else this.fetchLastReward(api, era - 1);
+    if (!lastReward) return this.fetchLastReward(api, era - 1);
+
+    console.debug(`reward era ${era}: ${lastReward} tJOY`);
+    let { status } = this.state;
+    status.lastReward = lastReward;
+    this.save("status", status);
   }
 
   async fetchTokenomics() {
@@ -191,61 +179,50 @@ class App extends React.Component<IProps, IState> {
     } catch (e) {}
   }
 
-  async fetchChannels(api: Api) {
-    const lastId = await get.currentChannelId(api);
-    for (let id = lastId; id > 0; id--) {
-      if (this.state.channels.find((c) => c.id === id)) return lastId;
-
-      console.debug(`Fetching channel ${id}`);
-      const data = await api.query.contentWorkingGroup.channelById(id);
-
-      const handle = String(data.handle);
-      const title = String(data.title);
-      const description = String(data.description);
-      const avatar = String(data.avatar);
-      const banner = String(data.banner);
-      const content = String(data.content);
-      const ownerId = Number(data.owner);
-      const accountId = String(data.role_account);
-      const publicationStatus =
-        data.publication_status === "Public" ? true : false;
-      const curation = String(data.curation_status);
-      const createdAt = data.created;
-      const principal = Number(data.principal_id);
-      //this.fetchMemberByAccount(api, accountId);
-
-      const channel: Channel = {
-        id,
-        handle,
-        title,
-        description,
-        avatar,
-        banner,
-        content,
-        ownerId,
-        accountId,
-        publicationStatus,
-        curation,
-        createdAt,
-        principal,
-      };
-
-      //console.debug(data, channel);
-      const channels = this.state.channels.concat(channel);
-      this.save("channels", channels);
-    }
-    return lastId;
-  }
+  async fetchChannel(api: Api, id: number) {
+    if (this.state.channels.find((c) => c.id === id)) return;
+    const data = await api.query.contentWorkingGroup.channelById(id);
 
-  async fetchCategories(api: Api) {
-    const lastId = await get.currentCategoryId(api);
-    for (let id = lastId; id > 0; id--) {
-      if (this.state.categories.find((c) => c.id === id)) return lastId;
-      this.enqueue(`category ${id}`, () => this.fetchCategory(api, id));
-    }
-    return lastId;
+    const handle = String(data.handle);
+    const title = String(data.title);
+    const description = String(data.description);
+    const avatar = String(data.avatar);
+    const banner = String(data.banner);
+    const content = String(data.content);
+    const ownerId = Number(data.owner);
+    const accountId = String(data.role_account);
+    const publicationStatus =
+      data.publication_status === "Public" ? true : false;
+    const curation = String(data.curation_status);
+    const createdAt = data.created;
+    const principal = Number(data.principal_id);
+    this.fetchMemberByAccount(api, accountId);
+
+    const channel: Channel = {
+      id,
+      handle,
+      title,
+      description,
+      avatar,
+      banner,
+      content,
+      ownerId,
+      accountId,
+      publicationStatus,
+      curation,
+      createdAt,
+      principal,
+    };
+
+    //console.debug(data, channel);
+    const channels = this.state.channels.concat(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;
     const data = await api.query.forum.categoryById(id);
     const threadId = Number(data.thread_id);
     const title = String(data.title);
@@ -275,91 +252,89 @@ class App extends React.Component<IProps, IState> {
     };
 
     this.save("categories", this.state.categories.concat(category));
-  }
-  async fetchPosts(api: Api) {
-    const lastId = get.currentPostId(api);
-    //const { data } = await axios.get(`${apiLocation}/posts`);
-    //console.log(`received posts`, data);
-    //this.save("posts", data);
-    //return lastId;
-
-    let { posts } = this.state;
-    for (let id = lastId; id > 0; id--) {
-      if (posts.find((p) => p.id === id)) return lastId;
-      this.enqueue(`post ${id}`, () => this.fetchPost(api, id));
-    }
-    return lastId;
+    if (id > 1)
+      this.enqueue(`category ${id - 1}`, () => this.fetchCategory(api, id - 1));
   }
   async fetchPost(api: Api, id: number) {
-    if (this.state.posts.find((p) => p.id === id)) return;
-    console.debug(`fetching post ${id}`);
-    const data = await api.query.forum.postById(id);
+    const exists = this.state.posts.find((p) => p.id === id);
+    if (exists) return this.fetchMemberByAccount(api, exists.authorId);
 
+    const data = await api.query.forum.postById(id);
     const threadId = Number(data.thread_id);
     const text = data.current_text.slice(0, 1000);
     //const moderation = data.moderation;
     //const history = data.text_change_history;
     const createdAt = data.created_at;
     const authorId = String(data.author_id);
+    this.fetchMemberByAccount(api, authorId);
 
     const post: Post = { id, threadId, text, authorId, createdAt };
     const posts = this.state.posts.concat(post);
     this.save("posts", posts);
+    if (id > 1)
+      this.enqueue(`post ${id - 1}`, () => this.fetchPost(api, id - 1));
   }
 
-  async fetchThreads(api: Api) {
-    const lastId = await get.currentThreadId(api);
-    for (let id = lastId; id > 0; id--) {
-      if (this.state.threads.find((t) => t.id === id)) return lastId;
-      console.debug(`fetching thread ${id}`);
-      const data = await api.query.forum.threadById(id);
-
-      const title = String(data.title);
-      const categoryId = Number(data.category_id);
-      const nrInCategory = Number(data.nr_in_category);
-      const moderation = data.moderation;
-      const createdAt = String(data.created_at.block);
-      const authorId = String(data.author_id);
-
-      const thread: Thread = {
-        id,
-        title,
-        categoryId,
-        nrInCategory,
-        moderation,
-        createdAt,
-        authorId,
-      };
-      const threads = this.state.threads.concat(thread);
-      this.save("threads", threads);
-    }
-    return lastId;
+  async fetchThread(api: Api, id: number) {
+    if (this.state.threads.find((t) => t.id === id)) return;
+    const data = await api.query.forum.threadById(id);
+
+    const title = String(data.title);
+    const categoryId = Number(data.category_id);
+    const nrInCategory = Number(data.nr_in_category);
+    const moderation = data.moderation;
+    const createdAt = String(data.created_at.block);
+    const authorId = String(data.author_id);
+
+    const thread: Thread = {
+      id,
+      title,
+      categoryId,
+      nrInCategory,
+      moderation,
+      createdAt,
+      authorId,
+    };
+    const threads = this.state.threads.concat(thread);
+    this.save("threads", threads);
+    if (id > 1)
+      this.enqueue(`thread ${id - 1}`, () => this.fetchThread(api, id - 1));
   }
 
-  async fetchCouncils(api: Api, currentRound: number) {
+  // council
+  async fetchCouncils(api: Api) {
+    const currentRound = await api.query.councilElection.round();
+    for (let round = Number(currentRound.toJSON()); 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;
-    const cycle = 201600;
+    councils[round] = council.toJSON();
+    this.save("councils", councils);
+    council.map((c) => this.fetchMemberByAccount(api, c.member));
+  }
 
-    for (let round = 0; round < currentRound; round++) {
-      const block = 57601 + round * cycle;
-      if (councils[round] || block > this.state.block) continue;
+  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];
+    const termEndsAt = Number((await api.query.council.termEndsAt()).toJSON());
 
-      console.debug(`Fetching council at block ${block}`);
-      const blockHash = await api.rpc.chain.getBlockHash(block);
-      if (!blockHash) continue;
+    let { status } = this.state;
+    const { council } = status;
+    if (council)
+      if (block < council.termEndsAt || block < council.stageEndsAt) return;
 
-      councils[round] = await api.query.council.activeCouncil.at(blockHash);
-      this.save("councils", councils);
-    }
+    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);
   }
 
   // proposals
-  async fetchProposals(api: Api) {
-    const lastId = await get.proposalCount(api);
-    for (let id = lastId; id > 0; id--)
-      this.enqueue(`proposal ${id}`, () => this.fetchProposal(api, id));
-    return lastId;
-  }
   async fetchProposal(api: Api, id: number) {
     const { proposals } = this.state;
     const exists = this.state.proposals.find((p) => p && p.id === id);
@@ -382,25 +357,30 @@ class App extends React.Component<IProps, IState> {
     }
     proposals[id] = proposal;
     this.save("proposals", proposals);
-    this.fetchVotesPerProposal(api, proposal);
+    this.enqueue(`votes for proposal ${id}`, () =>
+      this.fetchVotesPerProposal(api, proposal)
+    );
+    if (id > 1)
+      this.enqueue(`proposal ${id - 1}`, () => this.fetchProposal(api, id - 1));
   }
 
   async fetchVotesPerProposal(api: Api, proposal: ProposalDetail) {
     const { votesByAccount } = proposal;
     if (votesByAccount && votesByAccount.length) return;
 
-    console.debug(`Fetching proposal votes (${proposal.id})`);
     const { councils, proposals } = this.state;
     let members: Member[] = [];
-    councils.map((seats) =>
-      seats.forEach(async (seat: Seat) => {
-        if (members.find((member) => member.account === seat.member)) return;
-        const member = this.state.members.find(
-          (m) => m.account === seat.member
-        );
-        member && members.push(member);
-      })
-    );
+    councils
+      .filter((c) => c)
+      .map((seats) =>
+        seats.forEach(async (seat: Seat) => {
+          if (members.find((member) => member.account === seat.member)) return;
+          const member = this.state.members.find(
+            (m) => m.account === seat.member
+          );
+          member && members.push(member);
+        })
+      );
 
     const { id } = proposal;
     proposal.votesByAccount = await Promise.all(
@@ -418,7 +398,6 @@ class App extends React.Component<IProps, IState> {
     proposalId: number,
     voterId: number
   ): Promise<string> {
-    console.debug(`Fetching vote by ${voterId} for proposal ${proposalId}`);
     const vote: VoteKind = await api.query.proposalsEngine.voteExistsByProposalByVoter(
       proposalId,
       voterId
@@ -433,32 +412,30 @@ class App extends React.Component<IProps, IState> {
     return hasVoted ? String(vote) : "";
   }
 
-  // nominators, validators
+  // validators
 
-  async fetchNominators(api: Api) {
-    const nominatorEntries = await api.query.staking.nominators.entries();
-    const nominators = nominatorEntries.map((n: any) => String(n[0].toHuman()));
-    this.save("nominators", nominators);
-  }
   async fetchValidators(api: Api) {
-    // session.disabledValidators: Vec<u32>
-    // TODO check online: imOnline.keys
-    //  imOnline.authoredBlocks: 2
-    // TODO session.currentIndex: 17,081
+    const validatorEntries = await api.query.session.validators();
+    const validators = validatorEntries.map((v: any) => String(v));
+    this.save("validators", validators);
+
     const stashes = await api.derive.staking.stashes();
     this.save(
       "stashes",
       stashes.map((s: any) => String(s))
     );
+    this.enqueue("nominators", () => this.fetchNominators(api));
+    return validators;
+  }
 
-    const validatorEntries = await api.query.session.validators();
-    const validators = await validatorEntries.map((v: any) => String(v));
-    this.save("validators", validators);
+  async fetchNominators(api: Api) {
+    const nominatorEntries = await api.query.staking.nominators.entries();
+    const nominators = nominatorEntries.map((n: any) => String(n[0].toHuman()));
+    this.save("nominators", nominators);
   }
 
   async fetchStakes(api: Api, era: number, validators: string[]) {
     // TODO staking.bondedEras: Vec<(EraIndex,SessionIndex)>
-    console.debug(`fetching stakes`);
     const { stashes } = this.state;
     if (!stashes) return;
     stashes.forEach(async (validator: string) => {
@@ -495,27 +472,24 @@ class App extends React.Component<IProps, IState> {
   }
 
   // accounts
-  async fetchMembers(api: Api, lastId: number) {
-    for (let id = lastId; id > 0; id--) {
-      this.fetchMember(api, id);
-    }
-  }
   async fetchMemberByAccount(api: Api, account: string): Promise<Member> {
+    const empty = { id: -1, handle: `?`, account, about: ``, registeredAt: 0 };
+    if (!account) return empty;
     const exists = this.state.members.find(
       (m: Member) => String(m.account) === String(account)
     );
     if (exists) return exists;
 
     const id = await get.memberIdByAccount(api, account);
-    if (!id)
-      return { id: -1, handle: `unknown`, account, about: ``, registeredAt: 0 };
+    if (!id) return empty;
     return await this.fetchMember(api, Number(id));
   }
   async fetchMember(api: Api, id: number): Promise<Member> {
     const exists = this.state.members.find((m: Member) => m.id === id);
-    if (exists) return exists;
-
-    console.debug(`Fetching member ${id}`);
+    if (exists) {
+      setTimeout(() => this.fetchMember(api, id--), 0);
+      return exists;
+    }
     const membership = await get.membership(api, id);
 
     const handle = String(membership.handle);
@@ -526,6 +500,7 @@ class App extends React.Component<IProps, IState> {
     const members = this.state.members.concat(member);
     this.save(`members`, members);
     this.updateHandles(members);
+    this.enqueue(`member ${id--}`, () => this.fetchMember(api, id--));
     return member;
   }
   updateHandles(members: Member[]) {
@@ -596,12 +571,6 @@ class App extends React.Component<IProps, IState> {
     this.setState({ members });
     this.updateHandles(members);
   }
-  loadCouncils() {
-    const councils = this.load("councils");
-    if (!councils || !councils.length || typeof councils[0][0] === "number")
-      return;
-    this.setState({ councils });
-  }
   loadPosts() {
     const posts: Post[] = this.load("posts");
     posts.forEach(({ id, text }) => {
@@ -612,32 +581,31 @@ class App extends React.Component<IProps, IState> {
   }
 
   clearData() {
-    let { status } = this.state;
-    status.version = version;
-    this.save("status", status);
+    console.log(`Resetting db to version ${version}`);
+    this.save("status", { version });
     this.save("proposals", []);
     this.save("posts", []);
   }
+
   async loadData() {
     const status = this.load("status");
     if (status && status.version !== version) return this.clearData();
     if (status) this.setState({ status });
     console.debug(`Loading data`);
     this.loadMembers();
-    this.loadCouncils();
-    "categories channels proposals posts threads validators nominators handles tokenomics reports stakes stars"
+    "councils categories channels proposals posts threads handles tokenomics reports validators nominators stakes stars"
       .split(" ")
       .map((key) => this.load(key));
-    console.debug(`Finished loading.`);
   }
 
   load(key: string) {
-    console.debug(`loading ${key}`);
+    //console.debug(`loading ${key}`);
     try {
       const data = localStorage.getItem(key);
       if (!data) return;
       const size = data.length;
-      if (size > 10240) console.debug(`${key}: ${(size / 1024).toFixed(1)} KB`);
+      if (size > 10240)
+        console.debug(` -${key}: ${(size / 1024).toFixed(1)} KB`);
       this.setState({ [key]: JSON.parse(data) });
       return JSON.parse(data);
     } catch (e) {
@@ -650,26 +618,36 @@ class App extends React.Component<IProps, IState> {
       localStorage.setItem(key, JSON.stringify(data));
     } catch (e) {
       console.warn(`Failed to save ${key} (${data.length}KB)`, e);
-      if (key !== `posts`) {
-        localStorage.setItem(`posts`, `[]`);
-        localStorage.setItem(`channels`, `[]`);
-      }
+      //if (key !== `posts`) {
+      //  localStorage.setItem(`posts`, `[]`);
+      //  localStorage.setItem(`channels`, `[]`);
+      //}
     }
   }
 
   toggleFooter() {
-    console.log(this.state.hideFooter);
     this.setState({ hideFooter: !this.state.hideFooter });
   }
 
   render() {
     if (this.state.loading) return <Loading />;
+    const { connected, fetching, hideFooter } = this.state;
     return (
-      <Routes
-        toggleFooter={this.toggleFooter}
-        toggleStar={this.toggleStar}
-        {...this.state}
-      />
+      <>
+        <Routes
+          toggleFooter={this.toggleFooter}
+          toggleStar={this.toggleStar}
+          {...this.state}
+        />
+
+        <Footer
+          show={!hideFooter}
+          toggleHide={this.toggleFooter}
+          link={userLink}
+        />
+
+        <Status connected={connected} fetching={fetching} />
+      </>
     );
   }
 
@@ -679,7 +657,7 @@ class App extends React.Component<IProps, IState> {
     ApiPromise.create({ provider, types }).then((api) =>
       api.isReady.then(() => {
         console.log(`Connected to ${wsLocation}`);
-        this.setState({ connecting: false });
+        this.setState({ connected: true });
         this.handleApi(api);
       })
     );
@@ -688,9 +666,8 @@ class App extends React.Component<IProps, IState> {
   componentDidMount() {
     this.loadData();
     this.connectEndpoint();
+    setTimeout(() => this.fetchTokenomics(), 30000);
     //this.initializeSocket();
-    this.fetchTokenomics();
-    setInterval(this.fetchTokenomics, 900000);
   }
   componentWillUnmount() {}
   constructor(props: IProps) {

+ 15 - 18
src/components/Calendar/index.tsx

@@ -29,8 +29,6 @@ class Calendar extends Component<IProps, IState> {
     this.openProposal = this.openProposal.bind(this);
   }
 
-  componentDidMount() {}
-
   filterItems() {
     const { status, proposals } = this.props;
     const { hide } = this.state;
@@ -110,7 +108,7 @@ class Calendar extends Component<IProps, IState> {
 
   render() {
     const { hide, groups } = this.state;
-    const { status } = this.props;
+    const { history, status } = this.props;
 
     if (!status.block) return <Loading />;
     const items = this.state.items || this.filterItems();
@@ -132,25 +130,24 @@ class Calendar extends Component<IProps, IState> {
     );
 
     return (
-      <div>
+      <>
         <Link className="back left" to={"/"}>
-          <Button variant="secondary">Back</Button>
+          <Back history={history} />
         </Link>
 
-        <Timeline
-          groups={groups.filter((g) => !hide[g.id])}
-          items={items}
-          sidebarWidth={220}
-          sidebarContent={filters}
-          stackItems={true}
-          defaultTimeStart={moment(status.startTime).add(-1, "day")}
-          defaultTimeEnd={moment().add(15, "day")}
-          onItemSelect={this.openProposal}
-        />
-        <div className="position-fixed" style={{ left: "0", bottom: "0" }}>
-          <Back history={this.props.history} />
+        <div>
+          <Timeline
+            groups={groups.filter((g) => !hide[g.id])}
+            items={items}
+            sidebarWidth={220}
+            sidebarContent={filters}
+            stackItems={true}
+            defaultTimeStart={moment(status.startTime).add(-1, "day")}
+            defaultTimeEnd={moment().add(15, "day")}
+            onItemSelect={this.openProposal}
+          />
         </div>
-      </div>
+      </>
     );
   }
 }

+ 3 - 4
src/components/Council/ElectionStatus.tsx

@@ -24,17 +24,16 @@ const ElectionStage = (props: {
     return <div>election in {left}</div>;
   }
 
-  //const stageObject = JSON.parse(JSON.stringify(stage));
   let stageString = Object.keys(stage)[0];
   const left = timeLeft(stage[stageString] - block);
 
-  if (stageString === "Announcing")
+  if (stageString === "announcing")
     return <a href={`${domain}/#/council/applicants`}>{left} to apply</a>;
 
-  if (stageString === "Voting")
+  if (stageString === "voting")
     return <a href={`${domain}/#/council/applicants`}>{left} to vote</a>;
 
-  if (stageString === "Revealing")
+  if (stageString === "revealing")
     return <a href={`${domain}/#/council/votes`}>{left} to reveal votes</a>;
 
   return <div>{JSON.stringify(stage)}</div>;

+ 1 - 1
src/components/Council/index.tsx

@@ -39,7 +39,7 @@ const Council = (props: {
               <MemberBox
                 id={m.id || 0}
                 account={m.member}
-                handle={m.handle || handles[m.member]}
+                handle={handles[m.member]}
                 members={members}
                 councils={councils}
                 proposals={proposals}

+ 2 - 2
src/components/Councils/Leaderboard.tsx

@@ -22,8 +22,8 @@ const LeaderBoard = (props: {
   councils: Seat[][];
   cycle: number;
 }) => {
-  const { cycle, councils, members, proposals } = props;
-
+  const { cycle, members, proposals } = props;
+  const councils = props.councils.filter((c) => c);
   const summarizeVotes = (handle: string, propList: ProposalDetail[]) => {
     let votes = 0;
     propList.forEach((p) => {

+ 18 - 16
src/components/Councils/index.tsx

@@ -58,22 +58,24 @@ const Rounds = (props: {
       />
 
       <h2 className="w-100 text-center text-light">Votes per Council</h2>
-      {councils.map((council, i: number) => (
-        <CouncilVotes
-          key={i}
-          expand={i === councils.length - 1}
-          block={block}
-          round={i + 1}
-          council={council}
-          members={props.members}
-          proposals={props.proposals.filter(
-            (p: ProposalDetail) =>
-              p &&
-              p.createdAt > 57601 + i * cycle &&
-              p.createdAt < 57601 + (i + 1) * cycle
-          )}
-        />
-      ))}
+      {councils
+        .filter((c) => c)
+        .map((council, i: number) => (
+          <CouncilVotes
+            key={i}
+            expand={i === councils.length - 1}
+            block={block}
+            round={i + 1}
+            council={council}
+            members={props.members}
+            proposals={props.proposals.filter(
+              (p: ProposalDetail) =>
+                p &&
+                p.createdAt > 57601 + i * cycle &&
+                p.createdAt < 57601 + (i + 1) * cycle
+            )}
+          />
+        ))}
     </div>
   );
 };

+ 5 - 6
src/components/Dashboard/Footer.tsx

@@ -2,7 +2,6 @@ import React from "react";
 import { X, Info } from "react-feather";
 
 const Footer = (props: {
-  connecting: boolean;
   show: boolean;
   toggleHide: () => void;
   link: string;
@@ -13,14 +12,14 @@ const Footer = (props: {
       <Info className="footer-hidden" onClick={() => props.toggleHide()} />
     );
   return (
-    <div className="w-100 footer">
+    <div className="w-100 footer text-light">
       <X className="footer-hidden" onClick={() => props.toggleHide()} />
-      If you find this place useful,{" "}
-      <a className="mx-1" href={link}>
+      If you find this place useful, please consider to{" "}
+      <a className="mx-1 text-light" href={link}>
         <u>send some tokens</u>
       </a>
-      and a
-      <a className="mx-1" href="/forum/threads/257">
+      or a
+      <a className="mx-1 text-light" href="/forum/threads/257">
         <u>message with ideas</u>
       </a>{" "}
       to make it even better.

+ 39 - 0
src/components/Dashboard/Forum.tsx

@@ -0,0 +1,39 @@
+import React from "react";
+import { Link } from "react-router-dom";
+import LatestPost from "../Forum/LatestPost";
+import Loading from "../Loading";
+
+import { Handles, Post, Thread } from "../../types";
+
+const Forum = (props: {
+  handles: Handles;
+  posts: Post[];
+  threads: Thread[];
+}) => {
+  const { handles, posts, threads, startTime } = props;
+  if (!posts.length) return <Loading target="posts" />;
+  return (
+    <div className="w-100 p-3 m-3 d-flex flex-column">
+      <h3>
+        <Link className="text-light" to={"/forum"}>
+          Forum
+        </Link>
+      </h3>
+      {props.posts
+        .sort((a, b) => b.id - a.id)
+        .slice(0, 10)
+        .map((post) => (
+          <LatestPost
+            key={post.id}
+            selectThread={() => {}}
+            handles={handles}
+            post={post}
+            thread={threads.find((t) => t.id === post.threadId)}
+            startTime={startTime}
+          />
+        ))}
+    </div>
+  );
+};
+
+export default Forum;

+ 47 - 0
src/components/Dashboard/Proposals.tsx

@@ -0,0 +1,47 @@
+import React from "react";
+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 pending = proposals.filter((p) => p && p.result === "Pending");
+  if (!proposals.length) return <Loading target="proposals" />;
+  if (!pending.length) {
+    const loading = proposals.filter((p) => !p);
+    return loading.length ? (
+      <Loading />
+    ) : (
+      <div className="box">No active proposals.</div>
+    );
+  }
+  return (
+    <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={"/spending"}>
+          Spending
+        </Link>
+        <Link className="m-3 text-light" to={"/councils"}>
+          Votes
+        </Link>
+      </div>
+      <ProposalsTable
+        hideNav={true}
+        proposals={pending}
+        proposalPosts={props.proposalPosts}
+        members={members}
+        councils={councils}
+        posts={posts}
+        startTime={startTime}
+        validators={validators}
+      />
+    </div>
+  );
+};
+
+export default Proposals;

+ 10 - 0
src/components/Dashboard/Status.jsx

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

+ 56 - 61
src/components/Dashboard/index.tsx

@@ -3,8 +3,6 @@ import { Link } from "react-router-dom";
 import { Council } from "..";
 import Forum from "./Forum";
 import Proposals from "./Proposals";
-import Footer from "./Footer";
-import Status from "./Status";
 import Validators from "../Validators";
 import { IState } from "../../types";
 
@@ -16,8 +14,6 @@ interface IProps extends IState {
 const Dashboard = (props: IProps) => {
   const {
     toggleStar,
-    toggleFooter,
-    hideFooter,
     councils,
     domain,
     handles,
@@ -34,70 +30,69 @@ const Dashboard = (props: IProps) => {
     stakes,
     validators,
   } = 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="back bg-warning d-flex flex-column p-2">
-        <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>
-
-      <h1 className="title">
-        <a href={domain}>Joystream</a>
-      </h1>
+    <>
+      <div className="w-100 flex-grow-1 d-flex align-items-center justify-content-center d-flex flex-column pb-5">
+        <div className="back bg-warning d-flex flex-column p-2">
+          <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>
 
-      <Council
-        councils={councils}
-        members={members}
-        handles={handles}
-        posts={posts}
-        proposals={proposals}
-        stars={stars}
-        status={status}
-        validators={validators}
-      />
+        <h1 className="title">
+          <a href={domain}>Joystream</a>
+        </h1>
 
-      <Proposals
-        members={members}
-        councils={councils}
-        posts={posts}
-        proposals={proposals}
-        proposalPosts={props.proposalPosts}
-        validators={validators}
-      />
+        <Council
+          councils={councils}
+          members={members}
+          handles={handles}
+          posts={posts}
+          proposals={proposals}
+          stars={stars}
+          status={status}
+          validators={validators}
+        />
 
-      <Forum posts={posts} threads={threads} startTime={status.startTime} />
+        <Proposals
+          members={members}
+          councils={councils}
+          posts={posts}
+          proposals={proposals}
+          proposalPosts={props.proposalPosts}
+          validators={validators}
+          startTime={status.startTime}
+        />
 
-      <Validators
-        hideBackButton={true}
-        toggleStar={toggleStar}
-        councils={councils}
-        handles={handles}
-        members={members}
-        posts={posts}
-        proposals={proposals}
-        nominators={nominators}
-        validators={validators}
-        stashes={stashes}
-        stars={stars}
-        stakes={stakes}
-        rewardPoints={rewardPoints}
-        tokenomics={tokenomics}
-        status={status}
-      />
+        <Forum
+          handles={handles}
+          posts={posts}
+          threads={threads}
+          startTime={status.startTime}
+        />
 
-      <Footer
-        show={!hideFooter}
-        toggleHide={toggleFooter}
-        connecting={status.connecting}
-        link={userLink}
-      />
-      <Status connecting={status.connecting} loading={status.loading} />
-    </div>
+        <Validators
+          hideBackButton={true}
+          toggleStar={toggleStar}
+          councils={councils}
+          handles={handles}
+          members={members}
+          posts={posts}
+          proposals={proposals}
+          nominators={nominators}
+          validators={validators}
+          stashes={stashes}
+          stars={stars}
+          stakes={stakes}
+          rewardPoints={rewardPoints}
+          tokenomics={tokenomics}
+          status={status}
+        />
+      </div>
+    </>
   );
 };
 

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

@@ -15,8 +15,8 @@ const LatestPost = (props: {
   thread?: Thread;
   startTime: number;
 }) => {
-  const { selectThread, handles, thread, post, startTime } = props;
-  const { authorId, createdAt, id, threadId, text } = post;
+  const { selectThread, handles = [], thread, post, startTime } = props;
+  const { authorId, createdAt, id, threadId, text, handle } = post;
 
   return (
     <div

+ 6 - 8
src/components/Loading.tsx

@@ -1,16 +1,14 @@
 import React from "react";
-import { Spinner } from "react-bootstrap";
+import { Button, Spinner } from "react-bootstrap";
 
 const Loading = (props: { target?: string }) => {
   const { target } = props;
+  const title = target ? `Fetching ${target}` : "Connecting to Websocket";
   return (
-    <div className="h-100 d-flex flex-column flex-grow-1 align-items-center justify-content-center p-1">
-      <Spinner
-        animation="border"
-        variant="light"
-        title={target ? `Loading ${target}` : "Connecting to Websocket"}
-      />
-    </div>
+    <Button variant="warning" className="m-1">
+      <Spinner animation="border" variant="dark" size="sm" className="mr-1" />
+      {title}
+    </Button>
   );
 };
 

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

@@ -24,7 +24,7 @@ const MemberBox = (props: {
   const member = members.find(
     (m) => m.handle === h || String(m.account) === h || m.id === Number(h)
   );
-  if (!member) return <NotFound />;
+  if (!member) return <NotFound history={props.history} />;
 
   const council = councils[councils.length - 1];
   if (!council) return <Loading />;

+ 50 - 0
src/components/Members/MemberPosts.tsx

@@ -0,0 +1,50 @@
+import React from "react";
+import { Link } from "react-router-dom";
+import Markdown from "react-markdown";
+import gfm from "remark-gfm";
+import moment from "moment"
+
+const Posts = (props: {
+  posts: Post[];
+  threadTitle: (id: number) => string;
+  startTime: number;
+}) => {
+  const { posts, startTime, threadTitle } = props;
+
+  if (!posts.length) return <div />;
+
+  return (
+    <div className="mt-3">
+      <h3 className="text-center text-light">Latest Posts</h3>
+      {posts
+        .sort((a, b) => b.id - a.id)
+        .slice(0, 5)
+        .map((p) => (
+          <div key={p.id} className="box d-flex flex-row">
+            <div className="col-2 mr-3">
+              <div>
+                {moment(startTime + p.createdAt.block * 6000).fromNow()}
+              </div>
+              <a href={`${domain}/#/forum/threads/${p.threadId}`}>reply</a>
+            </div>
+
+            <div>
+              <Link to={`/forum/threads/${p.threadId}`}>
+                <div className="text-left font-weight-bold mb-3">
+                  {threadTitle(p.threadId)}
+                </div>
+              </Link>
+
+              <Markdown
+                plugins={[gfm]}
+                className="overflow-auto text-left"
+                children={p.text}
+              />
+            </div>
+          </div>
+        ))}
+    </div>
+  );
+};
+
+export default Posts

+ 45 - 0
src/components/Members/MemberProposals.tsx

@@ -0,0 +1,45 @@
+import React from "react";
+import { Link } from "react-router-dom";
+import { Badge, ListGroup } from "react-bootstrap";
+import moment from "moment"
+
+const Proposals = (props: {
+  proposals: ProposalDetail[];
+  startTime: number;
+}) => {
+  const { proposals, startTime } = props;
+
+  if (!proposals.length) return <div />;
+
+  return (
+    <div className="mt-3">
+      <h3 className="text-center text-light">Latest Proposals</h3>
+      <ListGroup className="box">
+        {proposals
+          .sort((a, b) => b.id - a.id)
+          .slice(0, 5)
+          .map((p) => (
+            <ListGroup.Item key={p.id} className="box bg-secondary">
+              <Link to={`/proposals/${p.id}`}>
+                <Badge
+                  className="mr-2"
+                  variant={
+                    p.result === "Approved"
+                      ? "success"
+                      : p.result === "Pending"
+                      ? "warning"
+                      : "danger"
+                  }
+                >
+                  {p.result}
+                </Badge>
+                {p.title}, {moment(startTime + p.createdAt * 6000).fromNow()}
+              </Link>
+            </ListGroup.Item>
+          ))}
+      </ListGroup>
+    </div>
+  );
+};
+
+export default Proposals

+ 8 - 4
src/components/Members/NotFound.tsx

@@ -1,12 +1,16 @@
 import React from "react";
 import { Link } from "react-router-dom";
+import { Back } from "..";
 
 const NotFound = (props: { nolink?: boolean }) => {
   return (
-    <div className="box">
-      <div>No membership found.</div>
-      {props.nolink || <Link to={`/members`}>Back</Link>}
-    </div>
+    <>
+      <Back history={props.history} />
+      <div className="box">
+        <div>No membership found.</div>
+        {props.nolink || <Link to={`/members`}>Back</Link>}
+      </div>
+    </>
   );
 };
 

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

@@ -26,7 +26,7 @@ const Summary = (props: {
   const { councils, handle, member, proposals, startTime } = props;
 
   const onCouncil = councils.filter((c) =>
-    c.find((seat) => seat.member === member.account)
+    c && c.find((seat) => seat.member === member.account)
   );
 
   let votes: ProposalVote[] = [];

+ 6 - 3
src/components/Mint/index.tsx

@@ -47,7 +47,9 @@ class Mint extends React.Component<IProps, IState> {
   }
 
   componentDidMount() {
-    this.setState({ payout: this.props.lastReward });
+    const payout = this.props.status.lastReward;
+    console.log(`payout`, payout, this.props.status);
+    this.setState({ payout });
     this.setRole({ target: { value: "consul" } });
   }
 
@@ -64,8 +66,9 @@ class Mint extends React.Component<IProps, IState> {
   }
 
   render() {
-    const { tokenomics } = this.props;
+    const { tokenomics, validators } = this.props;
     if (!tokenomics) return <Loading />;
+
     const { role, start, salary, end, payout } = this.state;
 
     const { price } = tokenomics;
@@ -175,7 +178,7 @@ class Mint extends React.Component<IProps, IState> {
 
         <ValidatorRewards
           handleChange={this.handleChange}
-          validators={this.props.validators.length}
+          validators={validators.length}
           payout={payout}
           price={this.props.tokenomics ? this.props.tokenomics.price : 0}
         />

+ 21 - 23
src/components/Proposals/Detail.tsx

@@ -5,59 +5,57 @@ import { TableFromObject } from "..";
 const amount = (amount: number) => (amount / 1000000).toFixed(2);
 const Detail = (props: { detail?: any; type: string }) => {
   const { detail, type } = props;
+  if (type === "text") return <p>Text</p>;
   if (!detail) return <p>{type}</p>;
+  const data = detail[type];
+  if (!data) return console.log(`empty proposal detail`, detail);
 
-  if (type === "Text") return <p>Text</p>;
-
-  if (type === "Spending")
+  if (type === "spending")
     return (
       <>
         <b>Spending</b>
-        <p>{amount(detail.Spending[0])} M tJOY</p>
+        <p title={`to ${data[1]}`}>{amount(data[0])} M tJOY</p>
       </>
     );
 
-  if (type === "SetWorkingGroupMintCapacity")
+  if (type === "setWorkingGroupMintCapacity")
     return (
       <p>
-        Fill {detail.SetWorkingGroupMintCapacity[1]} working group mint
-        <br />({amount(detail.SetWorkingGroupMintCapacity[0])} M tJOY)
+        Fill {data[1]} working group mint
+        <br />({amount(data[0])} M tJOY)
       </p>
     );
 
-  if (type === "SetWorkingGroupLeaderReward")
+  if (type === "setWorkingGroupLeaderReward")
     return (
       <p>
-        Set {detail.SetWorkingGroupLeaderReward[2]} working group reward (
-        {amount(detail.SetWorkingGroupLeaderReward[1])} M tJOY,
-        {detail.SetWorkingGroupLeaderReward[0]})
+        Set {data[2]} working group reward ({amount(data[1])} M tJOY,
+        {data[0]})
       </p>
     );
 
-  if (type === "SetContentWorkingGroupMintCapacity")
-    return (
-      <p>
-        SetContentWorkingGroupMintCapacity (
-        {amount(detail.SetContentWorkingGroupMintCapacity)} M tJOY)
-      </p>
-    );
+  if (type === "setContentWorkingGroupMintCapacity")
+    return <p>SetContentWorkingGroupMintCapacity ({amount(data)} M tJOY)</p>;
 
-  if (type === "BeginReviewWorkingGroupLeaderApplication")
+  if (type === "beginReviewWorkingGroupLeaderApplication")
     return (
       <p>
-        Hire {detail[type][1]} working group leader ({detail[type][0]})
+        Hire {data[1]} working group leader ({data[0]})
       </p>
     );
 
-  if (type === "SetValidatorCount")
-    return <p>SetValidatorCount ({detail.SetValidatorCount})</p>;
+  if (type === "setValidatorCount") return <p>SetValidatorCount ({data})</p>;
+
+  if (type === "addWorkingGroupLeaderOpening")
+    return <p>Hire {data.working_group} lead</p>;
 
+  console.log(detail);
   return (
     <OverlayTrigger
       placement={"right"}
       overlay={
         <Tooltip id={`${type}`} className="tooltip">
-          <TableFromObject data={detail[type]} />
+          <TableFromObject data={data} />
         </Tooltip>
       }
     >

+ 2 - 1
src/components/Proposals/Row.tsx

@@ -90,7 +90,7 @@ const ProposalRow = (props: {
   const duration = blocks ? `${daysStr} ${hoursStr} / ${blocks} blocks` : "";
 
   return (
-    <div className="d-flex flex-row justify-content-between text-left px-2">
+    <div className="d-flex flex-row justify-content-between text-left px-2 mt-3">
       <div className="col-3">
         <OverlayTrigger
           key={id}
@@ -127,6 +127,7 @@ const ProposalRow = (props: {
           </div>
         </OverlayTrigger>
       </div>
+      
       <div className="col-2 text-left">
         <Detail detail={detail} type={type} />
       </div>

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

@@ -18,7 +18,7 @@ const executionFailed = (result: string, executed: any) => {
 
 const Spending = (props: IState) => {
   const spending = props.proposals.filter(
-    (p: ProposalDetail) => p && p.type === "Spending"
+    (p: ProposalDetail) => p && p.type === "spending"
   );
 
   const rounds: ProposalDetail[][] = [];

+ 1 - 1
src/components/Proposals/index.tsx

@@ -34,7 +34,7 @@ const Proposals = (props: {
   // - list all proposals
   return (
     <ProposalTable
-      block={block}
+      block={status.block.id}
       members={members}
       proposals={proposals}
       proposalPosts={proposalPosts}

+ 4 - 1
src/components/Routes/index.tsx

@@ -69,7 +69,10 @@ const Routes = (props: IProps) => {
         path="/calendar"
         render={(routeprops) => <Calendar {...routeprops} {...props} />}
       />
-      <Route path="/timeline" render={() => <Timeline {...props} />} />
+      <Route
+        path="/timeline"
+        render={(routeprops) => <Timeline {...routeprops} {...props} />}
+      />
       <Route
         path="/validators"
         render={(routeprops) => <Validators {...routeprops} {...props} />}

+ 14 - 8
src/components/Timeline/index.tsx

@@ -49,17 +49,23 @@ const Timeline = (props: {
   if (!events.length) return <div />;
 
   return (
-    <div className="timeline-container">
-      <div className="back left">
+    <>
+      <div className="back">
         <Back history={props.history} />
       </div>
 
-      {events
-        .sort((a, b) => b.date - a.date)
-        .map((event: Event, idx) => (
-          <TimelineItem event={event} key={idx} startTime={status.startTime} />
-        ))}
-    </div>
+      <div className="timeline-container">
+        {events
+          .sort((a, b) => b.date - a.date)
+          .map((event: Event, idx) => (
+            <TimelineItem
+              event={event}
+              key={idx}
+              startTime={status.startTime}
+            />
+          ))}
+      </div>
+    </>
   );
 };
 

+ 0 - 20
src/components/Tokenomics/Navigation.tsx

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

+ 6 - 5
src/components/Tokenomics/index.tsx

@@ -1,16 +1,16 @@
 import React from "react";
 import Burns from "./Burns";
-import Navigation from "./Navigation";
 import Overview from "./Overview";
 import ReportBrowser from "./ReportBrowser";
 import Loading from "../Loading";
+import Back from "../Back";
 
 import { Tokenomics } from "../../types";
 
 interface IProps {
   reports: { [key: string]: string };
   tokenomics?: Tokenomics;
-  history:any
+  history: any;
 }
 
 const CouncilReports = (props: IProps) => {
@@ -19,7 +19,10 @@ const CouncilReports = (props: IProps) => {
   const { exchanges, extecutedBurnsAmount } = tokenomics;
 
   return (
-    <div className="h-100 py-3 d-flex flex-row justify-content-center">
+    <div className="h-100 py-3 d-flex flex-row justify-content-center pb-5">
+      <div className="back">
+        <Back history={props.history} />
+      </div>
       <div className="d-flex flex-column text-right  align-items-right">
         <div className="box">
           <h3>Tokenomics</h3>
@@ -30,8 +33,6 @@ const CouncilReports = (props: IProps) => {
           exchanges={exchanges}
           extecutedBurnsAmount={extecutedBurnsAmount}
         />
-
-        <Navigation history={props.history} />
       </div>
 
       <div className="box col-8">

+ 60 - 0
src/components/Validators/Waiting.tsx

@@ -0,0 +1,60 @@
+import React from "react";
+import { Button, ListGroup } from "react-bootstrap";
+import MemberBox from "../Members/MemberBox";
+
+const WaitingButton = (props: {}) => {
+  const { waiting, toggleWaiting } = props;
+  return (
+    <Button
+      variant="secondary"
+      className="mb-5"
+      onClick={() => toggleWaiting()}
+    >
+      Toggle {waiting} waiting nodes
+    </Button>
+  );
+};
+
+const Waiting = (props: {}) => {
+  const {
+    toggleWaiting,
+    show,
+    waiting,
+    councils,
+    handles,
+    members,
+    posts,
+    proposals,
+    startTime,
+    validators,
+  } = props;
+
+  if (!waiting.length) return <div />;
+  if (!show)
+    return (
+      <WaitingButton toggleWaiting={toggleWaiting} waiting={waiting.length} />
+    );
+
+  return (
+    <>
+      <WaitingButton toggleWaiting={toggleWaiting} waiting={waiting.length} />
+      <ListGroup className="waiting-validators">
+        {waiting.map((v) => (
+          <MemberBox
+            id={0}
+            account={v}
+            placement={"top"}
+            councils={councils}
+            handle={handles[v]}
+            members={members}
+            posts={posts}
+            proposals={proposals}
+            startTime={startTime}
+            validators={validators}
+          />
+        ))}
+      </ListGroup>
+    </>
+  );
+};
+export default Waiting;

+ 3 - 0
src/components/index.ts

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

+ 0 - 1
src/index.css

@@ -272,7 +272,6 @@ table td {
 }
 .footer {
   background: black;
-  text: white;
   text-align: center;
   position: fixed;
   bottom: 0px;

+ 1 - 0
src/types.ts

@@ -135,6 +135,7 @@ export interface ProposalPost {
   threadId: number;
   text: string;
   id: number;
+  handle?: string;
 }
 
 export interface Proposals {