Browse Source

Migrate to Semantic UI, Minor improvements

develop
Apostolos Fanakis 7 years ago
parent
commit
15d7110c2d
  1. 1
      package.json
  2. 42
      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. 12
      src/components/LoadingSpinner.js
  15. 66
      src/components/NavBar.js
  16. 156
      src/components/NewPost.js
  17. 65
      src/components/NewTopicPreview.js
  18. 122
      src/components/Post.js
  19. 68
      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. 69
      src/containers/StartTopicContainer.js
  26. 5
      src/containers/TopicContainer.js
  27. 71
      src/containers/UsernameFormContainer.js
  28. 38
      src/layouts/CoreLayout/CoreLayout.js
  29. 16
      src/redux/store.js

1
package.json

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

42
public/index.html

@ -1,16 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
@ -18,23 +18,19 @@
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<!-- Import Google Icon Font -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
-->
<!-- Import Google Icon Font -->
<!-- <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> -->
<!-- Compiled and minified CSS -->
<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>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.1/semantic.min.css"></link>
<title>Apella</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
@ -43,6 +39,6 @@
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
-->
</body>
</html>

203
src/assets/css/App.css

@ -14,167 +14,89 @@ strong {
font-weight: bold !important;
}
#root {
height: 100%;
}
.App {
width: 100%;
height: 100%;
margin: 0px;
display: flex;
flex-flow: column nowrap;
align-items: flex-start;
}
.view-container {
width: 60%;
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 {
.page-container {
width: 100%;
height: 100%;
margin: 71px 0px 0px;
}
.header-bar-text {
height: 100%;
margin-left: 8px;
.left-side-panel {
margin-top: 71px;
position: fixed;
width: 20%;
height: calc(100% - 71px);
top: 0;
left: 0;
}
.header-bar-text div {
.main-panel {
width: 60%;
height: 100%;
display: table;
}
.header-bar-text div>div {
display: table-cell;
vertical-align: middle;
margin: 0px 20%;
}
/* NAVBAR */
.navColor {
background: #0c1a2b;
}
.stick {
z-index: 2;
.right-side-panel {
margin-top: 71px;
position: fixed;
width: 20%;
height: calc(100% - 71px);
top: 0;
width: 100%;
}
/* TOPICS */
.topic {
width: 100%;
margin: 18px 0px;
}
.topic-subject {
margin: 0px 0px 5px;
right: 0;
}
.topic-meta {
margin: 5px 0px 0px;
}
.topic-date {
margin-bottom: 0px;
font-size: 0.82vw !important;
text-align: right;
}
/* START TOPIC */
.topic-form {
.view-container {
width: 100%;
margin: 20px 0px;
}
.topic-form input[type=text], textarea {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
border-radius: 4px;
resize: vertical;
box-sizing: border-box;
height: 100%;
margin: 0px auto;
}
.topic-form textarea {
min-height: 200px;
}
/* MISC */
.form-input-required {
border-color: red !important;
.form-textarea-required {
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 {
padding: 0px 20px;
.card {
width: 100% !important;
}
/* POSTS */
.post {
width: 100%;
background-color: #FFFFFF;
margin: 20px 0px;
.bottom-overlay-pad {
background: rgba(255, 255, 255, 0.85);
z-index: 10;
position: fixed;
bottom: 0px;
height: 62px;
width: 60%;
margin: 0px;
padding: 0px;
}
.post-meta {
float: right;
margin-right: 11.25px;
}
.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;
.action-button {
z-index: 11;
position: fixed;
bottom: 10px;
left: calc(50% - 24px);
}
/* PROFILE */
.profile-tab {
width: 100%;
.grey-text {
color: grey;
}
/* MISC */
.inline {
display: inline-block;
}
@ -192,7 +114,9 @@ hr {
margin: 0px;
}
*:focus {outline:none !important}
*:focus {
outline:none !important
}
.centerDiv {
margin: 0 auto;
@ -203,20 +127,24 @@ a {
text-decoration: none;
}
.post-content a{
color: #039be5;
}
.center-in-parent {
width: 100%;
height: 100%;
text-align: center;
}
.vertical-center-in-parent {
vertical-align: middle;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.vertical-center-children {
height: 100%;
display: flex;
flex-flow: row nowrap;
align-items: flex-start;
flex-direction: column;
justify-content: center;
}
#overlay {
@ -242,6 +170,11 @@ a {
-ms-transform: translate(-50%,-50%);
}
.fill {
width: 100%;
height: 100%;
}
.full-width {
max-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 { Link } from 'react-router';
import { Button, Icon } from 'semantic-ui-react'
const FloatingButton = (props) => {
return (
<div>
{props.to
?<div className="fixed-action-btn">
<Link className="btn-floating btn-large waves-effect waves-light teal lighten-1"
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 className="action-button" onClick={props.onClick}>
<Button icon color='teal' size='large'>
<Icon name='add'/>
</Button>
</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;

12
src/components/LoadingSpinner.js

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

66
src/components/NavBar.js

@ -1,51 +1,53 @@
import React, { Component } from '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 {
constructor(props){
super(props);
this.handleScroll = this.handleScroll.bind(this);
this.handleItemClick = this.handleItemClick.bind(this);
this.navRef = React.createRef();
}
handleItemClick(to) {
this.context.router.push(to);
}
render() {
return (
<nav ref={this.navRef}>
<div className="nav-wrapper navColor">
<ul id="nav-mobile" className="left hide-on-med-and-down">
<li>
<Link to="/">Home</Link>
</li>
{this.props.hasSignedUp &&
<li>
<Link to="/profile">Profile</Link>
</li>
}
</ul>
</div>
</nav>
<Menu fixed='top' inverted>
<Menu.Item header onClick={() => {this.handleItemClick("/")}}>
<Image
size='mini'
src={require('../resources/logo.png')}
style={{ marginRight: '1.5em' }}
/>
Apella
</Menu.Item>
<Menu.Item onClick={() => {this.handleItemClick("/")}}>
Home
</Menu.Item>
{this.props.hasSignedUp
? <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(){
this.headerHeight = this.navRef.current.offsetTop;
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");
}
}
NavBar.contextTypes = {
router: PropTypes.object
};
const mapStateToProps = state => {

156
src/components/NewPost.js

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

122
src/components/Post.js

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

68
src/components/Topic.js

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

21
src/containers/BoardContainer.js

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

6
src/containers/HomeContainer.js

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

6
src/containers/LoadingContainer.js

@ -17,7 +17,7 @@ class LoadingContainer extends Component {
}
return(
<main className="container loading-screen">
<main className="loading-screen">
<div>
<div>
<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)
{
return(
<main className="container loading-screen">
<main className="loading-screen">
<div>
<div>
<h1><span role="img" aria-label="Fox Face">🦊</span></h1>
@ -45,7 +45,7 @@ class LoadingContainer extends Component {
if (!this.props.orbitDB.ipfsInitialized)
{
return(
<main className="container loading-screen">
<main className="loading-screen">
<div>
<div>
<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 PropTypes from 'prop-types';
import { Tab } from 'semantic-ui-react'
import WithBlockchainData from '../components/WithBlockchainData';
import ProfileInformation from '../components/ProfileInformation';
import TopicList from '../components/TopicList';
import PostList from '../components/PostList';
import LoadingSpinner from '../components/LoadingSpinner';
import '../assets/css/materialTabs.css';
class Profile extends Component {
constructor(props, context) {
super(props);
this.handleTabClick = this.handleTabClick.bind(this);
this.propsToView = this.propsToView.bind(this);
this.drizzle = context.drizzle;
this.underlineBarRef = React.createRef();
this.infoSelectorRef = React.createRef();
this.topicsSelectorRef = React.createRef();
this.postsSelectorRef = React.createRef();
this.state = {
viewSelected: "profile-info-tab",
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() {
this.propsToView();
var infoTab =
@ -86,45 +63,36 @@ class Profile extends Component {
}
</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 (
<div>
<header>
<div id="material-tabs">
<a className={this.state.viewSelected === "profile-info-tab" ? "active" : ""}
id="profile-info-tab" href="#info" onClick={this.handleTabClick}
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>
<Tab
menu={{ secondary: true, pointing: true }}
panes={profilePanes}
renderActiveOnly={false} />
</div>
);
}
@ -145,10 +113,6 @@ class Profile extends Component {
}
}
}
componentDidMount() {
this.infoSelectorRef.current.click();
}
}
Profile.contextTypes = {

16
src/containers/SignUpContainer.js

@ -5,12 +5,12 @@ import UsernameFormContainer from './UsernameFormContainer';
class SignUp extends Component {
render() {
return (
<div className="valign-wrapper">
<div className="centerDiv">
<div className="sign-up-container">
<div>
<h1>Sign Up</h1>
<p>Username: {this.props.user.username}</p>
<p>Account: {this.props.user.address}</p>
<p>OrbitDB: {this.props.orbitDB.id}</p>
<p className="no-margin">
<strong>Account address:</strong> {this.props.user.address}
</p>
<UsernameFormContainer/>
</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.
const mapStateToProps = state => {
return {
accounts: state.accounts,
Forum: state.contracts.Forum,
user: state.user,
orbitDB: state.orbitDB,
drizzleStatus: state.drizzleStatus
user: state.user
}
};

69
src/containers/StartTopicContainer.js

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

5
src/containers/TopicContainer.js

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

71
src/containers/UsernameFormContainer.js

@ -1,10 +1,10 @@
import { drizzleConnect } from 'drizzle-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 PropTypes from 'prop-types'
const contract = "Forum";
const signUpMethod = "signUp";
const updateUsernameMethod ="updateUsername";
@ -15,12 +15,31 @@ class UsernameFormContainer extends Component {
this.handleInputChange = this.handleInputChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.completeAction = this.completeAction.bind(this);
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)
this.contracts[contract].methods[updateUsernameMethod].cacheSend(...[this.state.usernameInput]);
else
@ -29,31 +48,41 @@ class UsernameFormContainer extends Component {
this.contracts[contract].methods[signUpMethod].cacheSend(...[this.state.usernameInput, orbitdbInfo.id,
orbitdbInfo.topicsDB, orbitdbInfo.postsDB, orbitdbInfo.publicKey, orbitdbInfo.privateKey]);
}
}
handleInputChange(event) {
this.setState({[event.target.name]: event.target.value});
}
render() {
const hasSignedUp = this.props.user.hasSignedUp;
if(hasSignedUp!==null) {
if(hasSignedUp !== null) {
const buttonText = hasSignedUp ? "Update" : "Sign Up";
const placeholderText = hasSignedUp ? this.props.user.username : "Username";
var withError = this.state.error && {error: true};
return(
<form>
<div className="input-field">
<input key={"usernameInput"} name={"usernameInput"} id="usernameInput"
type="text" className="validate" value={this.state.usernameInput}
onChange={this.handleInputChange}/>
<label htmlFor="usernameInput">{placeholderText}</label>
</div>
<button key="submit" className="waves-effect waves-light btn-large" type="button" onClick={this.handleSubmit}>{buttonText}</button>
</form>
<div>
<Form onSubmit={this.handleSubmit} {...withError}>
<Form.Field required>
<label>Username</label>
<Form.Input
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>
);
}

38
src/layouts/CoreLayout/CoreLayout.js

@ -1,23 +1,37 @@
import React, { Component } from 'react';
import HeaderBar from '../../components/HeaderBar';
import NavBar from '../../components/NavBar';
// Styles
import '../../assets/fonts/fontawesome-free-5.0.13/fontawesome-all.js';
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 {
render() {
return (
<div className="App">
<HeaderBar/>
<NavBar/>
<div className="view-container">
{this.props.children}
</div>
</div>
);
}
render() {
return (
<div className="App">
<NavBar/>
<div className="page-container">
<aside className="left-side-panel">
</aside>
<div className="main-panel">
<div className="view-container">
{this.props.children}
</div>
</div>
<aside className="right-side-panel">
</aside>
</div>
</div>
);
}
}
export default CoreLayout;

16
src/redux/store.js

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

Loading…
Cancel
Save