Browse Source

Migrate to Semantic UI, Minor improvements

develop
Apostolos Fanakis 7 years ago
parent
commit
15d7110c2d
  1. 1
      package.json
  2. 8
      public/index.html
  3. 203
      src/assets/css/App.css
  4. 30
      src/assets/css/board-container.css
  5. 17
      src/assets/css/loading-container.css
  6. 61
      src/assets/css/materialTabs.css
  7. 7
      src/assets/css/navbar.css
  8. 5
      src/assets/css/profile-container.css
  9. 12
      src/assets/css/sign-up-container.css
  10. 10
      src/assets/css/start-topic-container.css
  11. 46
      src/assets/css/topic-container.css
  12. 20
      src/components/FloatingButton.js
  13. 19
      src/components/HeaderBar.js
  14. 4
      src/components/LoadingSpinner.js
  15. 66
      src/components/NavBar.js
  16. 86
      src/components/NewPost.js
  17. 65
      src/components/NewTopicPreview.js
  18. 66
      src/components/Post.js
  19. 34
      src/components/Topic.js
  20. 21
      src/containers/BoardContainer.js
  21. 6
      src/containers/HomeContainer.js
  22. 6
      src/containers/LoadingContainer.js
  23. 96
      src/containers/ProfileContainer.js
  24. 16
      src/containers/SignUpContainer.js
  25. 57
      src/containers/StartTopicContainer.js
  26. 5
      src/containers/TopicContainer.js
  27. 69
      src/containers/UsernameFormContainer.js
  28. 18
      src/layouts/CoreLayout/CoreLayout.js
  29. 16
      src/redux/store.js

1
package.json

@ -27,6 +27,7 @@
"react-user-avatar": "^1.10.0", "react-user-avatar": "^1.10.0",
"redux": "^3.7.2", "redux": "^3.7.2",
"redux-saga": "0.16.0", "redux-saga": "0.16.0",
"semantic-ui-react": "^0.81.1",
"uuid": "^3.2.1", "uuid": "^3.2.1",
"web3": "^1.0.0-beta.34" "web3": "^1.0.0-beta.34"
}, },

8
public/index.html

@ -20,13 +20,9 @@
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<!-- Import Google Icon Font --> <!-- Import Google Icon Font -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <!-- <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> -->
<!-- Compiled and minified CSS --> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.1/semantic.min.css"></link>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/css/materialize.min.css">
<!-- Compiled and minified JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/js/materialize.min.js"></script>
<title>Apella</title> <title>Apella</title>
</head> </head>
<body> <body>

203
src/assets/css/App.css

@ -14,167 +14,89 @@ strong {
font-weight: bold !important; font-weight: bold !important;
} }
#root {
height: 100%;
}
.App { .App {
width: 100%; width: 100%;
height: 100%;
margin: 0px; margin: 0px;
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
align-items: flex-start; align-items: flex-start;
} }
.view-container { .page-container {
width: 60%; width: 100%;
margin: 10px auto;
}
/* LOADING SCREEN */
.loading-screen {
opacity: 1;
visibility: visible;
transition: all .25s ease-in-out;
}
.loading-screen.loaded {
opacity: 0;
visibility: hidden;
}
/* HEADER BAR */
.header-bar {
max-height: 100px;
max-width: 100%;
padding: 7px 0px;
background: #0c1a2b;
color: white;
align-self: center;
text-align: center;
}
.logo {
height: 100%; height: 100%;
margin: 71px 0px 0px;
} }
.header-bar-text { .left-side-panel {
height: 100%; margin-top: 71px;
margin-left: 8px; position: fixed;
width: 20%;
height: calc(100% - 71px);
top: 0;
left: 0;
} }
.main-panel {
.header-bar-text div { width: 60%;
height: 100%; height: 100%;
display: table; margin: 0px 20%;
}
.header-bar-text div>div {
display: table-cell;
vertical-align: middle;
} }
/* NAVBAR */ .right-side-panel {
margin-top: 71px;
.navColor {
background: #0c1a2b;
}
.stick {
z-index: 2;
position: fixed; position: fixed;
width: 20%;
height: calc(100% - 71px);
top: 0; top: 0;
width: 100%; right: 0;
}
/* TOPICS */
.topic {
width: 100%;
margin: 18px 0px;
}
.topic-subject {
margin: 0px 0px 5px;
}
.topic-meta {
margin: 5px 0px 0px;
}
.topic-date {
margin-bottom: 0px;
font-size: 0.82vw !important;
text-align: right;
}
/* START TOPIC */
.topic-form {
width: 100%;
margin: 20px 0px;
} }
.topic-form input[type=text], textarea { .view-container {
width: 100%; width: 100%;
padding: 12px 20px; height: 100%;
margin: 8px 0; margin: 0px auto;
display: inline-block;
border: 1px solid #ccc;
border-radius: 4px;
resize: vertical;
box-sizing: border-box;
} }
.topic-form textarea { /* MISC */
min-height: 200px;
}
.form-input-required { .form-textarea-required {
border-color: red !important; color: rgb(159, 58, 56) !important;
outline-color: rgb(159, 58, 56) !important;
border-color: rgb(224, 180, 180) !important;
background-color: rgb(255, 246, 246) !important;
} }
.markdownPreview { .card {
padding: 0px 20px; width: 100% !important;
} }
/* POSTS */ .bottom-overlay-pad {
background: rgba(255, 255, 255, 0.85);
.post { z-index: 10;
width: 100%; position: fixed;
background-color: #FFFFFF; bottom: 0px;
margin: 20px 0px; height: 62px;
width: 60%;
margin: 0px;
padding: 0px; padding: 0px;
} }
.post-meta { .action-button {
float: right; z-index: 11;
margin-right: 11.25px; position: fixed;
} bottom: 10px;
left: calc(50% - 24px);
.user-avatar {
width: 52px;
height: 52px;
text-align: center;
}
.stretch-space-between {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
}
.user-info {
background-color: #FFFFFF;
margin: 12px auto;
padding: 7px;
} }
/* PROFILE */ .grey-text {
color: grey;
.profile-tab {
width: 100%;
} }
/* MISC */
.inline { .inline {
display: inline-block; display: inline-block;
} }
@ -192,7 +114,9 @@ hr {
margin: 0px; margin: 0px;
} }
*:focus {outline:none !important} *:focus {
outline:none !important
}
.centerDiv { .centerDiv {
margin: 0 auto; margin: 0 auto;
@ -203,20 +127,24 @@ a {
text-decoration: none; text-decoration: none;
} }
.post-content a{
color: #039be5;
}
.center-in-parent { .center-in-parent {
width: 100%; width: 100%;
height: 100%;
text-align: center; text-align: center;
} }
.vertical-center-in-parent {
vertical-align: middle;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.vertical-center-children { .vertical-center-children {
height: 100%;
display: flex; display: flex;
flex-flow: row nowrap; flex-direction: column;
align-items: flex-start; justify-content: center;
} }
#overlay { #overlay {
@ -242,6 +170,11 @@ a {
-ms-transform: translate(-50%,-50%); -ms-transform: translate(-50%,-50%);
} }
.fill {
width: 100%;
height: 100%;
}
.full-width { .full-width {
max-width: 100% !important; max-width: 100% !important;
width: 100% !important; width: 100% !important;

30
src/assets/css/board-container.css

@ -0,0 +1,30 @@
/* TOPICS LIST SCREEN */
.topics-list {
padding: 0px 2px;
margin-bottom: 75px;
}
.topics-list a {
color: black !important;
text-decoration: none !important;
}
.topics-list a:hover {
color: black !important;
text-decoration: none !important;
}
.topic-subject {
margin: 0px 0px 5px;
}
.topic-meta {
margin: 5px 0px 0px;
}
.topic-date {
margin-bottom: 0px;
font-size: 0.77vw !important;
text-align: right;
}

17
src/assets/css/loading-container.css

@ -0,0 +1,17 @@
/* LOADING SCREEN */
.loading-screen {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
opacity: 1;
visibility: visible;
transition: all .25s ease-in-out;
}
.loading-screen.loaded {
opacity: 0;
visibility: hidden;
}

61
src/assets/css/materialTabs.css

@ -1,61 +0,0 @@
header {
position: relative;
}
.hide {
display: none;
}
.show {
display: block;
}
.tab-content {
padding:25px;
}
#material-tabs {
position: relative;
display: block;
padding:0;
}
#material-tabs>a {
position: relative;
display:inline-block;
text-decoration: none;
padding: 22px;
text-transform: uppercase;
font-size: 14px;
font-weight: 600;
color: #0c1a2b;
text-align: center;
outline:;
}
#material-tabs>a.active {
font-weight: 700;
}
#material-tabs>a:not(.active):hover {
background-color: inherit;
color: #7c848a;
}
@media only screen and (max-width: 520px) {
.nav-tabs#material-tabs>li>a {
font-size: 11px;
}
}
.underline-bar {
position: absolute;
z-index: 10;
bottom: 0;
height: 3px;
background: #0c1a2b;
display: block;
left: 0;
transition: left .2s ease;
-webkit-transition: left .2s ease;
}

