Browse Source

Rebase, Add Routes, Redirects, Components, Views, Markdown support

develop
Apostolos Fanakis 7 years ago
parent
commit
ff9accf93a
  1. 1
      .gitignore
  2. 10
      contracts/Forum.sol
  3. 6
      package.json
  4. 19
      src/App.js
  5. 8
      src/App.test.js
  6. 339
      src/assets/css/App.css
  7. 0
      src/assets/css/index.css
  8. 0
      src/assets/css/open-sans.css
  9. 0
      src/assets/css/oswald.css
  10. 0
      src/assets/css/pure-min.css
  11. 0
      src/assets/fonts/Open-Sans-regular/LICENSE.txt
  12. 0
      src/assets/fonts/Open-Sans-regular/Open-Sans-regular.eot
  13. 0
      src/assets/fonts/Open-Sans-regular/Open-Sans-regular.svg
  14. 0
      src/assets/fonts/Open-Sans-regular/Open-Sans-regular.ttf
  15. 0
      src/assets/fonts/Open-Sans-regular/Open-Sans-regular.woff
  16. 0
      src/assets/fonts/Open-Sans-regular/Open-Sans-regular.woff2
  17. 0
      src/assets/fonts/Oswald-300/LICENSE.txt
  18. 0
      src/assets/fonts/Oswald-300/Oswald-300.eot
  19. 0
      src/assets/fonts/Oswald-300/Oswald-300.svg
  20. 0
      src/assets/fonts/Oswald-300/Oswald-300.ttf
  21. 0
      src/assets/fonts/Oswald-300/Oswald-300.woff
  22. 0
      src/assets/fonts/Oswald-300/Oswald-300.woff2
  23. 0
      src/assets/fonts/Oswald-regular/LICENSE.txt
  24. 0
      src/assets/fonts/Oswald-regular/Oswald-regular.eot
  25. 0
      src/assets/fonts/Oswald-regular/Oswald-regular.svg
  26. 0
      src/assets/fonts/Oswald-regular/Oswald-regular.ttf
  27. 0
      src/assets/fonts/Oswald-regular/Oswald-regular.woff
  28. 0
      src/assets/fonts/Oswald-regular/Oswald-regular.woff2
  29. 3271
      src/assets/fonts/fontawesome-free-5.0.13/fontawesome-all.js
  30. 13
      src/components/FloatingButton.js
  31. 19
      src/components/HeaderBar.js
  32. 28
      src/components/NavBar.js
  33. 91
      src/components/NewPost.js
  34. 11
      src/components/NotFoundView.js
  35. 37
      src/components/Post.js
  36. 45
      src/components/PostList.js
  37. 30
      src/components/ProfileInformation.js
  38. 126
      src/components/StartTopic.js
  39. 18
      src/components/Topic.js
  40. 50
      src/components/TopicList.js
  41. 50
      src/containers/BoardContainer.js
  42. 29
      src/containers/HomeContainer.js
  43. 46
      src/containers/PrivateRouteContainer.js
  44. 35
      src/containers/ProfileContainer.js
  45. 34
      src/containers/SignUpContainer.js
  46. 54
      src/containers/TopicContainer.js
  47. 110
      src/css/App.css
  48. 35
      src/index.js
  49. 26
      src/layouts/CoreLayout/CoreLayout.js
  50. 41
      src/layouts/home/HomeContainer.js
  51. 2
      src/redux/reducer/contractReducer.js
  52. 10
      src/redux/reducer/reducer.js
  53. 3
      src/redux/reducer/userReducer.js
  54. 0
      src/redux/sagas/contractSaga.js
  55. 2
      src/redux/sagas/rootSaga.js
  56. 0
      src/redux/sagas/userSaga.js
  57. 8
      src/redux/store.js
  58. BIN
      src/resources/PageNotFound.jpg
  59. BIN
      src/resources/logo.png
  60. 2
      src/util/drizzleOptions.js
  61. 6
      src/util/orbit.js
  62. 2
      src/util/orbitReducer.js
  63. 2
      src/util/orbitSaga.js

1
.gitignore

@ -25,6 +25,7 @@ yarn-error.log*
.env.development.local
.env.test.local
.env.production.local
/compileAndRun.sh
# Jetbrains
.idea

10
contracts/Forum.sol

