Przeglądaj źródła

Members: refactors, fix, user links

Joystream Stats 4 lat temu
rodzic
commit
552d90010d

+ 19 - 17
src/App.tsx

@@ -147,7 +147,7 @@ class App extends React.Component<IProps, IState> {
   }
 
   async fetchTokenomics() {
-    console.log(`Updating tokenomics`);
+    console.debug(`Updating tokenomics`);
     const { data } = await axios.get("https://status.joystream.org/status");
     if (!data) return;
     this.save("tokenomics", data);
@@ -156,7 +156,7 @@ class App extends React.Component<IProps, IState> {
   async fetchChannels(api: Api, lastId: number) {
     for (let id = lastId; id > 0; id--) {
       if (this.state.channels.find((c) => c.id === id)) continue;
-      console.log(`fetching channel ${id}`);
+      console.debug(`Fetching channel ${id}`);
       const data = await api.query.contentWorkingGroup.channelById(id);
 
       const handle = String(data.handle);
@@ -302,14 +302,17 @@ class App extends React.Component<IProps, IState> {
     for (let round = 0; round < currentRound; round++) {
       let council: number[] = [];
       const block = 57601 + round * cycle;
-      console.log(`fetching council at block ${block}`);
+      console.debug(`Fetching council at block ${block}`);
 
       const blockHash = await api.rpc.chain.getBlockHash(block);
       if (!blockHash) continue;
       const seats: Seat[] = await api.query.council.activeCouncil.at(blockHash);
 
       seats.forEach(async (seat) => {
-        const member = await this.fetchMemberByAccount(api, seat.member);
+        const member = await this.fetchMemberByAccount(
+          api,
+          String(seat.member)
+        );
         council = council.concat(Number(member.id));
         councils[round] = council;
         this.save("councils", councils);
@@ -333,7 +336,7 @@ class App extends React.Component<IProps, IState> {
       exists.stage === "Finalized"
     )
       return;
-    console.log(`Fetching proposal ${id}`);
+    console.debug(`Fetching proposal ${id}`);
     const proposal = await get.proposalDetail(api, id);
 
     if (!proposal) return console.warn(`Empty result (proposal ${id})`);
@@ -347,7 +350,7 @@ class App extends React.Component<IProps, IState> {
     const proposal = proposals.find((p) => p && p.id === proposalId);
     if (!proposal) return console.warn(`Proposal ${proposalId} not found.`);
 
-    console.log(`Fetching proposal votes (${proposalId})`);
+    console.debug(`Fetching proposal votes (${proposalId})`);
     let memberIds: { [key: string]: number } = {};
     councils.map((ids: number[]) =>
       ids.map((memberId: number) => memberIds[`${memberId}`]++)
@@ -417,10 +420,7 @@ class App extends React.Component<IProps, IState> {
     );
     return member ? member.handle : String(account);
   }
-  async fetchMemberByAccount(
-    api: Api,
-    account: AccountId | string
-  ): Promise<Member> {
+  async fetchMemberByAccount(api: Api, account: string): Promise<Member> {
     const exists = this.state.members.find(
       (m: Member) => String(m.account) === String(account)
     );
@@ -429,12 +429,13 @@ class App extends React.Component<IProps, IState> {
     const id = await get.memberIdByAccount(api, account);
     if (!id)
       return { id: -1, handle: `unknown`, account, about: ``, registeredAt: 0 };
-    return await this.fetchMember(api, id);
+    return await this.fetchMember(api, Number(id));
   }
-  async fetchMember(api: Api, id: MemberId | number): Promise<Member> {
+  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}`);
     const membership = await get.membership(api, id);
 
     const handle = String(membership.handle);
@@ -583,6 +584,7 @@ class App extends React.Component<IProps, IState> {
     const block = this.load("block");
     const now = this.load("now");
     this.setState({ block, now, termEndsAt, loading: false });
+    console.debug(`Finished loading.`);
   }
 
   load(key: string) {
@@ -590,16 +592,16 @@ class App extends React.Component<IProps, IState> {
       const data = localStorage.getItem(key);
       if (data) return JSON.parse(data);
     } catch (e) {
-      console.log(`Failed to load ${key}`, e);
+      console.warn(`Failed to load ${key}`, e);
     }
   }
   save(key: string, data: any) {
     try {
       localStorage.setItem(key, JSON.stringify(data));
     } catch (e) {
-      console.log(`Failed to save ${key}`, e);
+      console.warn(`Failed to save ${key}`, e);
     } finally {
-      //console.log(`saving ${key}`, data);
+      //console.debug(`saving ${key}`, data);
       this.setState({ [key]: data });
     }
   }
@@ -613,10 +615,10 @@ class App extends React.Component<IProps, IState> {
     this.loadData();
     this.initializeSocket();
     this.fetchTokenomics();
-    setInterval(this.fetchTokenomics, 300000);
+    setInterval(this.fetchTokenomics, 900000);
   }
   componentWillUnmount() {
-    console.log("unmounting...");
+    console.debug("unmounting...");
   }
   constructor(props: IProps) {
     super(props);

+ 2 - 2
src/components/Back.tsx

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

+ 26 - 115
src/components/Members/Member.tsx

@@ -1,140 +1,51 @@
 import React from "react";
-import { Link } from "react-router-dom";
 import { Member, Post, ProposalDetail } from "../../types";
 import { domain } from "../../config";
-import moment from "moment";
-
-interface Vote {
-  proposal: ProposalDetail;
-  vote: string;
-}
-
-const NotFound = () => {
-  return (
-    <div className="box">
-      <div> Member not found</div>
-      <Link to={`/members`}>Back</Link>
-    </div>
-  );
-};
+import Summary from "./Summary";
+import NotFound from "./NotFound";
+import Back from "../Back";
 
 const MemberBox = (props: {
-  match?: { params: { handle: string } };
-  handle?: string;
+  match: { params: { handle: string } };
   members: Member[];
   councils: number[][];
   proposals: ProposalDetail[];
   posts: Post[];
-  startTime: number;
+  block: number;
+  now: number;
 }) => {
-  const { councils, members, proposals } = props;
-  const handle = props.handle
-    ? props.handle
-    : props.match
-    ? props.match.params.handle
-    : `?`;
-  const member = members.find((m) => m.handle === handle);
+  const { block, now, councils, members, posts, proposals } = props;
+  const handle = props.match.params.handle;
+  const member = members.find(
+    (m) => m.handle === handle || String(m.account) === handle
+  );
   if (!member) return <NotFound />;
 
   const id = Number(member.id);
   const council = councils[councils.length - 1];
   if (!council) return <div>Loading..</div>;
   const isCouncilMember = council.includes(id);
-  const onCouncil = councils.filter((c) => c.includes(Number(member.id)));
-
-  let votes: Vote[] = [];
-  proposals.forEach((p) => {
-    if (!p || !p.votesByMemberId) return;
-    const vote = p.votesByMemberId.find((v) => v.memberId === id);
-    if (vote && vote.vote !== ``) votes.push({ proposal: p, vote: vote.vote });
-  });
-  const createdProposals = proposals.filter((p) => p && p.author === handle);
-
-  const posts = props.posts.filter(
-    (p) => p.authorId === String(member.account)
-  );
-
-  const time = props.startTime + member.registeredAt * 6000;
-  const date = moment(time);
-  const created = date.isValid()
-    ? date.format("DD/MM/YYYY HH:mm")
-    : member.registeredAt;
-
-  return (
-    <div className="box">
-      {props.match && (
-        <Link className="float-left" to={"/members"}>
-          back
-        </Link>
-      )}
-      {isCouncilMember && <div>council member</div>}
-      <a href={`${domain}/#/members/${handle}`}>
-        <h1>{handle}</h1>
-      </a>
-
-      <div className="text-left">
-        <div>
-          Registered on {created} (id {member.id})
-        </div>
-        <OnCouncil onCouncil={onCouncil.length} votes={votes.length} />
-        <Proposals proposals={createdProposals.length} />
-        <Posts posts={posts.length} />
-
-        <About about={member.about} />
-      </div>
-    </div>
-  );
-};
 
-const About = (props: { about: string }) => {
-  if (props.about === ``) return <div />;
-  return (
-    <div className="mt-3" style={{ maxWidth: "320px" }}>
-      About: {props.about}
-    </div>
-  );
-};
-
-const Proposals = (props: { proposals: number }) => {
-  const { proposals } = props;
-  if (!proposals) return <div />;
-  const count = proposals > 1 ? `proposals` : `proposal`;
   return (
     <div>
-      Authored <Link to={`/proposals`}>{proposals}</Link> {count}.
-    </div>
-  );
-};
-
-const Posts = (props: { posts: number }) => {
-  if (!props.posts) return <div />;
-  const count = props.posts > 1 ? `posts` : `post`;
-  return (
-    <div>
-      Wrote <Link to={`/forum`}>{props.posts}</Link> forum {count}.
-    </div>
-  );
-};
-
-const OnCouncil = (props: { onCouncil: number; votes: number }) => {
-  const { onCouncil, votes } = props;
-  if (!onCouncil) return <div />;
-  return (
-    <div>
-      <div>
-        Council member:{" "}
-        <Link to={`/councils`}>
-          {onCouncil > 1 ? `${onCouncil} times` : "once"}
-        </Link>
-      </div>
-      <div>
-        Voted on proposals:{" "}
-        <Link to={`/councils`}>{votes > 1 ? `${votes} times` : "once"}</Link>
+      <Back target="/members" />
+      <div className="box">
+        {isCouncilMember && <div>council member</div>}
+        <a href={`${domain}/#/members/${handle}`}>
+          <h1>{handle}</h1>
+        </a>
+
+        <Summary
+          councils={councils}
+          handle={handle}
+          member={member}
+          posts={posts}
+          proposals={proposals}
+          startTime={now - block * 6000}
+        />
       </div>
     </div>
   );
 };
 
-// <div>Account: {member.account}</div>
-
 export default MemberBox;

+ 5 - 6
src/components/Members/MemberBox.tsx

@@ -1,9 +1,8 @@
 import React from "react";
 import { OverlayTrigger, Tooltip } from "react-bootstrap";
-//import { Link } from "react-router-dom";
+import { Link } from "react-router-dom";
 import { Member, Post, ProposalDetail } from "../../types";
-import { domain } from "../../config";
-import MemberOverlay from "./Member";
+import MemberOverlay from "./MemberOverlay";
 
 const MemberBox = (props: {
   councils: number[][];
@@ -33,9 +32,9 @@ const MemberBox = (props: {
         </Tooltip>
       }
     >
-      <div className="box">
-        <a href={`${domain}/#/members/${handle}`}>{handle}</a>
-      </div>
+      <Link className="box" to={`/members/${handle}`}>
+        {handle}
+      </Link>
     </OverlayTrigger>
   );
 };

+ 42 - 0
src/components/Members/MemberOverlay.tsx

@@ -0,0 +1,42 @@
+import React from "react";
+import { Member, Post, ProposalDetail } from "../../types";
+import { domain } from "../../config";
+import Summary from "./Summary";
+import NotFound from "./NotFound";
+
+const MemberBox = (props: {
+  handle: string;
+  members: Member[];
+  councils: number[][];
+  proposals: ProposalDetail[];
+  posts: Post[];
+  startTime: number;
+}) => {
+  const { councils, handle, members, posts, proposals, startTime } = props;
+  const member = members.find((m) => m.handle === handle);
+  if (!member) return <NotFound />;
+
+  const council = councils[councils.length - 1];
+  if (!council) return <div>Loading..</div>;
+  const isCouncilMember = council.includes(member.id);
+
+  return (
+    <div className="box">
+      {isCouncilMember && <div>council member</div>}
+      <a href={`${domain}/#/members/${handle}`}>
+        <h1>{handle}</h1>
+      </a>
+
+      <Summary
+        councils={councils}
+        handle={handle}
+        member={member}
+        posts={posts}
+        proposals={proposals}
+        startTime={startTime}
+      />
+    </div>
+  );
+};
+
+export default MemberBox;

+ 13 - 0
src/components/Members/NotFound.tsx

@@ -0,0 +1,13 @@
+import React from "react";
+import { Link } from "react-router-dom";
+
+const NotFound = () => {
+  return (
+    <div className="box">
+      <div> Member not found</div>
+      <Link to={`/members`}>Back</Link>
+    </div>
+  );
+};
+
+export default NotFound;

+ 16 - 0
src/components/Members/Summary/About.tsx

@@ -0,0 +1,16 @@
+import React from "react";
+
+const About = (props: { about: string }) => {
+  const { about } = props;
+  const maxWidth = "320px"; // TODO OPTIMIZE
+
+  if (about === ``) return <div />;
+
+  return (
+    <div className="mt-3" style={{ maxWidth }}>
+      {about}
+    </div>
+  );
+};
+
+export default About;

+ 23 - 0
src/components/Members/Summary/Councils.tsx

@@ -0,0 +1,23 @@
+import React from "react";
+import { Link } from "react-router-dom";
+
+const Councils = (props: { onCouncil: number; votes: number }) => {
+  const { onCouncil, votes } = props;
+  if (!onCouncil) return <span />;
+
+  const councils = onCouncil > 1 ? `${onCouncil} times` : "once";
+  const voted = votes > 1 ? `${votes} proposals` : "one proposal";
+
+  return (
+    <div>
+      <div>
+        Council member: <Link to={`/councils`}>{councils}</Link>
+      </div>
+      <div>
+        Voted on <Link to={`/councils`}>{voted}</Link>.
+      </div>
+    </div>
+  );
+};
+
+export default Councils;

+ 17 - 0
src/components/Members/Summary/Posts.tsx

@@ -0,0 +1,17 @@
+import React from "react";
+import { Link } from "react-router-dom";
+
+const Posts = (props: { posts: number }) => {
+  const { posts } = props;
+  if (!posts) return <div />;
+
+  const post = posts > 1 ? `posts` : `post`;
+
+  return (
+    <div>
+      Wrote <Link to={`/forum`}>{posts} forum {post}</Link>.
+    </div>
+  );
+};
+
+export default Posts;

+ 27 - 0
src/components/Members/Summary/Proposals.tsx

@@ -0,0 +1,27 @@
+import React from "react";
+import { Link } from "react-router-dom";
+
+const Proposals = (props: {
+  proposals: number;
+  approved: number;
+  pending: number;
+}) => {
+  const { approved, pending, proposals } = props;
+  if (!proposals) return <div />;
+
+  const count = proposals > 1 ? `${proposals} proposals` : `one proposal`;
+
+  return (
+    <div>
+      Authored <Link to={`/proposals`}>{count}</Link>
+      {approved
+        ? `, ${approved} approved (${Math.round(
+            (100 * approved) / proposals
+          )}%)`
+        : null}
+      {pending ? `, ${pending} pending` : null}.
+    </div>
+  );
+};
+
+export default Proposals;

+ 63 - 0
src/components/Members/Summary/index.tsx

@@ -0,0 +1,63 @@
+import React from "react";
+import { Member, Post, ProposalDetail } from "../../../types";
+
+import About from "./About";
+import Posts from "./Posts";
+import Councils from "./Councils";
+import Proposals from "./Proposals";
+
+import moment from "moment";
+
+interface ProposalVote {
+  proposal: ProposalDetail;
+  vote: string;
+}
+
+const Summary = (props: {
+  councils: number[][];
+  handle: string;
+  member: Member;
+  posts: Post[];
+  proposals: ProposalDetail[];
+  startTime: number;
+}) => {
+  const { councils, handle, member, proposals, startTime } = props;
+
+  const onCouncil = councils.filter((c) => c.includes(member.id));
+
+  let votes: ProposalVote[] = [];
+  proposals.forEach((p) => {
+    if (!p || !p.votesByMemberId) return;
+    const vote = p.votesByMemberId.find((v) => v.memberId === member.id);
+    if (vote && vote.vote !== ``) votes.push({ proposal: p, vote: vote.vote });
+  });
+  const createdProposals = proposals.filter((p) => p && p.author === handle);
+  const approved = createdProposals.filter((p) => p.result === "Approved");
+  const pending = createdProposals.filter((p) => p.result === "Pending");
+
+  const posts = props.posts.filter((p) => p.authorId === member.account);
+
+  const time = startTime + member.registeredAt * 6000;
+  const date = moment(time);
+  const created = date.isValid()
+    ? date.format("DD/MM/YYYY HH:mm")
+    : member.registeredAt;
+
+  return (
+    <div className="text-left">
+      <div className="my-1">
+        Registered on {created} (id {member.id})
+      </div>
+
+      <Councils onCouncil={onCouncil.length} votes={votes.length} />
+      <Proposals
+        proposals={createdProposals.length}
+        approved={approved.length}
+        pending={pending.length}
+      />
+      <Posts posts={posts.length} />
+      <About about={member.about} />
+    </div>
+  );
+};
+export default Summary;

+ 30 - 62
src/components/Members/index.tsx

@@ -1,5 +1,4 @@
 import React from "react";
-//import { Link } from "react-router-dom";
 import { Handles, Member, Post, ProposalDetail } from "../../types";
 import MemberBox from "./MemberBox";
 import Loading from "../Loading";
@@ -31,76 +30,45 @@ class Members extends React.Component<IProps, IState> {
 
   render() {
     const { councils, handles, members, posts, proposals } = this.props;
-    let uniqueMembers: Member[] = [];
+    let unique: Member[] = [];
     members.forEach(
-      (m) =>
-        uniqueMembers.find((member) => member.id === m.id) ||
-        uniqueMembers.push(m)
+      (m) => unique.find((member) => member.id === m.id) || unique.push(m)
     );
-    uniqueMembers = uniqueMembers.sort((a, b) => Number(a.id) - Number(b.id));
-
-    if (!uniqueMembers.length) return <Loading />;
-
-    const third = members.length / 3;
-    const row1 = uniqueMembers.slice(0, third + 1);
-    const row2 = uniqueMembers.slice(third + 1, 2 * third + 1);
-    const row3 = uniqueMembers.slice(2 * third + 1, members.length);
+    unique = unique.sort((a, b) => +a.id - +b.id);
+    if (!unique.length) return <Loading />;
 
     const startTime = this.props.now - this.props.block * 6000;
+    const quart = Math.floor(unique.length / 4) + 1;
+    const cols = [
+      unique.slice(0, quart),
+      unique.slice(quart, 2 * quart),
+      unique.slice(2 * quart, 3 * quart),
+      unique.slice(3 * quart),
+    ];
 
     return (
       <div>
         <Back />
         <h1 className="text-center text-white">Joystream Members</h1>
-        <div className="d-flex flew-row justify-content-around">
-          <div className="col-4 d-flex flex-column">
-            {row1.map((m) => (
-              <MemberBox
-                key={String(m.account)}
-                id={Number(m.id)}
-                account={String(m.account)}
-                handle={m.handle || handles[String(m.account)]}
-                members={members}
-                councils={councils}
-                proposals={proposals}
-                placement={"right"}
-                posts={posts}
-                startTime={startTime}
-              />
-            ))}
-          </div>
-          <div className="col-4 d-flex flex-column">
-            {row2.map((m) => (
-              <MemberBox
-                key={String(m.account)}
-                id={Number(m.id)}
-                account={String(m.account)}
-                handle={m.handle || handles[String(m.account)]}
-                members={members}
-                councils={councils}
-                placement={"bottom"}
-                proposals={proposals}
-                posts={posts}
-                startTime={startTime}
-              />
-            ))}
-          </div>
-          <div className="col-4 d-flex flex-column">
-            {row3.map((m) => (
-              <MemberBox
-                key={String(m.account)}
-                id={Number(m.id)}
-                account={String(m.account)}
-                handle={m.handle || handles[String(m.account)]}
-                members={members}
-                councils={councils}
-                placement={"left"}
-                proposals={proposals}
-                posts={posts}
-                startTime={startTime}
-              />
-            ))}
-          </div>
+        <div className="d-flex flew-row justify-content-inbetween">
+          {cols.map((col, index: number) => (
+            <div key={`col-${index}`} className="d-flex flex-column col-3 p-0">
+              {col.map((m) => (
+                <MemberBox
+                  key={String(m.account)}
+                  id={Number(m.id)}
+                  account={String(m.account)}
+                  handle={m.handle || handles[String(m.account)]}
+                  members={members}
+                  councils={councils}
+                  proposals={proposals}
+                  placement={index === 3 ? "left" : "bottom"}
+                  posts={posts}
+                  startTime={startTime}
+                />
+              ))}
+            </div>
+          ))}
         </div>
       </div>
     );

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

@@ -28,6 +28,10 @@ const Routes = (props: IState) => {
       <Route path="/councils" render={() => <Councils {...props} />} />
       <Route path="/forum" render={() => <Forum {...props} />} />
       <Route path="/mint" render={() => <Mint {...props} />} />
+      <Route
+        path="/members/:handle"
+        render={(routeprops) => <Member {...routeprops} {...props} />}
+      />
       <Route path="/members" render={() => <Members {...props} />} />{" "}
       <Route path="/" render={() => <Dashboard {...props} />} />
     </Switch>

+ 3 - 3
src/components/User/index.tsx

@@ -1,6 +1,6 @@
 import React from "react";
+import { Link } from "react-router-dom";
 import { OverlayTrigger, Tooltip } from "react-bootstrap";
-import { domain } from "../../config";
 
 const shortName = (name: string) => {
   return `${name.slice(0, 5)}..${name.slice(+name.length - 5)}`;
@@ -8,14 +8,14 @@ const shortName = (name: string) => {
 
 const User = (props: { id: string; handle?: string }) => {
   const { id, handle } = props;
-  const href = `${domain}/#/members/${handle || ``}`;
+
   return (
     <OverlayTrigger
       placement="bottom"
       overlay={<Tooltip id={id}>{id}</Tooltip>}
     >
       <div className="user mx-1">
-        <a href={href}>{handle ? handle : shortName(id)}</a>
+        <Link to={`/members/${handle || id}`}>{handle || shortName(id)}</Link>
       </div>
     </OverlayTrigger>
   );

+ 2 - 2
src/types.ts

@@ -143,9 +143,9 @@ export interface Thread {
 }
 
 export interface Member {
-  account: AccountId | string;
+  account: string;
   handle: string;
-  id: MemberId | number;
+  id: number;
   registeredAt: number;
   about: string;
 }