7
src/assets/css/navbar.css

@ -0,0 +1,7 @@
/* NAVBAR */
.nav-link:hover {
background: rgba(255,255,255,.08) !important;
color: #fff !important;
cursor: pointer !important;
}

5
src/assets/css/profile-container.css

@ -0,0 +1,5 @@
/* PROFILE SCREEN */
.profile-tab {
width: 100%;
}

12
src/assets/css/sign-up-container.css

@ -0,0 +1,12 @@
/* SIGN UP SCREEN */
.sign-up-container {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.sign-up-container>div {
margin: auto;
}

10
src/assets/css/start-topic-container.css

@ -0,0 +1,10 @@
/* START TOPIC SCREEN */
.topic-form {
width: 100%;
margin: 20px 0px;
}
.markdownPreview {
padding: 0px 20px;
}

46
src/assets/css/topic-container.css

@ -0,0 +1,46 @@
/* POSTS LIST SCREEN */
.post {
width: 100%;
background-color: #FFFFFF;
margin: 20px 0px;
padding: 0px;
}
.post-meta {
float: right;
margin-right: 11.25px;
}
.user-avatar {
width: 52px;
height: 52px;
text-align: center;
}
.user-avatar a {
color: inherit !important;
text-decoration: none !important;
}
.stretch-space-between {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
}
.user-info {
background-color: #FFFFFF;
margin: 12px auto;
padding: 7px;
}
.post-content a{
margin-top: 10px;
color: #039be5;
}
.post-form {
width: 100%;
margin: 20px 0px;
}

20
src/components/FloatingButton.js

@ -1,23 +1,13 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { Button, Icon } from 'semantic-ui-react'
const FloatingButton = (props) => { const FloatingButton = (props) => {
return ( return (
<div> <div className="action-button" onClick={props.onClick}>
{props.to <Button icon color='teal' size='large'>
?<div className="fixed-action-btn"> <Icon name='add'/>
<Link className="btn-floating btn-large waves-effect waves-light teal lighten-1" </Button>
to={props.to}>
<i className="large material-icons">add</i>
</Link>
</div>
:<div className="fixed-action-btn">
<p className="btn-floating btn-large waves-effect waves-light teal lighten-1"
onClick={props.onClick}>
<i className="large material-icons">add</i>
</p>
</div>
}
</div> </div>
); );
}; };

19
src/components/HeaderBar.js

@ -1,19 +0,0 @@
import React from 'react';
const HeaderBar = (props) => {
return (
<div className="header-bar">
<img className="logo" src={require('../resources/logo.png')} alt="logo"/>
<div className="inline header-bar-text">
<div>
<div>
<h1 className="no-margin">Welcome to Apella</h1>
<p className="no-margin">A decentralized forum</p>
</div>
</div>
</div>
</div>
);
};
export default HeaderBar;

4
src/components/LoadingSpinner.js

@ -2,12 +2,14 @@ import React from 'react';
const LoadingSpinner = (props) => { const LoadingSpinner = (props) => {
return( return(
<div className={"center-in-parent" + (props.className ? props.className : "")} <div className="vertical-center-children">
<div className={"center-in-parent " + (props.className ? props.className : "")}
style={props.style ? props.style : []}> style={props.style ? props.style : []}>
<p> <p>
<i className="fas fa-spinner fa-3x fa-spin"></i> <i className="fas fa-spinner fa-3x fa-spin"></i>
</p> </p>
</div> </div>
</div>
); );
} }

66
src/components/NavBar.js

@ -1,51 +1,53 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { drizzleConnect } from 'drizzle-react'; import { drizzleConnect } from 'drizzle-react';
import { Link } from 'react-router'; import PropTypes from 'prop-types';
import { Image, Menu } from 'semantic-ui-react'
class NavBar extends Component { class NavBar extends Component {
constructor(props){ constructor(props){
super(props); super(props);
this.handleScroll = this.handleScroll.bind(this); this.handleItemClick = this.handleItemClick.bind(this);
this.navRef = React.createRef(); this.navRef = React.createRef();
} }
handleItemClick(to) {
this.context.router.push(to);
}
render() { render() {
return ( return (
<nav ref={this.navRef}> <Menu fixed='top' inverted>
<div className="nav-wrapper navColor"> <Menu.Item header onClick={() => {this.handleItemClick("/")}}>
<ul id="nav-mobile" className="left hide-on-med-and-down"> <Image
<li> size='mini'
<Link to="/">Home</Link> src={require('../resources/logo.png')}
</li> style={{ marginRight: '1.5em' }}
{this.props.hasSignedUp && />
<li> Apella
<Link to="/profile">Profile</Link> </Menu.Item>
</li> <Menu.Item onClick={() => {this.handleItemClick("/")}}>
} Home
</ul> </Menu.Item>
</div> {this.props.hasSignedUp
</nav> ? <Menu.Item onClick={() => {this.handleItemClick("/profile")}}>
Profile
</Menu.Item>
:<Menu.Menu position='right'>
<Menu.Item onClick={() => {this.handleItemClick("/signUp")}}>
Sign Up
</Menu.Item>
</Menu.Menu>
}
</Menu>
); );
} }
};
componentDidMount(){ NavBar.contextTypes = {
this.headerHeight = this.navRef.current.offsetTop; router: PropTypes.object
window.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount(){
window.removeEventListener('scroll', this.handleScroll);
}
handleScroll(){
if (window.pageYOffset >= this.headerHeight) {
this.navRef.current.classList.add("stick")
} else {
this.navRef.current.classList.remove("stick");
}
}
}; };
const mapStateToProps = state => { const mapStateToProps = state => {

86
src/components/NewPost.js

@ -3,6 +3,8 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import uuidv4 from 'uuid/v4'; import uuidv4 from 'uuid/v4';
import { Grid, Form, TextArea, Button, Icon, Divider } from 'semantic-ui-react'
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
import UserAvatar from 'react-user-avatar'; import UserAvatar from 'react-user-avatar';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
@ -20,7 +22,6 @@ class NewPost extends Component {
this.pushToDatabase = this.pushToDatabase.bind(this); this.pushToDatabase = this.pushToDatabase.bind(this);
this.newPostOuterRef = React.createRef(); this.newPostOuterRef = React.createRef();
this.subjectInputRef = React.createRef();
this.transactionProgressText = []; this.transactionProgressText = [];
this.drizzle = context.drizzle; this.drizzle = context.drizzle;
@ -97,76 +98,91 @@ class NewPost extends Component {
</div> </div>
</div> </div>
} }
<div className="row"> <Divider horizontal>
<div className="col s1"> <span className="grey-text">#{this.props.postIndex}</span>
</Divider>
<Grid>
<Grid.Row columns={16} stretched>
<Grid.Column width={1} className="user-avatar">
<UserAvatar <UserAvatar
size="40" size="52"
className="inline user-avatar" className="inline user-avatar"
src={this.props.avatarUrl} src={this.props.avatarUrl}
name={this.props.user.username} name={this.props.user.username}
/> />
</div> </Grid.Column>
<div className="col s11"> <Grid.Column width={15}>
<div> <div className="">
<div className="stretch-space-between"> <div className="stretch-space-between">
<strong><span>{this.props.user.username}</span></strong> <span><strong>{this.props.user.username}</strong></span>
<span className="grey-text text-darken-2"> <span className="grey-text">
{this.state.previewEnabled && {this.state.previewEnabled &&
<TimeAgo date={this.state.previewDate}/> <TimeAgo date={this.state.previewDate}/>
}{this.state.previewEnabled && ","} #{this.props.postIndex} }
</span> </span>
</div> </div>
<div className="stretch-space-between"> <div className="stretch-space-between">
<strong><span> <span><strong>
{this.state.previewEnabled && {this.state.previewEnabled &&
("Subject: " + this.state.postSubjectInput) ("Subject: " + this.state.postSubjectInput)
} }
</span></strong> </strong></span>
</div> </div>
<div> <div className="post-content">
<form className="topic-form"> <Form className="topic-form">
{this.state.previewEnabled {this.state.previewEnabled
? <ReactMarkdown source={this.state.postContentInput} ? <ReactMarkdown source={this.state.postContentInput}
className="markdownPreview" /> className="markdownPreview" />
: [ : [
<input key={"postSubjectInput"} <Form.Input key={"postSubjectInput"}
name={"postSubjectInput"} name={"postSubjectInput"}
className={this.state.postSubjectInputEmptySubmit ? "form-input-required" : ""} error={this.state.postSubjectInputEmptySubmit}
type="text" type="text"
value={this.state.postSubjectInput} value={this.state.postSubjectInput}
placeholder="Subject" placeholder="Subject"
id="postSubjectInput" id="postSubjectInput"
ref={this.subjectInputRef}
onChange={this.handleInputChange} />, onChange={this.handleInputChange} />,
<textarea key={"postContentInput"} <TextArea key={"postContentInput"}
name={"postContentInput"} name={"postContentInput"}
className={this.state.postContentInputEmptySubmit ? "form-textarea-required" : ""}
value={this.state.postContentInput} value={this.state.postContentInput}
placeholder="Post" placeholder="Post"
id="postContentInput" id="postContentInput"
onChange={this.handleInputChange} /> onChange={this.handleInputChange}
rows={4} autoHeight />
]} ]}
<button key="submit" <br/><br/>
<Button.Group>
<Button key="submit"
className="btn waves-effect waves-teal white black-text" className="btn waves-effect waves-teal white black-text"
type="button" type="button"
onClick={this.validateAndPost}> onClick={this.validateAndPost}
<i className="material-icons right">send</i>Post color='teal'
</button> animated>
<button className="waves-effect waves-orange btn white black-text margin-left-small" <Button.Content visible>Post</Button.Content>
<Button.Content hidden>
<Icon name='reply' />
</Button.Content>
</Button>
<Button className="waves-effect waves-orange btn white black-text margin-left-small"
type="button" type="button"
onClick={this.handlePreviewToggle}> onClick={this.handlePreviewToggle}
<span>{this.state.previewEnabled ? "Edit" : "Preview"}</span> color='yellow'>
</button> {this.state.previewEnabled ? "Edit" : "Preview"}
<button className="btn red margin-left-small" </Button>
<Button className="btn red margin-left-small"
type="button" type="button"
onClick={this.props.onCancelClick}> onClick={this.props.onCancelClick}
<span>Cancel</span> color='red'>
</button> Cancel
</form> </Button>
</div> </Button.Group>
</div> </Form>
</div> </div>
</div> </div>
<div className="divider"></div> </Grid.Column>
</Grid.Row>
</Grid>
</div> </div>
); );
} }

65
src/components/NewTopicPreview.js

@ -0,0 +1,65 @@
import React, { Component } from 'react';
import { drizzleConnect } from 'drizzle-react';
import { Grid, Divider, Button, Icon, Label } from 'semantic-ui-react'
import TimeAgo from 'react-timeago';
import UserAvatar from 'react-user-avatar';
import ReactMarkdown from 'react-markdown';
class Post extends Component {
constructor(props, context) {
super(props);
}
render(){
return (
<div className="post">
<Divider horizontal>
<span className="grey-text">#0</span>
</Divider>
<Grid>
<Grid.Row columns={16} stretched>
<Grid.Column width={1} className="user-avatar">
<UserAvatar
size="52"
className="inline"
src={this.props.user.avatarUrl}
name={this.props.user.username}/>
</Grid.Column>
<Grid.Column width={15}>
<div className="">
<div className="stretch-space-between">
<span>
<strong>
{this.props.user.username}
</strong>
</span>
<span className="grey-text">
<TimeAgo date={this.props.date}/>
</span>
</div>
<div className="stretch-space-between">
<span><strong>
Subject: {this.props.subject}
</strong></span>
</div>
<div className="post-content">
<ReactMarkdown source={this.props.content} />
</div>
</div>
</Grid.Column>
</Grid.Row>
</Grid>
</div>
);
}
};
const mapStateToProps = state => {
return {
user: state.user
}
};
export default drizzleConnect(Post, mapStateToProps);

66
src/components/Post.js

@ -3,6 +3,8 @@ import { Link, withRouter } from 'react-router';
import { drizzleConnect } from 'drizzle-react'; import { drizzleConnect } from 'drizzle-react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Grid, Divider, Button, Icon, Label } from 'semantic-ui-react'
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
import epochTimeConverter from '../helpers/EpochTimeConverter'; import epochTimeConverter from '../helpers/EpochTimeConverter';
import UserAvatar from 'react-user-avatar'; import UserAvatar from 'react-user-avatar';
@ -43,8 +45,12 @@ class Post extends Component {
return ( return (
<div className="post"> <div className="post">
<div className="row"> <Divider horizontal>
<div className="col s1 user-avatar"> <span className="grey-text">#{this.props.postIndex}</span>
</Divider>
<Grid>
<Grid.Row columns={16} stretched>
<Grid.Column width={1} className="user-avatar">
{this.props.blockchainData[0].returnData !== null {this.props.blockchainData[0].returnData !== null
?<Link to={"/profile/" + this.props.blockchainData[0].returnData[1] ?<Link to={"/profile/" + this.props.blockchainData[0].returnData[1]
+ "/" + this.props.blockchainData[0].returnData[2]} + "/" + this.props.blockchainData[0].returnData[2]}
@ -53,26 +59,30 @@ class Post extends Component {
</Link> </Link>
:avatarView :avatarView
} }
</div> </Grid.Column>
<div className="col s11"> <Grid.Column width={15}>
<div> <div className="">
<div className="stretch-space-between"> <div className="stretch-space-between">
<strong><span className={this.props.blockchainData[0].returnData !== null ? "" : "grey-text"}> <span className={this.props.blockchainData[0].returnData !== null ? "" : "grey-text"}>
<strong>
{this.props.blockchainData[0].returnData !== null {this.props.blockchainData[0].returnData !== null
?this.props.blockchainData[0].returnData[2] ?this.props.blockchainData[0].returnData[2]
:"Username" :"Username"
} }
</span></strong> </strong>
<span className="grey-text text-darken-2"> </span>
<span className="grey-text">
{this.props.blockchainData[0].returnData !== null && {this.props.blockchainData[0].returnData !== null &&
<TimeAgo date={epochTimeConverter(this.props.blockchainData[0].returnData[3])}/> <TimeAgo date={epochTimeConverter(this.props.blockchainData[0].returnData[3])}/>
}{this.props.blockchainData[0].returnData !== null && ","} #{this.props.postIndex} }
</span> </span>
</div> </div>
<div className="stretch-space-between"> <div className="stretch-space-between">
<strong><span className={this.orbitPostData.subject ? "" : "grey-text"}> <span className={this.orbitPostData.subject ? "" : "grey-text"}>
<strong>
Subject: {this.orbitPostData.subject} Subject: {this.orbitPostData.subject}
</span></strong> </strong>
</span>
</div> </div>
<div className="post-content"> <div className="post-content">
{this.orbitPostData.content {this.orbitPostData.content
@ -81,26 +91,26 @@ class Post extends Component {
} }
</div> </div>
</div> </div>
</div> </Grid.Column>
</div> </Grid.Row>
<div className="row"> <Grid.Row>
<div className="post-meta grey-text text-darken-2"> <Grid.Column floated="right" textAlign="right">
<i className="material-icons waves-effect waves-teal circle"> <Button icon size='mini' style={{marginRight: "0px"}}>
keyboard_arrow_up <Icon name='chevron up' />
</i> </Button>
<span>8</span> <Label color="teal">8000</Label>
<i className="material-icons waves-effect waves-teal circle"> <Button icon size='mini'>
keyboard_arrow_down <Icon name='chevron down' />
</i> </Button>
<i className="material-icons waves-effect waves-teal circle" <Button icon size='mini'
onClick={() => { this.context.router.push("/topic/" onClick={() => { this.context.router.push("/topic/"
+ this.props.blockchainData[0].returnData[4] + "/" + this.props.blockchainData[0].returnData[4] + "/"
+ this.props.postID)}}> + this.props.postID)}}>
link <Icon name='linkify' />
</i> </Button>
</div> </Grid.Column>
</div> </Grid.Row>
<div className="divider"></div> </Grid>
</div> </div>
); );
} }

34
src/components/Topic.js

@ -1,6 +1,9 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { drizzleConnect } from 'drizzle-react'; import { drizzleConnect } from 'drizzle-react';
import PropTypes from 'prop-types';
import { Card } from 'semantic-ui-react'
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
import epochTimeConverter from '../helpers/EpochTimeConverter' import epochTimeConverter from '../helpers/EpochTimeConverter'
@ -40,37 +43,38 @@ class Topic extends Component {
render(){ render(){
return ( return (
<Link to={"/topic/" + this.props.topicID}> <Card link className="card"
<div className="topic card white hoverable"> onClick={() => {this.context.router.push("/topic/" + this.props.topicID)}}>
<div className="card-content"> <Card.Content>
<div className={"topic-subject" + (this.topicSubject ? "" : "grey-text")}> <div className={"topic-subject" + (this.topicSubject ? "" : " grey-text")}>
<p> <p><strong>
{this.topicSubject !== null ? this.topicSubject : "Subject"} {this.topicSubject !== null ? this.topicSubject : "Subject"}
</p> </strong></p>
</div> </div>
<hr/> <hr/>
<div className="topic-meta"> <div className="topic-meta">
<p className={"no-margin" + (this.topicSubject ? "" : "grey-text")}> <p className={"no-margin" +
(this.props.blockchainData[0].returnData !== null ? "" : " grey-text")}>
{this.props.blockchainData[0].returnData !== null {this.props.blockchainData[0].returnData !== null
?this.props.blockchainData[0].returnData[2] ?this.props.blockchainData[0].returnData[2]
:"Username" :"Username"
} }
</p> </p>
<p className={"no-margin" + (this.props.blockchainData[0].returnData !== null ? "" : "grey-text")}> <p className={"no-margin" +
(this.props.blockchainData[0].returnData !== null ? "" : " grey-text")}>
{"Number of replies: " + (this.props.blockchainData[0].returnData !== null {"Number of replies: " + (this.props.blockchainData[0].returnData !== null
?this.props.blockchainData[0].returnData[4].length ?this.props.blockchainData[0].returnData[4].length
:"") :"")
} }
</p> </p>
<p className="topic-date grey-text darken-3"> <p className="topic-date grey-text">
Started {this.props.blockchainData[0].returnData !== null && {this.props.blockchainData[0].returnData !== null &&
<TimeAgo date={epochTimeConverter(this.props.blockchainData[0].returnData[3])}/> <TimeAgo date={epochTimeConverter(this.props.blockchainData[0].returnData[3])}/>
} }
</p> </p>
</div> </div>
</div> </Card.Content>
</div> </Card>
</Link>
); );
} }
@ -81,6 +85,10 @@ class Topic extends Component {
} }
}; };
Topic.contextTypes = {
router: PropTypes.object
};
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {
user: state.user, user: state.user,

21
src/containers/BoardContainer.js

@ -8,8 +8,14 @@ import FloatingButton from '../components/FloatingButton';
import LoadingSpinner from '../components/LoadingSpinner'; import LoadingSpinner from '../components/LoadingSpinner';
class Board extends Component { class Board extends Component {
constructor(props, context) { constructor(props) {
super(props); super(props);
this.handleCreateTopicClick = this.handleCreateTopicClick.bind(this);
}
handleCreateTopicClick() {
this.context.router.push("/startTopic");
} }
render() { render() {
@ -23,20 +29,25 @@ class Board extends Component {
for (var i = 0; i < this.props.blockchainData[0].returnData; i++) { for (var i = 0; i < this.props.blockchainData[0].returnData; i++) {
this.topicIDs.push(i); this.topicIDs.push(i);
} }
boardContents = <TopicList topicIDs={this.topicIDs}/> boardContents = ([
<TopicList topicIDs={this.topicIDs} key="topicList"/>,
<FloatingButton onClick={this.handleCreateTopicClick}
key="createTopicButton"/>
]);
} }
return ( return (
<div style={{marginBottom: '70px'}}> <div className="fill">
{boardContents} {boardContents}
<FloatingButton to="/startTopic"/> <div className="bottom-overlay-pad"></div>
</div> </div>
); );
} }
} }
Board.contextTypes = { Board.contextTypes = {
drizzle: PropTypes.object drizzle: PropTypes.object,
router: PropTypes.object
}; };
const mapStateToProps = state => { const mapStateToProps = state => {

6
src/containers/HomeContainer.js

@ -10,11 +10,7 @@ class Home extends Component {
? (<BoardContainer/>) //This may become multiple boards ? (<BoardContainer/>) //This may become multiple boards
: (<SignUpContainer/>); : (<SignUpContainer/>);
return ( return (view);
<div>
{view}
</div>
);
} }
} }