@ -123,20 +123,22 @@ contract Forum {
mapping (uint => Topic) topics;
mapping (uint => Post) posts;
function createTopic() public returns (uint topicID) {
function createTopic() public returns (uint) {
require(hasUserSignedUp(msg.sender)); // Only registered users can create topics
topicID = numTopics++;
uint topicID = numTopics++;
topics[topicID] = Topic(topicID, msg.sender, block.timestamp, new uint[](0));
users[msg.sender].topicIDs.push(topicID);
return topicID;
}
function createPost(uint topicID) public returns (uint postID) {
function createPost(uint topicID) public returns (uint) {
require(hasUserSignedUp(msg.sender)); // Only registered users can create posts
require(topicID<numTopics); // Only allow posting to a topic that exists
postID = numPosts++;
uint postID = numPosts++;
posts[postID] = Post(postID, msg.sender, block.timestamp);
topics[topicID].postIDs.push(postID);
users[msg.sender].postIDs.push(postID);
return postID;
}
function getTopicPosts (uint topicID) public view returns (uint[]) {

6
package.json

@ -17,10 +17,14 @@
"prop-types": "^15.6.1",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-scripts": "^1.1.4",
"react-markdown": "^3.3.2",
"react-redux": "^5.0.7",
"react-router": "^3.2.1",
"react-router-dom": "^4.2.2",
"react-router-redux": "^4.0.8",
"react-scripts": "^1.1.4",
"react-timeago": "^4.1.9",
"react-user-avatar": "^1.10.0",
"redux": "^3.7.2",
"redux-saga": "0.16.0",
"web3": "^1.0.0-beta.34"

19
src/App.js

@ -1,19 +0,0 @@
import React, { Component } from 'react'
// Styles
import './css/oswald.css'
import './css/open-sans.css'
import './css/pure-min.css'
import './css/App.css'
class App extends Component {
render() {
return (
<div className="App">
{this.props.children}
</div>
);
}
}
export default App

8
src/App.test.js

@ -1,8 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
});

339
src/assets/css/App.css

@ -0,0 +1,339 @@
/* PAGE */
html, body {
margin: 0;
display: block;
height: 100%;
}
body,
.pure-g [class*=pure-u] {
font-family: 'Open Sans', sans-serif;
}
.App {
width: 100%;
margin: 0px;
display: flex;
flex-flow: column nowrap;
align-items: flex-start;
}
.view-container {
width: calc(100% - 24px);
margin: 10px 12px;
}
.container {
box-sizing: border-box;
width: calc(100% - 60px);
max-width: 600px;
padding: 45px 20px;
margin: 0 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%;
}
.header-bar-text {
height: 100%;
margin-left: 8px;
}
.header-bar-text div {
height: 100%;
display: table;
}
.header-bar-text div>div {
display: table-cell;
vertical-align: middle;
}
/* NAVBAR */
.navbar {
max-width: 100%;
margin: 0px;
background: #0c1a2b;
font-family: 'Oswald', 'Arial Narrow', sans-serif;
}
.navbar ul {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
}
.navbar li {
float: left;
}
.navbar li a {
display: block;
color: white;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
.navbar a:active,
.navbar a:focus,
.navbar a:hover {
background: #233e5e;
}
/* TOPICS */
.topic {
width: calc(100% - 14px);
background-color: #FFFFFF;
margin: 12px 0px;
padding: 7px;
}
.topic-subject {
margin: 0px 0px 5px;
}
.topic-meta {
margin: 5px 0px 0px;
}
.topic-date {
margin-top: 2px;
margin-bottom: 0px;
color: #333333;
font-size: 0.82vw;
text-align: right;
}
/* START TOPIC */
.start-topic-back-button {
background-color: transparent;
margin-left: 7px;
margin-bottom: 7px;
color: #0c1a2b;
}
.start-topic-back-button .fa-arrow-left{
filter:drop-shadow(1px 1px 2px #0c1a2b);
}
.start-topic-back-button .fa-arrow-left:active{
filter:drop-shadow(0px 1px 1px #0A1624);
color: #0A1624;
transform: translateY(3px);
}
.start-topic-back-button:focus {
outline:none;
}
.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;
}
.topic-form textarea {
height: 200px;
}
.markdownPreview {
padding: 0px 20px;
}
/* FLOATING BUTTONS */
.floating-button{
position:fixed;
width:60px;
height:60px;
bottom:40px;
right:40px;
background-color:#0C9;
color:#FFF;
border-radius:50px;
text-align:center;
box-shadow: 2px 2px 3px #999;
}
.floating-button:focus {
outline:none;
}
.floating-button:active {
background-color: #00B386;
box-shadow: 0px 1px 3px #333;
transform: translateY(4px);
}
.floating-button svg {
min-height: 100%;
text-align: center;
vertical-align: middle;
}
/* POSTS */
.post {
width: calc(100% - 14px);
background-color: #FFFFFF;
margin: 12px 0px;
padding: 7px;
}
.post-header {
margin-bottom: 2px;
}
.user-avatar {
max-height: 40px;
margin-right: 7px;
}
.post-info {
width: 100%;
margin-top: 2px;
margin-bottom: 0px;
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
color: #333333;
font-size: 0.82vw;
}
.post-content {
margin: 0px;
}
.post-meta {
margin-top: 2px;
}
.user-column {
margin: 0px 10px;
order: 2;
}
.user-info {
display: inline-block;
background-color: #FFFFFF;
margin: 12px auto;
padding: 7px;
}
/* FORMS */
.pure-form {
display: inline-block;
}
.pure-form input[type="text"]:focus {
border-color: #0c1a2b;
}
/* BUTTONS */
.pure-button-primary {
background-color: #0c1a2b;
}
.pure-button-primary:hover {
background-color: #233e5e;
}
.pure-button-primary:active {
background-color: #0c1a2b;
}
/* ALERTS */
.alert {
padding: .5rem;
color: darkgreen;
background: darkseagreen;
border: 1px solid darkgreen;
}
/* MISC */
.inline {
display: inline-block;
}
.no-margin {
margin: 0px;
}
.margin-left-small {
margin-left: 7px;
}
.card {
box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);
}
.card:hover {
box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)
}
hr {
color: #0c1a2b;
margin: 0px;
}
h1, h2, h3 {
font-family: 'Oswald', 'Arial Narrow', sans-serif;
}
a:focus {outline:none}
code {
padding: .25rem;
margin: 0 .25rem;
background: #eee;
}
.center {
text-align: center;
}
a {
color:inherit;
text-decoration: none;
}

0
src/css/index.css → src/assets/css/index.css

0
src/css/open-sans.css → src/assets/css/open-sans.css

0
src/css/oswald.css → src/assets/css/oswald.css

0
src/css/pure-min.css → src/assets/css/pure-min.css

0
src/fonts/Open-Sans-regular/LICENSE.txt → src/assets/fonts/Open-Sans-regular/LICENSE.txt

0
src/fonts/Open-Sans-regular/Open-Sans-regular.eot → src/assets/fonts/Open-Sans-regular/Open-Sans-regular.eot

0
src/fonts/Open-Sans-regular/Open-Sans-regular.svg → src/assets/fonts/Open-Sans-regular/Open-Sans-regular.svg

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

0
src/fonts/Open-Sans-regular/Open-Sans-regular.ttf → src/assets/fonts/Open-Sans-regular/Open-Sans-regular.ttf

0
src/fonts/Open-Sans-regular/Open-Sans-regular.woff → src/assets/fonts/Open-Sans-regular/Open-Sans-regular.woff

0
src/fonts/Open-Sans-regular/Open-Sans-regular.woff2 → src/assets/fonts/Open-Sans-regular/Open-Sans-regular.woff2

0
src/fonts/Oswald-300/LICENSE.txt → src/assets/fonts/Oswald-300/LICENSE.txt

0
src/fonts/Oswald-300/Oswald-300.eot → src/assets/fonts/Oswald-300/Oswald-300.eot

0
src/fonts/Oswald-300/Oswald-300.svg → src/assets/fonts/Oswald-300/Oswald-300.svg

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

0
src/fonts/Oswald-300/Oswald-300.ttf → src/assets/fonts/Oswald-300/Oswald-300.ttf

0
src/fonts/Oswald-300/Oswald-300.woff → src/assets/fonts/Oswald-300/Oswald-300.woff

0
src/fonts/Oswald-300/Oswald-300.woff2 → src/assets/fonts/Oswald-300/Oswald-300.woff2

0
src/fonts/Oswald-regular/LICENSE.txt → src/assets/fonts/Oswald-regular/LICENSE.txt

0
src/fonts/Oswald-regular/Oswald-regular.eot → src/assets/fonts/Oswald-regular/Oswald-regular.eot

0
src/fonts/Oswald-regular/Oswald-regular.svg → src/assets/fonts/Oswald-regular/Oswald-regular.svg

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

0
src/fonts/Oswald-regular/Oswald-regular.ttf → src/assets/fonts/Oswald-regular/Oswald-regular.ttf

0
src/fonts/Oswald-regular/Oswald-regular.woff → src/assets/fonts/Oswald-regular/Oswald-regular.woff

0
src/fonts/Oswald-regular/Oswald-regular.woff2 → src/assets/fonts/Oswald-regular/Oswald-regular.woff2

3271
src/assets/fonts/fontawesome-free-5.0.13/fontawesome-all.js

File diff suppressed because one or more lines are too long

13
src/components/FloatingButton.js

@ -0,0 +1,13 @@
import React from 'react';
const FloatingButton = (props) => {
return (
<div className="pure-u-1-1">
<p className="no-margin floating-button" data-fa-transform="down-6" onClick={props.onClick}>
<i className="fa fa-plus fa-2x"></i>
</p>
</div>
);
};
export default FloatingButton;

19
src/components/HeaderBar.js

@ -0,0 +1,19 @@
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;

28
src/components/NavBar.js

@ -0,0 +1,28 @@
import React from 'react';
import { drizzleConnect } from 'drizzle-react';
import { Link } from 'react-router';
const NavBar = (props) => {
return (
<div className="pure-u-1-1 navbar">
<ul>
<li>
<Link to="/">Home</Link>
</li>
{props.hasSignedUp &&
<li>
<Link to="/profile">Profile</Link>
</li>
}
</ul>
</div>
);
};
const mapStateToProps = state => {
return {
hasSignedUp: state.user.hasSignedUp
}
};
export default drizzleConnect(NavBar, mapStateToProps);

91
src/components/NewPost.js

@ -0,0 +1,91 @@
import React, { Component } from 'react';
import { drizzleConnect } from 'drizzle-react';
import UserAvatar from 'react-user-avatar';
import ReactMarkdown from 'react-markdown';
class NewPost extends Component {
constructor(props, context) {
super(props);
this.handleInputChange = this.handleInputChange.bind(this);
this.handlePreviewToggle = this.handlePreviewToggle.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
postContent: '',
previewEnabled: false
};
}
async handleSubmit() {
/*this.stackId = this.contracts[contract].methods[startTopicMethod].cacheSend();*/
}
handleInputChange(event) {
this.setState({[event.target.name]: event.target.value});
}
handlePreviewToggle(){
this.setState((prevState, props) => ({
previewEnabled: !prevState.previewEnabled
}));
}
render() {
return (
<div className="pure-u-1-1 post card">
<div className="post-header">
<UserAvatar
size="40"
className="inline user-avatar"
src={this.props.user.avatarUrl}
name={this.props.user.username}/>
<p className="inline no-margin">
<strong>{this.props.user.username}<br/>Subject: {this.props.subject}</strong>
</p>
<div className="post-info">
<span></span>
<span>#{this.props.postIndex}</span>
</div>
</div>
<hr/>
<div className="post-content">
<form className="topic-form">
{this.state.previewEnabled
? <ReactMarkdown source={this.state.postContent} className="markdownPreview" />
: <textarea key={"postContent"}
name={"postContent"}
value={this.state.postContent}
placeholder="Post"
id="postContent"
onChange={this.handleInputChange} />}
<button key="submit"
className="pure-button pure-button-primary"
type="button"
onClick={this.handleSubmit}>
Post
</button>
<button className="pure-button margin-left-small"
type="button"
onClick={this.handlePreviewToggle}>
{this.state.previewEnabled ? "Edit" : "Preview"}
</button>
<button className="pure-button margin-left-small"
type="button"
onClick={this.props.onCancelClick}>
Cancel
</button>
</form>
</div>
</div>
);
}
};
const mapStateToProps = state => {
return {
user: state.user
}
};
export default drizzleConnect(NewPost, mapStateToProps);

11
src/components/NotFoundView.js

@ -0,0 +1,11 @@
import React from 'react';
const NotFoundView = (props) => {
return (
<div className="pure-u-1-1 center">
<img src={require('../resources/PageNotFound.jpg')} alt="Page not found!"/>
</div>
);
};
export default NotFoundView;

37
src/components/Post.js

@ -0,0 +1,37 @@
import React from 'react';
import UserAvatar from 'react-user-avatar';
import TimeAgo from 'react-timeago';
import ReactMarkdown from 'react-markdown';
const Post = (props) => {
const username = props.username && [props.username, <br key={props.id}/>];
return (
<div className="pure-u-1-1 post card">
<div className="post-header">
{props.avatarUrl && <UserAvatar
size="40"
className="inline user-avatar"
src={props.avatarUrl}
name={props.username}/>}
<p className="inline no-margin">
<strong>{username}Subject: {props.subject}</strong>
</p>
<div className="post-info">
<span>Posted <TimeAgo date={props.date}/></span>
{props.postIndex && <span>#{props.postIndex}</span>}
</div>
</div>
<hr/>
<div className="post-content">
<ReactMarkdown source={props.postContent} />
</div>
<hr/>
<div className="post-meta">
Maybe add buttons for upvote etc here...
</div>
</div>
);
};
export default Post;

45
src/components/PostList.js

@ -0,0 +1,45 @@
import React from 'react';
import Post from './Post';
const posts1 = [
{avatarUrl: "https://i.makeagif.com/media/4-18-2018/8BLiwJ.gif",
username: "Apostolof",
subject: "Some very important topic of discussion2!",
date: "May 25, 2018, 11:11:11",
postIndex: "1",
postContent: "# We have markdown!!!\n \n**Oh yes we do!!** \n*ITALICS* \n \n```Some code```",
id: 2,
address: 0x083c41ea13af6c2d5aaddf6e73142eb9a7b00183
},
{avatarUrl: "",
username: "",
subject: "Some very important topic of discussion!",
date: "May 20, 2018, 10:10:10",
postIndex: "",
postContent: "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequatur, natus ipsum minima.",
id: 1,
address: 0x5fe3062B24033113fbf52b2b75882890D7d8CA54
}
];
const PostList = (props) => {
const posts = posts1.map((post) =>
<Post avatarUrl={post.avatarUrl}
username={post.username}
subject={post.subject}
date={post.date}
postIndex={post.postIndex}
postContent={post.postContent}
id={post.id}
key={post.id}
address={post.address}/>
);
return (
<div className="posts-list">
{posts}
</div>
);
};
export default PostList;

30
src/components/ProfileInformation.js

@ -0,0 +1,30 @@
import React from 'react';
import UserAvatar from 'react-user-avatar';
import UsernameFormContainer from '../containers/UsernameFormContainer';
const ProfileInformation = (props) => {
return (
<div className="pure-u-1-1 user-info card">
{props.avatarUrl && <UserAvatar
size="40"
className="inline user-avatar"
src={props.avatarUrl}
name={props.username}/>}
<p className="no-margin inline">
<strong>Username</strong>: {props.username}
</p>
<p className="no-margin">
<strong>Account address:</strong> {props.address}
</p>
<p className="no-margin">
<strong>OrbitDB:</strong> {props.orbitAddress}
</p>
<p className="no-margin">
Number of posts: TODO?
</p>
<UsernameFormContainer/>
</div>
);
};
export default ProfileInformation;

126
src/components/StartTopic.js

@ -0,0 +1,126 @@
import { drizzleConnect } from 'drizzle-react'
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Post from './Post'
const contract = "Forum";
const startTopicMethod = "createTopic";
class StartTopic extends Component {
constructor(props, context) {
super(props);
this.handleInputChange = this.handleInputChange.bind(this);
this.handlePreviewToggle = this.handlePreviewToggle.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.drizzle = context.drizzle;
this.drizzleState = this.drizzle.store.getState();
this.contracts = this.drizzle.contracts;
this.abi = this.contracts[contract].abi;
this.state = {
topicSubjectInput: '',
topicMessageInput: '',
previewEnabled: false,
previewDate: ""
};
}
async handleSubmit() {
console.log("contracts:");
console.log(this.contracts);
console.log("DS contracts:");
console.log(this.drizzleState.contracts);
this.dataKey = this.drizzleState.contracts[contract].methods[startTopicMethod].cacheCall();
//TODO get return value and pass it to orbit
}
handleInputChange(event) {
this.setState({[event.target.name]: event.target.value});
}
handlePreviewToggle() {
this.setState((prevState, props) => ({
previewEnabled: !prevState.previewEnabled,
previewDate: this.getDate()
}));
}
getDate() {
const currentdate = new Date();
return ((currentdate.getMonth() + 1) + " "
+ currentdate.getDate() + ", "
+ currentdate.getFullYear() + ", "
+ currentdate.getHours() + ":"
+ currentdate.getMinutes() + ":"
+ currentdate.getSeconds());
}
render() {
if(this.dataKey) {
/*console.log(this.drizzleState);*/
if (this.drizzleState.contracts[contract]) {
console.log(this.drizzleState.contracts[contract].storedData[this.dataKey].value);
}
}
return(
<div>
<div className="pure-u-1-1 start-topic-back-button">
<p className="no-margin" onClick={this.props.onClick}>
<i className="fas fa-arrow-left fa-3x"></i>
</p>
</div>
{this.state.previewEnabled &&
<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}/>}
<form className="topic-form">
{!this.state.previewEnabled &&
[
<input key={"topicSubjectInput"}
name={"topicSubjectInput"}
type="text"
value={this.state.topicSubjectInput}
placeholder="Subject"
id="topicSubjectInput"
onChange={this.handleInputChange} />,
<textarea key={"topicMessageInput"}
name={"topicMessageInput"}
value={this.state.topicMessageInput}
placeholder="Post"
id="topicMessageInput"
onChange={this.handleInputChange} />
]}
<button key="submit"
className="pure-button"
type="button"
onClick={this.handleSubmit}>
Post
</button>
<button className="pure-button margin-left-small"
type="button"
onClick={this.handlePreviewToggle}>
{this.state.previewEnabled ? "Edit" : "Preview"}
</button>
</form>
</div>
);
}
}
StartTopic.contextTypes = {
drizzle: PropTypes.object
};
const mapStateToProps = state => {
return {
contracts: state.contracts,
user: state.user
}
};
export default drizzleConnect(StartTopic, mapStateToProps);

18
src/components/Topic.js

@ -0,0 +1,18 @@
import React from 'react';
import TimeAgo from 'react-timeago';
const Topic = (props) => {
return (
<div className="topic card">
<p className="topic-subject"><strong>{props.topicSubject}</strong></p>
<hr/>
<div className="topic-meta">
<p className="no-margin">{props.topicStarter}</p>
<p className="no-margin">Number of replies: {props.numberOfReplies}</p>
<p className="topic-date">Started <TimeAgo date={props.date}/></p>
</div>
</div>
);
};
export default Topic;

50
src/components/TopicList.js

@ -0,0 +1,50 @@
import React from 'react';
import Topic from './Topic';
import { Link } from 'react-router';
const topics1 = [
{topicSubject: 'This is a topic about something 1',
topicStarter: 'username1',
numberOfReplies: 12,
date: 'May 20, 2018, 10:10:10',
id: 1,
address: 0x5fe3062B24033113fbf52b2b75882890D7d8CA54
},
{topicSubject: 'This is a topic about something 2',
topicStarter: 'username2',
numberOfReplies: 41,
date: 'May 20, 2018, 10:10:10',
id: 2,
address: 0x083c41ea13af6c2d5aaddf6e73142eb9a7b00183
},
{topicSubject: 'This is a topic about something 3',
topicStarter: 'username3',
numberOfReplies: 73,
date: 'May 20, 2018, 10:10:10',
id: 3,
address: 0x26d1ec50b4e62c1d1a40d16e7cacc6a6580757d5
}
];
const TopicList = (props) => {
const topics = topics1.map((topic) =>
<Link to={"/topic/" + topic.id + "/" + topic.topicSubject}
key={topic.id}>
<Topic topicSubject={topic.topicSubject}
topicStarter={topic.topicStarter}
numberOfReplies={topic.numberOfReplies}
date={topic.date}
id={topic.id}
key={topic.id}
address={topic.address}/>
</Link>
);
return (
<div className="topics-list">
{topics}
</div>
);
};
export default TopicList;

50
src/containers/BoardContainer.js

@ -0,0 +1,50 @@
import { drizzleConnect } from 'drizzle-react';
import React, { Component } from 'react';
import TopicList from '../components/TopicList';
import FloatingButton from '../components/FloatingButton';
import StartTopic from '../components/StartTopic';
class Board extends Component {
constructor(props) {
super(props);
this.state = {
startingNewTopic: false
};
this.handleClick = this.handleClick.bind(this);
}
handleClick(event) {
event.preventDefault();
this.setState(prevState => ({
startingNewTopic: !prevState.startingNewTopic
}));
}
render() {
return (
this.state.startingNewTopic
?(<div>
<StartTopic onClick={this.handleClick}/>
</div>)
:(<div style={{marginBottom: '100px'}}>
<TopicList/>
<FloatingButton onClick={this.handleClick}/>
</div>)
);
}
}
const mapStateToProps = state => {
return {
accounts: state.accounts,
Forum: state.contracts.Forum,
user: state.user,
orbitDB: state.orbitDB,
drizzleStatus: state.drizzleStatus
}
};
const BoardContainer = drizzleConnect(Board, mapStateToProps);
export default BoardContainer;

29
src/containers/HomeContainer.js

@ -0,0 +1,29 @@
import { drizzleConnect } from 'drizzle-react';
import React, { Component } from 'react';
import SignUpContainer from './SignUpContainer';
import BoardContainer from './BoardContainer';
class Home extends Component {
render() {
//This must change to routes and redirects
const view = this.props.user.hasSignedUp
? (<BoardContainer/>) //This may become multiple boards
: (<SignUpContainer/>);
return (
<div>
{view}
</div>
);
}
}
const mapStateToProps = state => {
return {
user: state.user
}
};
const HomeContainer = drizzleConnect(Home, mapStateToProps);
export default HomeContainer;

46
src/containers/PrivateRouteContainer.js

@ -0,0 +1,46 @@
import React from 'react';
import { drizzleConnect } from 'drizzle-react';
import { Route } from "react-router";
import { Redirect } from "react-router-dom";
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux'
const PrivateRoute = ({ component, redirectTo, user, ...rest }) => {
console.log("rest");
console.log(JSON.stringify(component));
return (
<Route
{...rest} render={routeProps => {
return user.hasSignedUp
? (renderMergedProps(component, routeProps, rest))
: (<Redirect
to={{
pathname: redirectTo,
state: { from: routeProps.location }
}}
/>);
}}/>
);
};
const renderMergedProps = (component, ...rest) => {
const finalProps = Object.assign({}, ...rest);
return (
React.createElement(component, finalProps)
);
}
const PrivateRouteContainer = withRouter(connect(state => ({
hasSignedUp: state.user.hasSignedUp
}))(PrivateRoute));
/*const mapStateToProps = state => {
return {
user: state.user
}
};*/
/*const PrivateRouteContainer = withRouter(drizzleConnect(PrivateRoute, mapStateToProps));*/
export default PrivateRouteContainer;

35
src/containers/ProfileContainer.js

@ -0,0 +1,35 @@
import { drizzleConnect } from 'drizzle-react';
import React, { Component } from 'react';
import ProfileInformation from '../components/ProfileInformation';
import PostList from '../components/PostList';
class SignUp extends Component {
render() {
return (
<div className="pure-g">
<ProfileInformation username={this.props.user.username}
address={this.props.user.address}
orbitAddress={this.props.orbitDB.id}/>
<p className="pure-u-1-1">
My posts:
</p>
<PostList/> {/*TODO change this with actual user's posts*/}
</div>
);
}
}
const mapStateToProps = state => {
return {
accounts: state.accounts,
Forum: state.contracts.Forum,
user: state.user,
orbitDB: state.orbitDB,
drizzleStatus: state.drizzleStatus
}
};
const ProfileContainer = drizzleConnect(SignUp, mapStateToProps);
export default ProfileContainer;

34
src/containers/SignUpContainer.js

@ -0,0 +1,34 @@
import { drizzleConnect } from 'drizzle-react';
import React, { Component } from 'react';
import UsernameFormContainer from './UsernameFormContainer';
class SignUp extends Component {
render() {
return (
<div className="container pure-g">
<div className="pure-u-1-1">
<h1>Sign Up</h1>
<p><strong>Username</strong>: {this.props.user.username}</p>
<p><strong>Account</strong>: {this.props.user.address}</p>
<p><strong>OrbitDB</strong>: {this.props.orbitDB.id}</p>
<UsernameFormContainer/>
</div>
</div>
);
}
}
// 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
}
};
const SignUpContainer = drizzleConnect(SignUp, mapStateToProps);
export default SignUpContainer;

