Joystream Stats 3 år sedan
förälder
incheckning
b3e41016e5

+ 3 - 0
package.json

@@ -12,11 +12,14 @@
     "@types/react-dom": "^16.9.8",
     "axios": "^0.21.1",
     "bootstrap": "^4.5.3",
+    "d3-timeline": "^1.0.1",
+    "d3-timeline-chart": "^1.3.0",
     "htmr": "^0.9.2",
     "react": "^17.0.1",
     "react-bootstrap": "^1.4.0",
     "react-dom": "^17.0.1",
     "react-feather": "^2.0.9",
+    "react-horizontal-timeline": "^1.5.3",
     "react-markdown": "^5.0.3",
     "react-router": "^5.2.0",
     "react-router-dom": "^5.2.0",

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

@@ -3,9 +3,21 @@ import { Link } from "react-router-dom";
 import { ActiveProposals, Council } from "..";
 import Validators from "../Validators";
 import { IState } from "../../types";
+import moment from "moment";
 
 const Dashboard = (props: IState) => {
   const { block, now, domain, handles, members, proposals, tokenomics } = props;
+  const start = now - block * 6000;
+
+  let times = [];
+  proposals.map(
+    (p) =>
+      p &&
+      times.push({
+        starting_time: moment(start + p.createdAt * 6000).valueOf(),
+        display: "circle",
+      })
+  );
 
   return (
     <div className="w-100 flex-grow-1 d-flex align-items-center justify-content-center d-flex flex-column">

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

@@ -4,6 +4,7 @@ import NavBar from "./NavBar";
 import Content from "./Content";
 
 interface IProps {
+  match?: { params: { thread: string } };
   block: number;
   posts: Post[];
   categories: Category[];
@@ -28,6 +29,9 @@ class Forum extends React.Component<IProps, IState> {
 
   componentDidMount() {
     document.addEventListener("keydown", this.handleKeyDown);
+    const { match } = this.props;
+    if (match && match.params.thread)
+      match.params.thread && this.selectThread(parseInt(match.params.thread));
   }
 
   handleKeyDown(event: { key: string }) {

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

@@ -8,6 +8,7 @@ import {
   Mint,
   Proposals,
   Proposal,
+  Timeline,
   Tokenomics,
 } from "..";
 import { IState } from "../../types";
@@ -26,13 +27,18 @@ const Routes = (props: IState) => {
       />
       <Route path="/proposals" render={() => <Proposals {...props} />} />
       <Route path="/councils" render={() => <Councils {...props} />} />
+      <Route
+        path="/forum/threads/:thread"
+        render={(routeprops) => <Forum {...routeprops} {...props} />}
+      />
       <Route path="/forum" render={() => <Forum {...props} />} />
       <Route path="/mint" render={() => <Mint {...props} />} />
       <Route
         path="/members/:handle"
         render={(routeprops) => <Member {...routeprops} {...props} />}
       />
-      <Route path="/members" render={() => <Members {...props} />} />{" "}
+      <Route path="/members" render={() => <Members {...props} />} />
+      <Route path="/timeline" render={() => <Timeline {...props} />} />
       <Route path="/" render={() => <Dashboard {...props} />} />
     </Switch>
   );

+ 33 - 0
src/components/Timeline/Item.tsx

@@ -0,0 +1,33 @@
+import React from "react";
+import { Link } from "react-router-dom";
+import moment from "moment";
+import { Event } from "../../types";
+
+const TimelineItem = (props: { event: Event; startTime: number }) => {
+  const { category, date, text, link } = props.event;
+  const time = moment().format;
+
+  const formatTime = (time: number) => {
+    return moment(time).format("DD/MM/YYYY HH:mm");
+  };
+
+  const created = formatTime(props.startTime + date * 6000);
+
+  return (
+    <div className="timeline-item">
+      <div className="timeline-item-content">
+        <span className="tag" style={{ background: category.color }}>
+          {category.tag}
+        </span>
+        <time>{created}</time>
+        <p>{text}</p>
+
+        <Link to={link.url}>{link.text}</Link>
+
+        <span className="circle" />
+      </div>
+    </div>
+  );
+};
+
+export default TimelineItem;

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

@@ -0,0 +1,62 @@
+import React from "react";
+import TimelineItem from "./Item";
+import { Event, Post, ProposalDetail } from "../../types";
+
+const Timeline = (props: {
+  posts: Post[];
+  proposals: ProposalDetail[];
+  block: number;
+  now: number;
+}) => {
+  const { block, now, posts, proposals } = props;
+  const startTime: number = now - block * 6000;
+  let events: Event[] = [];
+
+  proposals.forEach(
+    (p) =>
+      p &&
+      events.push({
+        text: p.title,
+        date: p.createdAt,
+        category: {
+          tag: "Proposal",
+          color: p.result === `Approved` ? `green` : `red`,
+        },
+        link: {
+          url: `/proposals/${p.id}`,
+          text: `Proposal ${p.id}`,
+        },
+      })
+  );
+
+  posts.forEach(
+    (p) =>
+      p &&
+      events.push({
+        text: p.text.slice(0, 100),
+        date: p.createdAt.block,
+        category: {
+          tag: "Forum Post",
+          color: `blue`,
+        },
+        link: {
+          url: `/forum/threads/${p.threadId}`,
+          text: `Post ${p.id}`,
+        },
+      })
+  );
+
+  if (!events.length) return <div />;
+
+  return (
+    <div className="timeline-container">
+      {events
+        .sort((a, b) => b.date - a.date)
+        .map((event: Event, idx) => (
+          <TimelineItem event={event} key={idx} startTime={startTime} />
+        ))}
+    </div>
+  );
+};
+
+export default Timeline;

+ 2 - 1
src/components/index.ts

@@ -14,4 +14,5 @@ export { default as User } from "./User";
 export { default as Member } from "./Members/Member";
 export { default as Members } from "./Members";
 export { default as Tokenomics } from "./Tokenomics";
-export { default as Validators } from "./Validators";
+export { default as Validators } from "./Validators";
+export { default as Timeline } from "./Timeline";

+ 158 - 0
src/index.css

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

+ 13 - 0
src/types.ts

@@ -234,3 +234,16 @@ export interface Exchange {
   status: string; // FINALIZED | PENDING
   xmrAddress: string; //"No address found"
 }
+
+export interface Event {
+  text: string;
+  date: number;
+  category: {
+    tag: string;
+    color: string;
+  };
+  link: {
+    url: string;
+    text: string;
+  };
+}