6
src/containers/LoadingContainer.js

@ -17,7 +17,7 @@ class LoadingContainer extends Component {
} }
return( return(
<main className="container loading-screen"> <main className="loading-screen">
<div> <div>
<div> <div>
<h1><span role="img" aria-label="Warning Sign"></span></h1> <h1><span role="img" aria-label="Warning Sign"></span></h1>
@ -31,7 +31,7 @@ class LoadingContainer extends Component {
if (this.props.web3.status === 'initialized' && Object.keys(this.props.accounts).length === 0) if (this.props.web3.status === 'initialized' && Object.keys(this.props.accounts).length === 0)
{ {
return( return(
<main className="container loading-screen"> <main className="loading-screen">
<div> <div>
<div> <div>
<h1><span role="img" aria-label="Fox Face">🦊</span></h1> <h1><span role="img" aria-label="Fox Face">🦊</span></h1>
@ -45,7 +45,7 @@ class LoadingContainer extends Component {
if (!this.props.orbitDB.ipfsInitialized) if (!this.props.orbitDB.ipfsInitialized)
{ {
return( return(
<main className="container loading-screen"> <main className="loading-screen">
<div> <div>
<div> <div>
<img src={ipfs_logo} alt="ipfs_logo" height="50"/> <img src={ipfs_logo} alt="ipfs_logo" height="50"/>

96
src/containers/ProfileContainer.js

@ -3,50 +3,27 @@ import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Tab } from 'semantic-ui-react'
import WithBlockchainData from '../components/WithBlockchainData'; import WithBlockchainData from '../components/WithBlockchainData';
import ProfileInformation from '../components/ProfileInformation'; import ProfileInformation from '../components/ProfileInformation';
import TopicList from '../components/TopicList'; import TopicList from '../components/TopicList';
import PostList from '../components/PostList'; import PostList from '../components/PostList';
import LoadingSpinner from '../components/LoadingSpinner'; import LoadingSpinner from '../components/LoadingSpinner';
import '../assets/css/materialTabs.css';
class Profile extends Component { class Profile extends Component {
constructor(props, context) { constructor(props, context) {
super(props); super(props);
this.handleTabClick = this.handleTabClick.bind(this);
this.propsToView = this.propsToView.bind(this); this.propsToView = this.propsToView.bind(this);
this.drizzle = context.drizzle; this.drizzle = context.drizzle;
this.underlineBarRef = React.createRef();
this.infoSelectorRef = React.createRef();
this.topicsSelectorRef = React.createRef();
this.postsSelectorRef = React.createRef();
this.state = { this.state = {
viewSelected: "profile-info-tab",
userAddress: this.props.params.address ? this.props.params.address : this.props.user.address userAddress: this.props.params.address ? this.props.params.address : this.props.user.address
}; };
} }
handleTabClick(event) {
this.setState({viewSelected: event.target.id});
if (event.target.id === "profile-info-tab"){
this.underlineBarRef.current.style.left = this.infoSelectorRef.current.offsetLeft + 'px';
this.underlineBarRef.current.style.width = ReactDOM.
findDOMNode(this.infoSelectorRef.current).getBoundingClientRect().width + 'px';
} else if (event.target.id === "profile-topics-tab"){
this.underlineBarRef.current.style.left = this.topicsSelectorRef.current.offsetLeft + 'px';
this.underlineBarRef.current.style.width = ReactDOM.
findDOMNode(this.topicsSelectorRef.current).getBoundingClientRect().width + 'px';
} else if (event.target.id === "profile-posts-tab"){
this.underlineBarRef.current.style.left = this.postsSelectorRef.current.offsetLeft + 'px';
this.underlineBarRef.current.style.width = ReactDOM.
findDOMNode(this.postsSelectorRef.current).getBoundingClientRect().width + 'px';
}
}
render() { render() {
this.propsToView(); this.propsToView();
var infoTab = var infoTab =
@ -86,45 +63,36 @@ class Profile extends Component {
} }
</div>); </div>);
const profilePanes = [
{
menuItem: 'INFORMATION',
pane: {
key: 'INFORMATION',
content: (infoTab),
},
},
{
menuItem: 'TOPICS',
pane: {
key: 'TOPICS',
content: (topicsTab),
},
},
{
menuItem: 'POSTS',
pane: {
key: 'POSTS',
content: (postsTab),
},
},
]
return ( return (
<div> <div>
<header> <Tab
<div id="material-tabs"> menu={{ secondary: true, pointing: true }}
<a className={this.state.viewSelected === "profile-info-tab" ? "active" : ""} panes={profilePanes}
id="profile-info-tab" href="#info" onClick={this.handleTabClick} renderActiveOnly={false} />
ref={this.infoSelectorRef}>
INFORMATION
</a>
<a className={this.state.viewSelected === "profile-topics-tab" ? "active" : ""}
id="profile-topics-tab" href="#topics" onClick={this.handleTabClick}
ref={this.topicsSelectorRef}>
TOPICS
</a>
<a className={this.state.viewSelected === "profile-posts-tab" ? "active" : ""}
id="profile-posts-tab" href="#posts" onClick={this.handleTabClick}
ref={this.postsSelectorRef}>
POSTS
</a>
<span ref={this.underlineBarRef} className="underline-bar"></span>
</div>
</header>
<div className="tab-content">
<div id="profile-info" className={
this.state.viewSelected === "profile-info-tab" ? "show" : "hide"
}>
{infoTab}
</div>
<div id="profile-topics" className={
this.state.viewSelected === "profile-topics-tab" ? "show" : "hide"
}>
{topicsTab}
</div>
<div id="profile-posts" className={
this.state.viewSelected === "profile-posts-tab" ? "show" : "hide"
}>
{postsTab}
</div>
</div>
</div> </div>
); );
} }
@ -145,10 +113,6 @@ class Profile extends Component {
} }
} }
} }
componentDidMount() {
this.infoSelectorRef.current.click();
}
} }
Profile.contextTypes = { Profile.contextTypes = {

16
src/containers/SignUpContainer.js

@ -5,12 +5,12 @@ import UsernameFormContainer from './UsernameFormContainer';
class SignUp extends Component { class SignUp extends Component {
render() { render() {
return ( return (
<div className="valign-wrapper"> <div className="sign-up-container">
<div className="centerDiv"> <div>
<h1>Sign Up</h1> <h1>Sign Up</h1>
<p>Username: {this.props.user.username}</p> <p className="no-margin">
<p>Account: {this.props.user.address}</p> <strong>Account address:</strong> {this.props.user.address}
<p>OrbitDB: {this.props.orbitDB.id}</p> </p>
<UsernameFormContainer/> <UsernameFormContainer/>
</div> </div>
</div> </div>
@ -21,11 +21,7 @@ class SignUp extends Component {
// May still need this even with data function to refresh component on updates for this contract. // May still need this even with data function to refresh component on updates for this contract.
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {
accounts: state.accounts, user: state.user
Forum: state.contracts.Forum,
user: state.user,
orbitDB: state.orbitDB,
drizzleStatus: state.drizzleStatus
} }
}; };

57
src/containers/StartTopicContainer.js

@ -3,7 +3,9 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import uuidv4 from 'uuid/v4'; import uuidv4 from 'uuid/v4';
import Post from '../components/Post' import { Form, TextArea, Button, Icon } from 'semantic-ui-react'
import NewTopicPreview from '../components/NewTopicPreview'
const contract = "Forum"; const contract = "Forum";
const contractMethod = "createTopic"; const contractMethod = "createTopic";
@ -40,6 +42,7 @@ class StartTopic extends Component {
topicSubjectInputEmptySubmit: this.state.topicSubjectInput === '', topicSubjectInputEmptySubmit: this.state.topicSubjectInput === '',
topicMessageInputEmptySubmit: this.state.topicMessageInput === '' topicMessageInputEmptySubmit: this.state.topicMessageInput === ''
}); });
return; return;
} }
@ -98,46 +101,48 @@ class StartTopic extends Component {
</div> </div>
} }
{this.state.previewEnabled && {this.state.previewEnabled &&
<Post post = {{ <NewTopicPreview
avatarUrl: this.props.user.avatarUrl, date={this.state.previewDate}
username: this.props.user.username, subject={this.state.topicSubjectInput}
subject: this.state.topicSubjectInput, content={this.state.topicMessageInput}
date: this.state.previewDate,
postContent: this.state.topicMessageInput
}}
id={0}
/> />
} }
<form className="topic-form"> <Form>
{!this.state.previewEnabled && {!this.state.previewEnabled &&
[<input key={"topicSubjectInput"} [<Form.Field key={"topicSubjectInput"}>
name={"topicSubjectInput"} <Form.Input name={"topicSubjectInput"}
className={this.state.topicSubjectInputEmptySubmit ? "form-input-required" : ""} error={this.state.topicSubjectInputEmptySubmit}
type="text" type="text"
value={this.state.topicSubjectInput} value={this.state.topicSubjectInput}
placeholder="Subject" placeholder="Subject"
id="topicSubjectInput" id="topicSubjectInput"
onChange={this.handleInputChange} />, onChange={this.handleInputChange} />
<textarea key={"topicMessageInput"} </Form.Field>,
<TextArea key={"topicMessageInput"}
name={"topicMessageInput"} name={"topicMessageInput"}
className={this.state.topicMessageInputEmptySubmit ? "form-input-required" : ""} className={this.state.topicMessageInputEmptySubmit ? "form-textarea-required" : ""}
value={this.state.topicMessageInput} value={this.state.topicMessageInput}
placeholder="Post" placeholder="Post"
id="topicMessageInput" id="topicMessageInput"
rows={5}
autoHeight
onChange={this.handleInputChange} />] onChange={this.handleInputChange} />]
} }
<button key="submit" <br/><br/>
className="btn waves-effect waves-teal white black-text" <Button.Group>
type="button" <Button animated key="submit" type="button" color='teal'
onClick={this.validateAndPost}> onClick={this.validateAndPost}>
<i className="material-icons right">send</i>Post <Button.Content visible>Post</Button.Content>
</button> <Button.Content hidden>
<button className="waves-effect waves-orange btn white black-text margin-left-small" <Icon name='send' />
type="button" </Button.Content>
</Button>
<Button type="button" color='yellow'
onClick={this.handlePreviewToggle}> onClick={this.handlePreviewToggle}>
<span>{previewEditText}</span> {previewEditText}
</button> </Button>
</form> </Button.Group>
</Form>
</div> </div>
); );
} }