54
src/containers/TopicContainer.js

@ -0,0 +1,54 @@
import { drizzleConnect } from 'drizzle-react';
import React, { Component } from 'react';
import PostList from '../components/PostList';
import NewPost from '../components/NewPost';
import FloatingButton from '../components/FloatingButton';
class Topic extends Component {
constructor(props) {
super(props);
this.state = {
posting: false
};
this.handleClick = this.handleClick.bind(this);
}
handleClick(event) {
if (event){
event.preventDefault();
}
this.setState(prevState => ({
posting: !prevState.posting
}));
}
render() {
return (
this.state.posting
?(<div style={{marginBottom: '100px'}}>
<PostList/>
<NewPost onCancelClick={() => {this.handleClick()}}/>
</div>)
:(<div style={{marginBottom: '100px'}}>
<PostList/>
<FloatingButton onClick={this.handleClick}/>
</div>)
);
}
}
const mapStateToProps = state => {
return {
accounts: state.accounts,
Forum: state.contracts.Forum,
user: state.user,
orbitDB: state.orbitDB,
drizzleStatus: state.drizzleStatus
}
};
const TopicContainer = drizzleConnect(Topic, mapStateToProps);
export default TopicContainer;

110
src/css/App.css

@ -1,110 +0,0 @@
/* PAGE */
body,
.pure-g [class*=pure-u] {
font-family: 'Open Sans', sans-serif;
}
.header {
text-align: center;
}
h1, h2, h3 {
font-family: 'Oswald', 'Arial Narrow', sans-serif;
}
code {
padding: .25rem;
margin: 0 .25rem;
background: #eee;
}
.container {
box-sizing: border-box;
width: calc(100% - 60px);
max-width: 600px;
padding: 45px 20px;
margin: 0 auto;
}
.pure-button-primary {
background-color: #0c1a2b;
}
.pure-button-primary:hover {
background-color: #233e5e;
}
.pure-form input[type="text"]:focus {
border-color: #0c1a2b;
}
ul {
list-style-type: none;
padding-left: 0;
}
ul li:not(:last-child) {
margin-bottom: 15px;
}
/* NAVBAR */
.navbar {
position: fixed;
padding: 5px;
background: #0c1a2b;
font-family: 'Oswald', 'Arial Narrow', sans-serif;
}
.navbar a {
color: #fff;
}
.navbar a:active,
.navbar a:focus,
.navbar a:hover {
background: #233e5e;
}
.navbar .pure-menu-heading {
font-weight: bold;
text-transform: none;
}
.navbar .navbar-right {
float: right;
}
.navbar .uport-logo {
height: 16px;
margin-right: 10px;
}
/* LOADING SCREEN */
.loading-screen {
opacity: 1;
visibility: visible;
transition: all .25s ease-in-out;
}
.loading-screen.loaded {
opacity: 0;
visibility: hidden;
}
/* FORMS */
.pure-form {
display: inline-block;
}
/* ALERTS */
.alert {
padding: .5rem;
color: darkgreen;
background: darkseagreen;
border: 1px solid darkgreen;
}

35
src/index.js

@ -1,18 +1,25 @@
import React from 'react';
import { render } from 'react-dom';
import { Router, Route, IndexRoute, browserHistory } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'
import { DrizzleProvider } from 'drizzle-react'
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
import { DrizzleProvider } from 'drizzle-react';
// Layouts
import App from './App'
import HomeContainer from './layouts/home/HomeContainer'
import LoadingContainer from './containers/LoadingContainer'
// Layout
import CoreLayout from './layouts/CoreLayout/CoreLayout';
import store from './store'
import drizzleOptions from './util/drizzleOptions'
// Containers
import LoadingContainer from './containers/LoadingContainer';
import PrivateRouteContainer from './containers/PrivateRouteContainer';
import './css/index.css'
import HomeContainer from './containers/HomeContainer';
import TopicContainer from './containers/TopicContainer';
import ProfileContainer from './containers/ProfileContainer';
import NotFoundView from './components/NotFoundView';
import store from './redux/store';
import drizzleOptions from './util/drizzleOptions';
import './assets/css/index.css';
// Initialize react-router-redux.
const history = syncHistoryWithStore(browserHistory, store);
@ -21,12 +28,16 @@ render((
<DrizzleProvider options={drizzleOptions} store={store}>
<LoadingContainer>
<Router history={history}>
<Route path="/" component={App}>
<Route path="/" component={CoreLayout}>
<IndexRoute component={HomeContainer} />
<PrivateRouteContainer path="/topic/:topicId/:topicSubject" component={TopicContainer} redirectTo="/" />
<PrivateRouteContainer path='/profile' component={ProfileContainer} redirectTo="/" />
<Route path='/404' component={NotFoundView} />
<Route path='*' component={NotFoundView} />
</Route>
</Router>
</LoadingContainer>
</DrizzleProvider>
),
document.getElementById('root')
);
);