5
src/containers/TopicContainer.js

@ -83,8 +83,11 @@ class Topic extends Component {
} }
return ( return (
<div style={{marginBottom: '70px'}}> <div className="fill">
{topicContents} {topicContents}
{!this.state.posting &&
<div className="bottom-overlay-pad"></div>
}
</div> </div>
); );
} }

69
src/containers/UsernameFormContainer.js

@ -1,10 +1,10 @@
import { drizzleConnect } from 'drizzle-react'
import React, { Component } from 'react' import React, { Component } from 'react'
import { drizzleConnect } from 'drizzle-react'
import PropTypes from 'prop-types'
import { Button, Message, Form, Dimmer, Loader, Header } from 'semantic-ui-react'
import { createDatabases } from './../util/orbit' import { createDatabases } from './../util/orbit'
import PropTypes from 'prop-types'
const contract = "Forum"; const contract = "Forum";
const signUpMethod = "signUp"; const signUpMethod = "signUp";
const updateUsernameMethod ="updateUsername"; const updateUsernameMethod ="updateUsername";
@ -15,12 +15,31 @@ class UsernameFormContainer extends Component {
this.handleInputChange = this.handleInputChange.bind(this); this.handleInputChange = this.handleInputChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
this.completeAction = this.completeAction.bind(this);
this.contracts = context.drizzle.contracts; this.contracts = context.drizzle.contracts;
this.state = {usernameInput:''};
this.state = {
usernameInput: '',
error: false,
completingAction: false
};
}
handleInputChange(e, { name, value }) {
this.setState({ [name]: value })
} }
async handleSubmit() { handleSubmit() {
if (this.state.usernameInput === ''){
this.setState({ error: true });
} else {
this.completeAction();
}
}
async completeAction() {
this.setState({ completingAction: true });
if(this.props.user.hasSignedUp) if(this.props.user.hasSignedUp)
this.contracts[contract].methods[updateUsernameMethod].cacheSend(...[this.state.usernameInput]); this.contracts[contract].methods[updateUsernameMethod].cacheSend(...[this.state.usernameInput]);
else else
@ -29,31 +48,41 @@ class UsernameFormContainer extends Component {
this.contracts[contract].methods[signUpMethod].cacheSend(...[this.state.usernameInput, orbitdbInfo.id, this.contracts[contract].methods[signUpMethod].cacheSend(...[this.state.usernameInput, orbitdbInfo.id,
orbitdbInfo.topicsDB, orbitdbInfo.postsDB, orbitdbInfo.publicKey, orbitdbInfo.privateKey]); orbitdbInfo.topicsDB, orbitdbInfo.postsDB, orbitdbInfo.publicKey, orbitdbInfo.privateKey]);
} }
}
handleInputChange(event) {
this.setState({[event.target.name]: event.target.value});
} }
render() { render() {
const hasSignedUp = this.props.user.hasSignedUp; const hasSignedUp = this.props.user.hasSignedUp;
if(hasSignedUp!==null) { if(hasSignedUp !== null) {
const buttonText = hasSignedUp ? "Update" : "Sign Up"; const buttonText = hasSignedUp ? "Update" : "Sign Up";
const placeholderText = hasSignedUp ? this.props.user.username : "Username"; const placeholderText = hasSignedUp ? this.props.user.username : "Username";
var withError = this.state.error && {error: true};
return( return(
<form> <div>
<div className="input-field"> <Form onSubmit={this.handleSubmit} {...withError}>
<input key={"usernameInput"} name={"usernameInput"} id="usernameInput" <Form.Field required>
type="text" className="validate" value={this.state.usernameInput} <label>Username</label>
onChange={this.handleInputChange}/> <Form.Input
<label htmlFor="usernameInput">{placeholderText}</label> placeholder={placeholderText}
name='usernameInput'
value={this.state.usernameInput}
onChange={this.handleInputChange}
/>
</Form.Field>
<Message
error
header='Data Incomplete'
content='You need to provide a username to sign up for an account.'
/>
<Button type='submit'>{buttonText}</Button>
</Form>
<Dimmer active={this.state.completingAction} page>
<Header as='h2' inverted>
<Loader size='large'>Magic elfs are processing your nobel request.</Loader>
</Header>
</Dimmer>
</div> </div>
<button key="submit" className="waves-effect waves-light btn-large" type="button" onClick={this.handleSubmit}>{buttonText}</button>
</form>
); );
} }

18
src/layouts/CoreLayout/CoreLayout.js

@ -1,21 +1,35 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import HeaderBar from '../../components/HeaderBar';
import NavBar from '../../components/NavBar'; import NavBar from '../../components/NavBar';
// Styles // Styles
import '../../assets/fonts/fontawesome-free-5.0.13/fontawesome-all.js'; import '../../assets/fonts/fontawesome-free-5.0.13/fontawesome-all.js';
import '../../assets/css/App.css'; import '../../assets/css/App.css';
import '../../assets/css/loading-container.css';
import '../../assets/css/sign-up-container.css';
import '../../assets/css/navbar.css';
import '../../assets/css/board-container.css';
import '../../assets/css/start-topic-container.css';
import '../../assets/css/topic-container.css';
import '../../assets/css/profile-container.css';
class CoreLayout extends Component { class CoreLayout extends Component {
render() { render() {
return ( return (
<div className="App"> <div className="App">
<HeaderBar/>
<NavBar/> <NavBar/>
<div className="page-container">
<aside className="left-side-panel">
</aside>
<div className="main-panel">
<div className="view-container"> <div className="view-container">
{this.props.children} {this.props.children}
</div> </div>
</div> </div>
<aside className="right-side-panel">
</aside>
</div>
</div>
); );
} }
} }

16
src/redux/store.js

@ -1,11 +1,11 @@
import { browserHistory } from 'react-router' import { browserHistory } from 'react-router';
import { createStore, applyMiddleware, compose } from 'redux' import { createStore, applyMiddleware, compose } from 'redux';
import { routerMiddleware } from 'react-router-redux' import { routerMiddleware } from 'react-router-redux';
import reducer from './reducer/reducer' import reducer from './reducer/reducer';
import rootSaga from './sagas/rootSaga' import rootSaga from './sagas/rootSaga';
import createSagaMiddleware from 'redux-saga' import createSagaMiddleware from 'redux-saga';
import { generateContractsInitialState } from 'drizzle' import { generateContractsInitialState } from 'drizzle';
import drizzleOptions from '../util/drizzleOptions' import drizzleOptions from '../util/drizzleOptions';
// Redux DevTools // Redux DevTools
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

Loading…
Cancel
Save