26
src/layouts/CoreLayout/CoreLayout.js

@ -0,0 +1,26 @@
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/oswald.css';
import '../../assets/css/open-sans.css';
import '../../assets/css/pure-min.css';
import '../../assets/css/App.css';
class CoreLayout extends Component {
render() {
return (
<div className="App">
<HeaderBar/>
<NavBar/>
<div className="view-container">
{this.props.children}
</div>
</div>
);
}
}
export default CoreLayout;

41
src/layouts/home/HomeContainer.js

@ -1,41 +0,0 @@
import { drizzleConnect } from 'drizzle-react'
import React, { Component } from 'react'
import UsernameFormContainer from '../../containers/UsernameFormContainer'
class Home extends Component {
render() {
return (
<main className="container">
<div className="pure-g">
<div className="pure-u-1-1 header">
<h1>Apella</h1>
<br/><br/>
</div>
<div className="pure-u-1-1">
<h2>Account</h2>
<p><strong>Username</strong>: {this.props.user.username}</p>
<p><strong>Account</strong>: {this.props.user.address}</p>
<p><strong>OrbitDB</strong>: {this.props.orbitDB.id}</p>
<UsernameFormContainer/>
<br/><br/>
</div>
</div>
</main>
)
}
}
// 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
}
};
const HomeContainer = drizzleConnect(Home, mapStateToProps);
export default HomeContainer

2
src/contractReducer.js → src/redux/reducer/contractReducer.js

@ -13,4 +13,4 @@ const contractReducer = (state = initialState, action) => {
}
};
export default contractReducer
export default contractReducer;

10
src/reducer.js → src/redux/reducer/reducer.js

@ -1,9 +1,9 @@
import { combineReducers } from 'redux'
import { routerReducer } from 'react-router-redux'
import { drizzleReducers } from 'drizzle'
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import { drizzleReducers } from 'drizzle';
import userReducer from "./userReducer";
import contractReducer from "./contractReducer";
import orbitReducer from "./util/orbitReducer";
import orbitReducer from "../../util/orbitReducer";
const reducer = combineReducers({
routing: routerReducer,
@ -13,4 +13,4 @@ const reducer = combineReducers({
...drizzleReducers
});
export default reducer
export default reducer;

3
src/userReducer.js → src/redux/reducer/userReducer.js

@ -1,6 +1,7 @@
const initialState = {
username: "",
address: "0x0",
avatarUrl: "",
hasSignedUp: null
};
@ -23,4 +24,4 @@ const userReducer = (state = initialState, action) => {
}
};
export default userReducer
export default userReducer;

0
src/contractSaga.js → src/redux/sagas/contractSaga.js

2
src/rootSaga.js → src/redux/sagas/rootSaga.js

@ -2,7 +2,7 @@ import { all, fork } from 'redux-saga/effects'
import { drizzleSagas } from 'drizzle'
import { contractSaga } from "./contractSaga";
import userSaga from "./userSaga";
import orbitSaga from "./util/orbitSaga";
import orbitSaga from "../../util/orbitSaga";
export default function* root() {
let sagas = [...drizzleSagas,userSaga,orbitSaga,contractSaga];

0
src/userSaga.js → src/redux/sagas/userSaga.js

8
src/store.js → 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'
import rootSaga from './rootSaga'
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 drizzleOptions from '../util/drizzleOptions'
// Redux DevTools
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
@ -30,4 +30,4 @@ const store = createStore(
sagaMiddleware.run(rootSaga);
export default store
export default store;

BIN
src/resources/PageNotFound.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
src/resources/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

2
src/util/drizzleOptions.js

@ -19,4 +19,4 @@ const drizzleOptions = {
},
};
export default drizzleOptions
export default drizzleOptions;

6
src/util/orbit.js

@ -2,7 +2,7 @@ import IPFS from 'ipfs';
import OrbitDB from 'orbit-db';
import Keystore from 'orbit-db-keystore';
import path from 'path';
import store from './../store';
import store from './../redux/store';
// OrbitDB uses Pubsub which is an experimental feature
// and need to be turned on manually.
@ -45,6 +45,4 @@ async function loadDatabases(id,topicsDB, postsDB,publicKey,privateKey) { //TO
store.dispatch({type: "DATABASES_LOADED", id: orbitdb.id});
}
export { createDatabases, loadDatabases }
export { createDatabases, loadDatabases };

2
src/util/orbitReducer.js

@ -34,4 +34,4 @@ const orbitReducer = (state = initialState, action) => {
}
};
export default orbitReducer
export default orbitReducer;

2
src/util/orbitSaga.js

@ -1,6 +1,6 @@
import { loadDatabases } from './../util/orbit'
import { call, put, select, takeLatest } from 'redux-saga/effects'
import {grabbedContract as contract} from "../contractSaga";
import {grabbedContract as contract} from "../redux/sagas/contractSaga";
const accounts = (state) => state.accounts;

Loading…
Cancel
Save