mirror of
https://github.com/fazo96/ipfs-boards
synced 2025-01-24 14:44:19 +01:00
WIP next.js version
This commit is contained in:
parent
e90e840a60
commit
bdcc6c1274
13
.gitignore
vendored
13
.gitignore
vendored
@ -1,20 +1,11 @@
|
||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
# build
|
||||
.next/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
12
README.md
12
README.md
@ -28,7 +28,7 @@ is the underlying library
|
||||
|
||||
## Try it out
|
||||
|
||||
Super early build hosted on IPFS:
|
||||
Years old build hosted on IPFS, probably broken by now:
|
||||
|
||||
https://ipfs.io/ipfs/QmYT9EzvQY8zwtxQxUhPcphSGR4XtTRkT4MnXmQKPFamQ7
|
||||
|
||||
@ -37,17 +37,15 @@ or commenting yet and a lot of things are super wonky
|
||||
|
||||
## Working on the code
|
||||
|
||||
This is a react project using redux, react-router-redux and redux-saga
|
||||
|
||||
The UI is being implemented using semantic-ui-react
|
||||
This is a next.js project, a React framework.
|
||||
|
||||
Clone this repo, then run
|
||||
|
||||
- `npm install` to install dependencies
|
||||
- `npm start` to start a development server
|
||||
- `npm run build` to create a production build
|
||||
- `npm run dev` to start a development server
|
||||
- `npm run build` to create a production build then `npm start` to start the frontend server
|
||||
|
||||
## Old Version
|
||||
|
||||
You're looking at the new implementation of Boards. If you want to check out the
|
||||
old one [follow this link](https://github.com/fazo96/ipfs-boards/tree/legacy)
|
||||
old one [follow this link](https://github.com/fazo96/ipfs-boards/tree/legacy)
|
||||
|
56
components/nav.js
Normal file
56
components/nav.js
Normal file
@ -0,0 +1,56 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
const links = [
|
||||
{ href: 'https://zeit.co/now', label: 'ZEIT' },
|
||||
{ href: 'https://github.com/zeit/next.js', label: 'GitHub' }
|
||||
].map(link => {
|
||||
link.key = `nav-link-${link.href}-${link.label}`
|
||||
return link
|
||||
})
|
||||
|
||||
const Nav = () => (
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<Link href='/'>
|
||||
<a>Home</a>
|
||||
</Link>
|
||||
</li>
|
||||
{links.map(({ key, href, label }) => (
|
||||
<li key={key}>
|
||||
<a href={href}>{label}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<style jsx>{`
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Avenir Next, Avenir,
|
||||
Helvetica, sans-serif;
|
||||
}
|
||||
nav {
|
||||
text-align: center;
|
||||
}
|
||||
ul {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
nav > ul {
|
||||
padding: 4px 16px;
|
||||
}
|
||||
li {
|
||||
display: flex;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
a {
|
||||
color: #067df7;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
}
|
||||
`}</style>
|
||||
</nav>
|
||||
)
|
||||
|
||||
export default Nav
|
20949
package-lock.json
generated
20949
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@ -1,36 +1,18 @@
|
||||
{
|
||||
"name": "ipfs-boards",
|
||||
"version": "0.1.0",
|
||||
"homepage": ".",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"connected-react-router": "^6.3.1",
|
||||
"ipfs": "~0.33.0",
|
||||
"moment": "^2.24.0",
|
||||
"orbit-db": "~0.19.9",
|
||||
"orbit-db-discussion-board": "https://github.com/fazo96/orbit-db-discussion-board.git",
|
||||
"react": "^16.8.4",
|
||||
"react-dom": "^16.8.4",
|
||||
"react-hot-loader": "^4.8.0",
|
||||
"react-redux": "^6.0.1",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"react-scripts": "2.1.8",
|
||||
"redux": "^4.0.1",
|
||||
"redux-saga": "^1.0.2",
|
||||
"semantic-ui-css": "^2.4.1",
|
||||
"semantic-ui-react": "^0.85.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build"
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"devDependencies": {
|
||||
"redux-immutable-state-invariant": "^2.1.0"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all"
|
||||
]
|
||||
"dependencies": {
|
||||
"next": "9.1.1",
|
||||
"ipfs": "~0.38.0",
|
||||
"orbit-db": "~0.22.0",
|
||||
"orbit-db-discussion-board": "https://github.com/fazo96/orbit-db-discussion-board.git",
|
||||
"react": "16.10.2",
|
||||
"react-dom": "16.10.2"
|
||||
}
|
||||
}
|
||||
|
88
pages/index.js
Normal file
88
pages/index.js
Normal file
@ -0,0 +1,88 @@
|
||||
import React from 'react'
|
||||
import Head from 'next/head'
|
||||
import Nav from '../components/nav'
|
||||
|
||||
const Home = () => (
|
||||
<div>
|
||||
<Head>
|
||||
<title>Home</title>
|
||||
<link rel='icon' href='/favicon.ico' />
|
||||
</Head>
|
||||
|
||||
<Nav />
|
||||
|
||||
<div className='hero'>
|
||||
<h1 className='title'>Welcome to Next.js!</h1>
|
||||
<p className='description'>
|
||||
To get started, edit <code>pages/index.js</code> and save to reload.
|
||||
</p>
|
||||
|
||||
<div className='row'>
|
||||
<a href='https://nextjs.org/docs' className='card'>
|
||||
<h3>Documentation →</h3>
|
||||
<p>Learn more about Next.js in the documentation.</p>
|
||||
</a>
|
||||
<a href='https://nextjs.org/learn' className='card'>
|
||||
<h3>Next.js Learn →</h3>
|
||||
<p>Learn about Next.js by following an interactive tutorial!</p>
|
||||
</a>
|
||||
<a
|
||||
href='https://github.com/zeit/next.js/tree/master/examples'
|
||||
className='card'
|
||||
>
|
||||
<h3>Examples →</h3>
|
||||
<p>Find other example boilerplates on the Next.js GitHub.</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.hero {
|
||||
width: 100%;
|
||||
color: #333;
|
||||
}
|
||||
.title {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
padding-top: 80px;
|
||||
line-height: 1.15;
|
||||
font-size: 48px;
|
||||
}
|
||||
.title,
|
||||
.description {
|
||||
text-align: center;
|
||||
}
|
||||
.row {
|
||||
max-width: 880px;
|
||||
margin: 80px auto 40px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.card {
|
||||
padding: 18px 18px 24px;
|
||||
width: 220px;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
color: #434343;
|
||||
border: 1px solid #9b9b9b;
|
||||
}
|
||||
.card:hover {
|
||||
border-color: #067df7;
|
||||
}
|
||||
.card h3 {
|
||||
margin: 0;
|
||||
color: #067df7;
|
||||
font-size: 18px;
|
||||
}
|
||||
.card p {
|
||||
margin: 0;
|
||||
padding: 12px 0 0;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default Home
|
@ -1,40 +0,0 @@
|
||||
<!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">
|
||||
<!--
|
||||
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">
|
||||
<!--
|
||||
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.
|
||||
|
||||
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`.
|
||||
-->
|
||||
<title>IPFS Boards</title>
|
||||
</head>
|
||||
<body>
|
||||
<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.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
@ -1,15 +0,0 @@
|
||||
{
|
||||
"short_name": "Boards",
|
||||
"name": "IPFS Boards",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
|
||||
export const ADD_POST = 'ADD_POST'
|
||||
export const EDIT_POST = 'EDIT_POST'
|
||||
export const HIDE_POST = 'HIDE_POST'
|
||||
export const UPDATE_BOARD_METADATA = 'UPDATE_BOARD_METADATA'
|
||||
|
||||
export const ADD_COMMENT = 'ADD_COMMENT'
|
||||
export const EDIT_COMMENT = 'EDIT_COMMENT'
|
||||
export const HIDE_COMMENT = 'HIDE_COMMENT'
|
||||
|
||||
export const OPEN_BOARD = 'OPEN_BOARD'
|
||||
export const OPENED_BOARD = 'OPENED_BOARD'
|
||||
export const CLOSE_BOARD = 'CLOSE_BOARD'
|
||||
|
||||
export const UPDATE_BOARD = 'UPDATE_BOARD'
|
||||
|
||||
export const ORBITDB_WRITE = 'ORBITDB_WRITE'
|
||||
|
||||
export const ORBITDB_REPLICATE = 'ORBITDB_REPLICATE'
|
||||
export const ORBITDB_REPLICATE_PROGRESS = 'ORBITDB_REPLICATE_PROGRESS'
|
||||
export const ORBITDB_REPLICATED = 'ORBITDB_REPLICATED'
|
||||
|
||||
export const ERROR = 'ERROR'
|
@ -1,35 +0,0 @@
|
||||
import {
|
||||
OPEN_BOARD,
|
||||
CLOSE_BOARD,
|
||||
OPENED_BOARD,
|
||||
UPDATE_BOARD_METADATA
|
||||
} from './actionTypes'
|
||||
|
||||
export function openBoard(board) {
|
||||
return {
|
||||
type: OPEN_BOARD,
|
||||
board
|
||||
}
|
||||
}
|
||||
|
||||
export function createdBoard(board) {
|
||||
return {
|
||||
type: OPENED_BOARD,
|
||||
board
|
||||
}
|
||||
}
|
||||
|
||||
export function updateBoardMetadata(address, metadata) {
|
||||
return {
|
||||
type: UPDATE_BOARD_METADATA,
|
||||
address,
|
||||
metadata
|
||||
}
|
||||
}
|
||||
|
||||
export function closeBoard(address) {
|
||||
return {
|
||||
type: CLOSE_BOARD,
|
||||
address
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import { HIDE_COMMENT, ADD_COMMENT, EDIT_COMMENT } from './actionTypes'
|
||||
|
||||
export function addComment(address, postId, comment, replyTo = 'post') {
|
||||
return {
|
||||
type: ADD_COMMENT,
|
||||
address,
|
||||
postId,
|
||||
comment,
|
||||
replyTo
|
||||
}
|
||||
}
|
||||
|
||||
export function editComment(address, postId, commentId, comment, replyTo = 'post') {
|
||||
return {
|
||||
type: EDIT_COMMENT,
|
||||
address,
|
||||
postId,
|
||||
commentId,
|
||||
comment,
|
||||
replyTo
|
||||
}
|
||||
}
|
||||
|
||||
export function hideComment(address, postId, commentId, replyTo = 'post') {
|
||||
return {
|
||||
type: HIDE_COMMENT,
|
||||
address,
|
||||
postId,
|
||||
commentId,
|
||||
replyTo
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import { ADD_POST, EDIT_POST, HIDE_POST } from './actionTypes'
|
||||
|
||||
export function addPost(address, post) {
|
||||
return {
|
||||
type: ADD_POST,
|
||||
post,
|
||||
address
|
||||
}
|
||||
}
|
||||
|
||||
export function editPost(address, postId, post) {
|
||||
return {
|
||||
type: EDIT_POST,
|
||||
address,
|
||||
postId,
|
||||
post,
|
||||
}
|
||||
}
|
||||
|
||||
export function hidePost(address, postId) {
|
||||
return {
|
||||
type: HIDE_POST,
|
||||
address,
|
||||
postId
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Switch, Route, withRouter } from 'react-router-dom'
|
||||
import Boards from '../containers/Boards'
|
||||
import OpenBoard from '../containers/OpenBoard'
|
||||
import WithBoard from '../containers/WithBoard'
|
||||
import BoardPage from '../components/BoardPage'
|
||||
import 'semantic-ui-css/semantic.css'
|
||||
|
||||
class App extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path='/b/new' component={OpenBoard} />
|
||||
<Route path='/b/:hash/:name/' component={withRouter(WithBoard(BoardPage))} />
|
||||
<Route path='/' component={Boards} />
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(App)
|
@ -1,108 +0,0 @@
|
||||
import React from 'react'
|
||||
import Post from './Post'
|
||||
import { Divider, Icon, Grid, Header, List, Button, Card } from 'semantic-ui-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { shortenAddress } from '../utils/orbitdb';
|
||||
import moment from 'moment'
|
||||
|
||||
export default function Board({ address, posts, metadata, replicating, stats, replicationInfo, lastReplicated }) {
|
||||
const { email, website, title, description } = metadata || {}
|
||||
const peerCount = (stats.peers || []).length
|
||||
const online = peerCount > 0
|
||||
const writeable = stats.access ? (stats.access.writeable ? 'Yes' : 'No') : '?'
|
||||
let replicationMessage = lastReplicated ? ('Last Activity at ' + moment(lastReplicated).format('H:mm')) : 'No Activity'
|
||||
if (replicating) {
|
||||
if (replicationInfo && replicationInfo.max !== undefined) {
|
||||
replicationMessage = 'Progress: ' + (replicationInfo.progress || 0) + '/' + replicationInfo.max
|
||||
} else {
|
||||
replicationMessage = 'Initializing Transfer'
|
||||
}
|
||||
}
|
||||
return <Grid container divided colums={2}>
|
||||
<Grid.Column width={8}>
|
||||
<Header size='large' style={{marginTop:'.5em'}}>
|
||||
<Header.Content>{title || 'Unnamed Board'}</Header.Content>
|
||||
<Header.Subheader>Board</Header.Subheader>
|
||||
</Header>
|
||||
{ description ? <p>{description}</p> : null }
|
||||
<Divider />
|
||||
<List relaxed>
|
||||
<List.Item>
|
||||
<List.Icon name='linkify' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Address</List.Header>
|
||||
<List.Content>
|
||||
{address}
|
||||
</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Icon name='disk outline' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Size</List.Header>
|
||||
<List.Content>{stats.opLogLength || 0} Entries</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Icon name={online ? 'heart' : 'heartbeat'} size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>{online ? 'Online' : 'Offline'}</List.Header>
|
||||
<List.Content>{online ? peerCount + ' Connections' : 'No Connections'}</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Icon color={replicating ? 'green' : null} name='feed' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>{replicating ? 'Downloading' : 'Download'}</List.Header>
|
||||
<List.Content>{replicationMessage}</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Icon name='edit' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Write Access</List.Header>
|
||||
<List.Content>{writeable}</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<Divider/>
|
||||
<List.Item>
|
||||
<List.Icon name='file text outline' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Posts</List.Header>
|
||||
<List.Content>{Object.values(posts || {}).length}</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Icon name='globe' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Website</List.Header>
|
||||
<List.Content>{website ? <a href={website} target="__blank">{website}</a> : 'N/A'}</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Icon name='mail' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Mail</List.Header>
|
||||
<List.Content>{email ? <a href={'mailto:'+email}>{email}</a> : 'N/A'}</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
</List>
|
||||
<div className='ui three buttons basic'>
|
||||
<Button as={Link} to={'/'}>
|
||||
<Icon name='left arrow'/> Boards
|
||||
</Button>
|
||||
<Button disabled={!writeable} as={Link} to={shortenAddress(address)+'/edit'}>
|
||||
<Icon name='pencil'/> Edit
|
||||
</Button>
|
||||
<Button disabled={!writeable} as={Link} to={shortenAddress(address)+'/p/new'}>
|
||||
<Icon name='plus'/> New Post
|
||||
</Button>
|
||||
</div>
|
||||
</Grid.Column>
|
||||
<Grid.Column width={8}>
|
||||
<Card.Group className="centered" style={{marginTop:'.5em'}}>
|
||||
{Object.keys(posts || {}).map(i => <Post key={posts[i].multihash} {...posts[i]}/>)}
|
||||
</Card.Group>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Icon, Container, Card, Form, Button } from 'semantic-ui-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { shortenAddress } from '../utils/orbitdb';
|
||||
|
||||
export default class BoardEditorForm extends Component {
|
||||
constructor(props){
|
||||
super(props)
|
||||
this.state = {
|
||||
title: props.title || '',
|
||||
website: props.website || '',
|
||||
email: props.email || ''
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { title, website, email } = this.state
|
||||
const { address, updateBoardMetadata } = this.props
|
||||
return <Container>
|
||||
<Card fluid centered style={{marginTop:'5em',maxWidth:'40em'}}>
|
||||
<Card.Content>
|
||||
<Card.Header>Edit Board</Card.Header>
|
||||
<Card.Meta>
|
||||
Boards is an experimental peer to peer application.<br/>
|
||||
All content you publish will be public and may be lost or
|
||||
changed at any time.<br/>
|
||||
Please do not use this version of Boards
|
||||
for anything other than testing purposes
|
||||
</Card.Meta>
|
||||
</Card.Content>
|
||||
<Card.Content>
|
||||
<Form>
|
||||
<Form.Field>
|
||||
<label>Title</label>
|
||||
<input
|
||||
value={title}
|
||||
onChange={this.updateTitle.bind(this)}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Group widths="equal">
|
||||
<Form.Field>
|
||||
<label>Website</label>
|
||||
<input
|
||||
value={website}
|
||||
onChange={this.updateWebsite.bind(this)}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>Email</label>
|
||||
<input
|
||||
value={email}
|
||||
type="email"
|
||||
onChange={this.updateEmail.bind(this)}
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form.Group>
|
||||
<div className="ui two buttons">
|
||||
<Button as={Link} to={shortenAddress(address)}>
|
||||
<Icon name="arrow left"/> Back
|
||||
</Button>
|
||||
<Button type="submit" onClick={() => updateBoardMetadata(address, this.state)}>
|
||||
<Icon name="save"/> Save
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</Container>
|
||||
}
|
||||
|
||||
updateTitle(event) {
|
||||
const title = event.target.value
|
||||
this.setState({ title })
|
||||
}
|
||||
|
||||
updateWebsite(event) {
|
||||
const website = event.target.value
|
||||
this.setState({ website })
|
||||
}
|
||||
|
||||
updateEmail(event) {
|
||||
const email = event.target.value
|
||||
this.setState({ email })
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Switch, Route } from 'react-router-dom'
|
||||
import Board from '../containers/Board'
|
||||
import BoardEditor from '../containers/BoardEditor'
|
||||
import PostEditor from '../containers/PostEditor'
|
||||
import WithStats from '../containers/WithStats'
|
||||
|
||||
function BoardPage({ match, address, posts, metadata }) {
|
||||
return <Switch>
|
||||
<Route path={match.path+'p/new'} component={PostEditor} />
|
||||
<Route path={match.path+'edit'} component={BoardEditor} />
|
||||
<Route path={match.path} component={WithStats(Board)} />
|
||||
</Switch>
|
||||
}
|
||||
|
||||
export default BoardPage
|
@ -1,73 +0,0 @@
|
||||
import React from 'react'
|
||||
import { List, Icon, Segment, Divider, Grid, Header, Button, Card } from 'semantic-ui-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import BoardsItem from './BoardsItem'
|
||||
|
||||
export default function Boards({ stats, boards, createBoard, closeBoard }) {
|
||||
return <Grid container divided colums={2}>
|
||||
<Grid.Column width={8}>
|
||||
<Header size='large' style={{marginTop:'.5em'}}>
|
||||
<Icon name="cube" color="blue" circular/>
|
||||
<Header.Content>
|
||||
<Header.Content>IPFS Boards</Header.Content>
|
||||
<Header.Subheader>Experimental Build</Header.Subheader>
|
||||
</Header.Content>
|
||||
</Header>
|
||||
<Divider />
|
||||
<List relaxed>
|
||||
<List.Item>
|
||||
<List.Icon name='leaf' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Seeding</List.Header>
|
||||
<List.Content>{Object.keys(boards).length} Boards</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Icon name='signal' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Connected Peers</List.Header>
|
||||
<List.Content>{stats.peers.length}</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Icon name='disk outline' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Used Space</List.Header>
|
||||
<List.Content>Not Supported Yet</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Icon name='user circle' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>IPFS ID</List.Header>
|
||||
<List.Content>{stats.id}</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Icon name='key' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>OrbitDB Public Key</List.Header>
|
||||
<List.Content style={{wordBreak:'break-all'}}>{stats.pubKey}</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
</List>
|
||||
<div className="ui three buttons">
|
||||
<Button as='a' href="https://github.com/fazo96/ipfs-boards" target="__blank" >
|
||||
<Icon name="github"/> GitHub
|
||||
</Button>
|
||||
<Button as={Link} to={'/b/new'}>
|
||||
<Icon name="plus"/> Add Board
|
||||
</Button>
|
||||
<Button as='a' href="https://github.com/fazo96/ipfs-boards/issues/new" target="__blank">
|
||||
<Icon name="comment"/> Leave Feedback
|
||||
</Button>
|
||||
</div>
|
||||
</Grid.Column>
|
||||
<Grid.Column width={8} style={{paddingTop:'3em'}}>
|
||||
<Card.Group className="centered">
|
||||
{Object.values(boards).map(board => <BoardsItem key={board.address} closeBoard={closeBoard} {...board} />)}
|
||||
{Object.keys(boards).length === 0 ? <Segment>No boards opened</Segment> : null}
|
||||
</Card.Group>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Icon, List, Button, Card } from 'semantic-ui-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { shortenAddress } from '../utils/orbitdb'
|
||||
|
||||
export default function BoardsItem({ address, metadata, name, closeBoard }) {
|
||||
return <Card fluid>
|
||||
<Card.Content>
|
||||
<Card.Header>
|
||||
{ metadata.title || 'Unnamed board' }
|
||||
</Card.Header>
|
||||
<Card.Meta>Board</Card.Meta>
|
||||
</Card.Content>
|
||||
<Card.Content>
|
||||
<List>
|
||||
<List.Item>
|
||||
<List.Icon name="hashtag" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Name</List.Header>
|
||||
<List.Content>{name}</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Icon name="chain" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Address</List.Header>
|
||||
<List.Content>{address}</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
</List>
|
||||
</Card.Content>
|
||||
<Card.Content>
|
||||
<div className="ui two buttons">
|
||||
<Button onClick={() => closeBoard(address)} basic>
|
||||
<Icon name="close"/> Close
|
||||
</Button>
|
||||
<Button as={Link} to={shortenAddress(address)} basic>
|
||||
<Icon name="list"/> View
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Icon, Container, Card, Form, Button } from 'semantic-ui-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export default class OpenBoardForm extends Component {
|
||||
constructor(props){
|
||||
super(props)
|
||||
this.state = {
|
||||
address: props.address || ''
|
||||
}
|
||||
}
|
||||
|
||||
updateAddress(event) {
|
||||
const address = event.target.value
|
||||
this.setState({ address })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { address } = this.state
|
||||
const { openBoard, opening } = this.props
|
||||
return <Container>
|
||||
<Card fluid centered style={{marginTop:'5em',maxWidth:'40em'}}>
|
||||
<Card.Content>
|
||||
<Card.Header>Open a Board</Card.Header>
|
||||
<Card.Meta>
|
||||
Boards is an experimental peer to peer application.<br/>
|
||||
All content you publish will be public and may be lost or
|
||||
changed at any time.<br/>
|
||||
Please do not use this version of Boards
|
||||
for anything other than testing purposes
|
||||
</Card.Meta>
|
||||
</Card.Content>
|
||||
<Card.Content>
|
||||
<Form loading={opening}>
|
||||
<Form.Field>
|
||||
<input
|
||||
placeholder="Paste an existing address or write your new board ID"
|
||||
value={address}
|
||||
onChange={this.updateAddress.bind(this)}
|
||||
/>
|
||||
</Form.Field>
|
||||
<div className="ui two buttons">
|
||||
<Button as={Link} to={'/'}>
|
||||
<Icon name="arrow left"/> Back
|
||||
</Button>
|
||||
<Button type="submit" onClick={() => openBoard({ address, redirect: true })}>
|
||||
<Icon name="plus"/> Open
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</Container>
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import React from 'react'
|
||||
import { List, Card } from 'semantic-ui-react'
|
||||
|
||||
export default function Post({ title, multihash, pubKey }) {
|
||||
return <Card fluid>
|
||||
<Card.Content>
|
||||
<Card.Header>
|
||||
{title}
|
||||
</Card.Header>
|
||||
<Card.Meta>Post</Card.Meta>
|
||||
</Card.Content>
|
||||
<Card.Content style={{wordBreak:'break-all'}}>
|
||||
<List>
|
||||
<List.Item>
|
||||
<List.Icon name="key" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Signed By</List.Header>
|
||||
<List.Content>{pubKey || 'Unknown'}</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Icon name="comments" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Comments</List.Header>
|
||||
<List.Content>Not Supported Yet</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Icon name="chain" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Content</List.Header>
|
||||
<List.Content>
|
||||
<a href={'//ipfs.io/ipfs/'+multihash}>{multihash}</a>
|
||||
</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
</List>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Card, Container, Form, Icon, Button } from 'semantic-ui-react'
|
||||
import { shortenAddress } from '../utils/orbitdb';
|
||||
|
||||
export default class PostForm extends Component {
|
||||
constructor(props){
|
||||
super(props)
|
||||
this.state = {
|
||||
title: props.title || '',
|
||||
content: props.content || ''
|
||||
}
|
||||
}
|
||||
|
||||
updateTitle(event) {
|
||||
this.setState({ title: event.target.value })
|
||||
}
|
||||
|
||||
updateContent(event) {
|
||||
this.setState({ content: event.target.value })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { title, content } = this.state
|
||||
const { onSave, board } = this.props
|
||||
const { address } = board
|
||||
return <Container>
|
||||
<Card fluid centered style={{marginTop:'5em',maxWidth:'40em'}}>
|
||||
<Card.Content>
|
||||
<Card.Header>New Post</Card.Header>
|
||||
<Card.Meta>
|
||||
Boards is an experimental peer to peer application.<br/>
|
||||
All content you publish will be public and may be lost or
|
||||
changed at any time.<br/>
|
||||
Please do not use this version of Boards
|
||||
for anything other than testing purposes
|
||||
</Card.Meta>
|
||||
</Card.Content>
|
||||
<Card.Content extra>
|
||||
<Icon name="chain"/> {address}
|
||||
</Card.Content>
|
||||
<Card.Content>
|
||||
<Form>
|
||||
<Form.Field>
|
||||
<label>Title</label>
|
||||
<input
|
||||
placeholder="What's this about?"
|
||||
value={title}
|
||||
onChange={this.updateTitle.bind(this)}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>Content</label>
|
||||
<input
|
||||
placeholder='Write your thoughts'
|
||||
value={content}
|
||||
onChange={this.updateContent.bind(this)}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Button as={Link} to={shortenAddress(address)}>
|
||||
<Icon name="chevron left"/> Board
|
||||
</Button>
|
||||
<Button type='submit' onClick={() => onSave({ title, text: content })}>
|
||||
<Icon name="save"/> Submit
|
||||
</Button>
|
||||
</Form>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</Container>
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import BoardComponent from '../components/Board'
|
||||
import { getBoardAddress } from '../utils/orbitdb'
|
||||
|
||||
function Board({ stats, location, match, boards }) {
|
||||
const { hash, name } = match.params
|
||||
const address = getBoardAddress(hash, name)
|
||||
const boardStats = stats.dbs[address] || {}
|
||||
return <BoardComponent stats={boardStats} {...boards[address]} />
|
||||
}
|
||||
|
||||
function mapStateToProps(state){
|
||||
return {
|
||||
boards: state.boards.boards
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(Board)
|
@ -1,35 +0,0 @@
|
||||
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import BoardEditorForm from '../components/BoardEditorForm'
|
||||
import { updateBoardMetadata } from '../actions/board'
|
||||
import { getBoardAddress } from '../utils/orbitdb'
|
||||
|
||||
function BoardEditor({ boards, boardEditor, match, updateBoardMetadata }) {
|
||||
const { hash, name } = match.params
|
||||
const address = getBoardAddress(hash, name)
|
||||
const board = boards[address]
|
||||
return <BoardEditorForm
|
||||
board={board}
|
||||
address={address}
|
||||
updateBoardMetadata={updateBoardMetadata}
|
||||
{...board.metadata}
|
||||
/>
|
||||
}
|
||||
|
||||
function mapStateToProps(state){
|
||||
return {
|
||||
boards: state.boards.boards
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
updateBoardMetadata: (address, metadata) => dispatch(updateBoardMetadata(address, metadata))
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BoardEditor)
|
@ -1,34 +0,0 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { push } from 'react-router-redux'
|
||||
import BoardsComponent from '../components/Boards'
|
||||
import WithStats from './WithStats'
|
||||
import { closeBoard } from '../actions/board'
|
||||
|
||||
const WrappedComponent = WithStats(BoardsComponent)
|
||||
|
||||
function Boards({ boards, createBoard, closeBoard }) {
|
||||
return <WrappedComponent
|
||||
boards={boards}
|
||||
createBoard={createBoard}
|
||||
closeBoard={closeBoard}
|
||||
/>
|
||||
}
|
||||
|
||||
function mapStateToProps(state){
|
||||
return {
|
||||
boards: state.boards.boards
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch){
|
||||
return {
|
||||
createBoard: () => dispatch(push('/b/new')),
|
||||
closeBoard: address => dispatch(closeBoard(address)),
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Boards)
|
@ -1,25 +0,0 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import OpenBoardForm from '../components/OpenBoardForm'
|
||||
import { openBoard } from '../actions/board'
|
||||
|
||||
function OpenBoard(props) {
|
||||
return <OpenBoardForm {...props} />
|
||||
}
|
||||
|
||||
function mapStateToProps(state){
|
||||
return {
|
||||
opening: state.openBoard.opening
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
openBoard: board => dispatch(openBoard(board))
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(OpenBoard)
|
@ -1,32 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import PostForm from '../components/PostForm'
|
||||
import { addPost } from '../actions/post'
|
||||
import { getBoardAddress } from '../utils/orbitdb';
|
||||
|
||||
class PostEditor extends Component {
|
||||
render() {
|
||||
const { post, addPost, match, boards } = this.props
|
||||
const address = getBoardAddress(match.params.hash, match.params.name)
|
||||
const board = boards[address]
|
||||
return <PostForm post={post} board={board} onSave={p => addPost(address, p)} />
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state){
|
||||
return {
|
||||
post: state.postEditor.post,
|
||||
boards: state.boards.boards
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
addPost: (address, post) => dispatch(addPost(address, post))
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(PostEditor)
|
@ -1,56 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Dimmer } from 'semantic-ui-react'
|
||||
import { connect } from 'react-redux'
|
||||
import { openBoard } from '../actions/board'
|
||||
import { getBoardAddress } from '../utils/orbitdb'
|
||||
|
||||
function mapStateToProps(state){
|
||||
return {
|
||||
boards: state.boards.boards
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch){
|
||||
return {
|
||||
openBoard: address => dispatch(openBoard({ address, redirect: false }))
|
||||
}
|
||||
}
|
||||
|
||||
export default function WithBoard(WrappedComponent) {
|
||||
class ToExport extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
const { boards, match } = this.props
|
||||
const address = getBoardAddress(match.params.hash, match.params.name)
|
||||
if (!boards[address]) {
|
||||
this.props.openBoard(address)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps({ match, boards }) {
|
||||
const address = getBoardAddress(match.params.hash, match.params.name)
|
||||
if (!boards[address]) {
|
||||
this.props.openBoard(address)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { boards, match } = this.props
|
||||
const address = getBoardAddress(match.params.hash, match.params.name)
|
||||
const board = boards[address]
|
||||
if (board) {
|
||||
return <WrappedComponent {...board} {...this.props} />
|
||||
} else {
|
||||
return <Dimmer page active={true}>
|
||||
Opening this board
|
||||
</Dimmer>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ToExport)
|
||||
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import { getStats } from '../utils/ipfs'
|
||||
|
||||
export default function(WrappedComponent) {
|
||||
return class extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
stats: {
|
||||
id: '?',
|
||||
peers: [],
|
||||
pubKey: '?',
|
||||
dbs: {}
|
||||
},
|
||||
timeout: null
|
||||
}
|
||||
}
|
||||
|
||||
async refresh(loop = true) {
|
||||
const newStats = await getStats()
|
||||
const stats = Object.assign({}, this.state.stats, newStats)
|
||||
this.setState({ stats }, loop ? this.refreshDelayed.bind(this) : undefined)
|
||||
}
|
||||
|
||||
refreshDelayed() {
|
||||
this.timeout = setTimeout(() => {
|
||||
this.refresh()
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.refresh()
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.timeout) clearTimeout(this.timeout)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <WrappedComponent stats={this.state.stats} {...this.props} />
|
||||
}
|
||||
}
|
||||
}
|
42
src/index.js
42
src/index.js
@ -1,42 +0,0 @@
|
||||
import 'react-hot-loader/patch'
|
||||
import React from 'react'
|
||||
import { render } from 'react-dom'
|
||||
import { AppContainer } from 'react-hot-loader'
|
||||
import configureStore, { history } from './store/configureStore'
|
||||
import App from './components/App'
|
||||
import registerServiceWorker from './registerServiceWorker'
|
||||
import { Provider } from 'react-redux'
|
||||
import { ConnectedRouter } from 'connected-react-router'
|
||||
import { start } from './orbitdb'
|
||||
|
||||
const store = configureStore();
|
||||
|
||||
render(
|
||||
<AppContainer>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<App />
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
</AppContainer>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept('./components/App', () => {
|
||||
const NewApp = require('./components/App').default
|
||||
render(
|
||||
<AppContainer>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<NewApp />
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
</AppContainer>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
registerServiceWorker()
|
||||
start(store.dispatch)
|
@ -1,8 +0,0 @@
|
||||
|
||||
module.exports = {
|
||||
ADD_POST: 'ADD_POST',
|
||||
UPDATE_POST: 'UPDATE_POST',
|
||||
ADD_COMMENT: 'ADD_COMMENT',
|
||||
UPDATE_COMMENT: 'UPDATE_COMMENT',
|
||||
UPDATE_METADATA: 'UPDATE_METADATA',
|
||||
};
|
@ -1,112 +0,0 @@
|
||||
import IPFS from 'ipfs'
|
||||
import OrbitDB from 'orbit-db'
|
||||
import BoardStore from 'orbit-db-discussion-board'
|
||||
import multihashes from 'multihashes'
|
||||
|
||||
export function isValidID(id) {
|
||||
try {
|
||||
if (typeof id === 'string' && multihashes.fromB58String(id)) return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export async function start() {
|
||||
if (!window.ipfs) {
|
||||
window.ipfs = new IPFS({
|
||||
repo: 'ipfs-v6-boards-v0',
|
||||
EXPERIMENTAL: {
|
||||
pubsub: true
|
||||
}
|
||||
});
|
||||
await new Promise(resolve => {
|
||||
window.ipfs.on('ready', () => resolve())
|
||||
})
|
||||
}
|
||||
if (!window.orbitDb) {
|
||||
OrbitDB.addDatabaseType(BoardStore.type, BoardStore)
|
||||
window.orbitDb = new OrbitDB(window.ipfs)
|
||||
}
|
||||
}
|
||||
|
||||
export async function open(address, metadata) {
|
||||
if (window.dbs && window.dbs[address]) return window.dbs[address]
|
||||
await start()
|
||||
const options = {
|
||||
type: BoardStore.type,
|
||||
create: true,
|
||||
write: ['*']
|
||||
}
|
||||
const db = await window.orbitDb.open(address, options)
|
||||
await db.load()
|
||||
if (metadata) {
|
||||
await db.updateMetadata(metadata)
|
||||
}
|
||||
if (!window.dbs) window.dbs = {}
|
||||
window.dbs[db.address.toString()] = db
|
||||
return db
|
||||
}
|
||||
|
||||
export function connectDb(db, dispatch) {
|
||||
db.events.on('write', (dbname, hash, entry) => {
|
||||
dispatch({
|
||||
type: 'ORBITDB_WRITE',
|
||||
time: Date.now(),
|
||||
address: db.address.toString(),
|
||||
hash,
|
||||
entry
|
||||
})
|
||||
})
|
||||
db.events.on('replicated', address => {
|
||||
dispatch({
|
||||
type: 'ORBITDB_REPLICATED',
|
||||
time: Date.now(),
|
||||
address: db.address.toString()
|
||||
})
|
||||
})
|
||||
db.events.on('replicate.progress', (address, hash, entry, progress, have) => {
|
||||
dispatch({
|
||||
type: 'ORBITDB_REPLICATE_PROGRESS',
|
||||
address: db.address.toString(),
|
||||
hash,
|
||||
entry,
|
||||
progress,
|
||||
have,
|
||||
time: Date.now(),
|
||||
replicationInfo: Object.assign({}, db._replicationInfo)
|
||||
})
|
||||
})
|
||||
db.events.on('replicate', address => {
|
||||
dispatch({
|
||||
type: 'ORBITDB_REPLICATE',
|
||||
time: Date.now(),
|
||||
address: db.address.toString()
|
||||
})
|
||||
})
|
||||
db.events.on('close', address => {
|
||||
dispatch({
|
||||
type: 'ORBITDB_CLOSE',
|
||||
time: Date.now(),
|
||||
address: db.address.toString()
|
||||
})
|
||||
})
|
||||
db.events.on('load', address => {
|
||||
dispatch({
|
||||
type: 'ORBITDB_LOAD',
|
||||
time: Date.now(),
|
||||
address: db.address.toString()
|
||||
})
|
||||
})
|
||||
db.events.on('load.progress', (address, hash, entry, progress, total) => {
|
||||
dispatch({
|
||||
type: 'ORBITDB_LOAD_PROGRESS',
|
||||
time: Date.now(),
|
||||
address: db.address.toString(),
|
||||
hash,
|
||||
entry,
|
||||
progress,
|
||||
total
|
||||
})
|
||||
})
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
import {
|
||||
OPENED_BOARD,
|
||||
CLOSE_BOARD,
|
||||
UPDATE_BOARD,
|
||||
ORBITDB_REPLICATE_PROGRESS,
|
||||
ORBITDB_REPLICATED,
|
||||
ORBITDB_REPLICATE
|
||||
} from '../actions/actionTypes'
|
||||
|
||||
function getInitialState() {
|
||||
return {
|
||||
boards: {}
|
||||
}
|
||||
}
|
||||
|
||||
function updateBoard(existingBoards, address, value) {
|
||||
return Object.assign({}, existingBoards, {
|
||||
[address]: Object.assign({}, existingBoards[address] || {}, value)
|
||||
})
|
||||
}
|
||||
|
||||
function deleteBoard(existingBoards, address) {
|
||||
const boards = Object.assign({}, existingBoards)
|
||||
delete boards[address]
|
||||
return boards
|
||||
}
|
||||
|
||||
export default function BoardsReducer(state = getInitialState(), action) {
|
||||
let address
|
||||
switch (action.type) {
|
||||
case OPENED_BOARD:
|
||||
address = action.board.address
|
||||
return Object.assign({}, state, { boards: updateBoard(state.boards, address, Object.assign({}, action.board, { open: true })) })
|
||||
case UPDATE_BOARD:
|
||||
address = action.address
|
||||
let { posts, metadata } = action
|
||||
return Object.assign({}, state, { boards: updateBoard(state.boards, address, { posts, metadata })})
|
||||
case ORBITDB_REPLICATE:
|
||||
address = action.address
|
||||
return Object.assign({}, state, { boards: updateBoard(state.boards, address, {
|
||||
replicating: true
|
||||
})})
|
||||
case ORBITDB_REPLICATE_PROGRESS:
|
||||
address = action.address
|
||||
return Object.assign({}, state, { boards: updateBoard(state.boards, address, {
|
||||
replicating: true,
|
||||
replicationInfo: action.replicationInfo
|
||||
})})
|
||||
case ORBITDB_REPLICATED:
|
||||
address = action.address
|
||||
return Object.assign({}, state, { boards: updateBoard(state.boards, address, {
|
||||
replicating: false,
|
||||
lastReplicated: action.time
|
||||
})})
|
||||
case CLOSE_BOARD:
|
||||
address = action.address
|
||||
return Object.assign({}, state, {
|
||||
boards: deleteBoard(state.boards, address)
|
||||
})
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import { combineReducers } from 'redux'
|
||||
import { connectRouter} from 'connected-react-router'
|
||||
import postReducer from './post'
|
||||
import boardsReducer from './boards'
|
||||
import openBoardReducer from './openboard'
|
||||
|
||||
export default history => combineReducers({
|
||||
router: connectRouter(history),
|
||||
postEditor: postReducer,
|
||||
boards: boardsReducer,
|
||||
openBoard: openBoardReducer
|
||||
})
|
@ -1,21 +0,0 @@
|
||||
import {
|
||||
OPEN_BOARD,
|
||||
OPENED_BOARD
|
||||
} from '../actions/actionTypes'
|
||||
|
||||
function getInitialState() {
|
||||
return {
|
||||
opening: false
|
||||
}
|
||||
}
|
||||
|
||||
export default function openBoardReducer(state = getInitialState(), action) {
|
||||
switch (action.type) {
|
||||
case OPEN_BOARD:
|
||||
return Object.assign({}, state, { opening: true })
|
||||
case OPENED_BOARD:
|
||||
return Object.assign({}, state, { opening: false })
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { ADD_POST } from '../actions/actionTypes'
|
||||
|
||||
function getInitialState(){
|
||||
return {
|
||||
post: {
|
||||
title: '',
|
||||
content: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function(state = getInitialState(), action) {
|
||||
switch (action.type) {
|
||||
case ADD_POST:
|
||||
return Object.assign({}, state, { post: action.post })
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
// In production, we register a service worker to serve assets from local cache.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on the "N+1" visit to a page, since previously
|
||||
// cached resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
|
||||
// This link also includes instructions on opting out of this behavior.
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
export default function register() {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Lets check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://goo.gl/SC7cgQ'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not local host. Just register service worker
|
||||
registerValidSW(swUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the old content will have been purged and
|
||||
// the fresh content will have been added to the cache.
|
||||
// It's the perfect time to display a "New content is
|
||||
// available; please refresh." message in your web app.
|
||||
console.log('New content is available; please refresh.');
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl)
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
if (
|
||||
response.status === 404 ||
|
||||
response.headers.get('content-type').indexOf('javascript') === -1
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
import { put, call, fork, take } from 'redux-saga/effects'
|
||||
import { eventChannel } from 'redux-saga'
|
||||
import { push } from 'react-router-redux'
|
||||
import { open, connectDb } from '../orbitdb'
|
||||
import { createdBoard } from '../actions/board'
|
||||
import { shortenAddress, closeBoard as closeOrbitDBBoard } from '../utils/orbitdb'
|
||||
import { UPDATE_BOARD } from '../actions/actionTypes'
|
||||
import { saveSaga } from './persistence'
|
||||
|
||||
export function* goToBoard({ board }){
|
||||
if (board.redirect) {
|
||||
yield put(push(shortenAddress(board.address)))
|
||||
}
|
||||
}
|
||||
|
||||
export function* updateBoard({ address }){
|
||||
const db = window.dbs[address]
|
||||
yield put({
|
||||
type: UPDATE_BOARD,
|
||||
address,
|
||||
posts: db.posts,
|
||||
metadata: Object.assign({}, db._index._index.metadata) // TODO: fix in lib and use db.metadata
|
||||
})
|
||||
}
|
||||
|
||||
export function* closeBoard({ address }){
|
||||
yield call(closeOrbitDBBoard, address)
|
||||
yield saveSaga()
|
||||
}
|
||||
|
||||
export function* updateBoardMetadata({ address, metadata }){
|
||||
const db = window.dbs[address]
|
||||
if (db) {
|
||||
yield call([db, db.updateMetadata], [metadata])
|
||||
yield goToBoard({ board: { address } });
|
||||
} else {
|
||||
yield put({ type: 'ERROR', error: address + ' not found' })
|
||||
}
|
||||
}
|
||||
|
||||
export function* openBoard({ board }) {
|
||||
let db
|
||||
try {
|
||||
const metadata = board.title ? { title: board.title } : null
|
||||
db = yield call(open, board.address, metadata)
|
||||
} catch (error) {
|
||||
yield put({ type: 'ERROR', error })
|
||||
}
|
||||
if (db) {
|
||||
const address = db.address.toString()
|
||||
const dbInfo = { address }
|
||||
dbInfo.posts = db.posts
|
||||
dbInfo.metadata = Object.assign({}, db._index._index.metadata) // TODO: fix in lib and use db.metadata
|
||||
dbInfo.name = db.dbname
|
||||
try {
|
||||
const channel = yield call(createDbEventChannel, db)
|
||||
yield fork(watchDb, channel)
|
||||
yield put(createdBoard(Object.assign({ redirect: !!board.redirect }, board, dbInfo)))
|
||||
} catch (error) {
|
||||
yield put({ type: 'ERROR', error })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function* watchDb(channel) {
|
||||
// Dispatches action coming from the channel, terminates when ORBITDB_CLOSE arrives
|
||||
let action
|
||||
while(!action || action.type !== 'ORBITDB_CLOSE') {
|
||||
action = yield take(channel)
|
||||
yield put(action)
|
||||
}
|
||||
}
|
||||
|
||||
function createDbEventChannel(db) {
|
||||
return eventChannel(emitter => {
|
||||
connectDb(db, emitter)
|
||||
return () => db.close()
|
||||
})
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import { takeEvery, put, call } from 'redux-saga/effects'
|
||||
import {
|
||||
OPEN_BOARD,
|
||||
OPENED_BOARD,
|
||||
CLOSE_BOARD,
|
||||
ADD_POST,
|
||||
ORBITDB_REPLICATED,
|
||||
ORBITDB_WRITE,
|
||||
UPDATE_BOARD_METADATA
|
||||
} from '../actions/actionTypes'
|
||||
import { openBoard, updateBoard, goToBoard, updateBoardMetadata, closeBoard } from './boards'
|
||||
import { addPost } from './posts'
|
||||
import { openPreviousBoards, saveSaga } from './persistence'
|
||||
|
||||
export default function* saga(){
|
||||
yield takeEvery(OPEN_BOARD, openBoard)
|
||||
yield takeEvery(OPENED_BOARD, goToBoard)
|
||||
yield takeEvery(OPENED_BOARD, saveSaga)
|
||||
yield takeEvery(CLOSE_BOARD, closeBoard)
|
||||
|
||||
yield takeEvery(ADD_POST, addPost)
|
||||
yield takeEvery(UPDATE_BOARD_METADATA, updateBoardMetadata)
|
||||
|
||||
yield takeEvery(ORBITDB_WRITE, updateBoard)
|
||||
yield takeEvery(ORBITDB_REPLICATED, updateBoard)
|
||||
|
||||
yield openPreviousBoards()
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { call, put } from 'redux-saga/effects'
|
||||
import { openBoard } from '../actions/board'
|
||||
import { save, load } from '../utils/persistence'
|
||||
|
||||
export function* openPreviousBoards() {
|
||||
const data = yield call(load)
|
||||
if (Array.isArray(data.addresses)) {
|
||||
for (const address of data.addresses) {
|
||||
yield put(openBoard({ address, redirect: false }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* saveSaga() {
|
||||
yield call(save)
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import { call } from 'redux-saga/effects'
|
||||
import { goToBoard } from './boards';
|
||||
|
||||
export function* addPost({ address, post }) {
|
||||
const db = window.dbs[address]
|
||||
const { title, text } = post
|
||||
yield call([db, db.addPost], { title, text })
|
||||
yield goToBoard({ board: { address, redirect: true } });
|
||||
// TODO: goto post
|
||||
}
|
||||
|
||||
export function* editPost({ address, postId, post }) {
|
||||
const db = window.dbs[address]
|
||||
const { title, text } = post
|
||||
yield call([db, db.updatePost], postId, { title, text })
|
||||
yield goToBoard({ board: { address, redirect: true } });
|
||||
// TODO: goto post
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
import {createStore, compose, applyMiddleware} from 'redux';
|
||||
import reduxImmutableStateInvariant from 'redux-immutable-state-invariant';
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
import saga from '../sagas';
|
||||
import createHistory from 'history/createHashHistory';
|
||||
import { routerMiddleware } from 'connected-react-router';
|
||||
import createRootReducer from '../reducers';
|
||||
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
|
||||
export const history = createHistory();
|
||||
|
||||
function configureStoreProd(initialState) {
|
||||
const middlewares = [
|
||||
routerMiddleware(history),
|
||||
sagaMiddleware,
|
||||
];
|
||||
|
||||
const store = createStore(createRootReducer(history), initialState, compose(
|
||||
applyMiddleware(...middlewares)
|
||||
)
|
||||
);
|
||||
|
||||
sagaMiddleware.run(saga);
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
function configureStoreDev(initialState) {
|
||||
const middlewares = [
|
||||
// Redux middleware that spits an error on you when you try to mutate your state either inside a dispatch or between dispatches.
|
||||
reduxImmutableStateInvariant(),
|
||||
routerMiddleware(history),
|
||||
sagaMiddleware,
|
||||
];
|
||||
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; // add support for Redux dev tools
|
||||
const store = createStore(createRootReducer(history), initialState, composeEnhancers(
|
||||
applyMiddleware(...middlewares)
|
||||
)
|
||||
);
|
||||
|
||||
let sagaTask = sagaMiddleware.run(saga);
|
||||
|
||||
if (module.hot) {
|
||||
// Enable Webpack hot module replacement for reducers
|
||||
module.hot.accept('../reducers', () => {
|
||||
const nextReducer = require('../reducers').default; // eslint-disable-line global-require
|
||||
store.replaceReducer(nextReducer);
|
||||
});
|
||||
// Enable Webpack hot module replacement for sagas
|
||||
module.hot.accept('../sagas', () => {
|
||||
const newSaga = require('../sagas').default;
|
||||
sagaTask.cancel();
|
||||
sagaTask.done.then(() => {
|
||||
sagaTask = sagaMiddleware.run(newSaga);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
const configureStore = process.env.NODE_ENV === 'production' ? configureStoreProd : configureStoreDev;
|
||||
|
||||
export default configureStore;
|
@ -1,55 +0,0 @@
|
||||
export async function ipfsPut(content) {
|
||||
const obj = {
|
||||
content: Buffer.from(content),
|
||||
path: '/'
|
||||
}
|
||||
const response = await window.ipfs.files.add(obj)
|
||||
return response[0].hash
|
||||
}
|
||||
|
||||
export async function readText(multihash) {
|
||||
const buffer = await window.ipfs.object.get(multihash)
|
||||
return buffer.toString('utf-8')
|
||||
}
|
||||
|
||||
export async function getStats() {
|
||||
const ipfs = window.ipfs;
|
||||
const orbitDb = window.orbitDb
|
||||
const dbs = {}
|
||||
const stats = {}
|
||||
if (ipfs && ipfs.isOnline()) {
|
||||
stats.ipfsLoaded = true
|
||||
const peers = await ipfs.swarm.peers()
|
||||
const id = await ipfs.id()
|
||||
stats.peers = peers.map(p => p.peer.id._idB58String)
|
||||
stats.id = id.id
|
||||
} else {
|
||||
stats.ipfsLoaded = false
|
||||
}
|
||||
if (stats.ipfsLoaded && orbitDb) {
|
||||
stats.orbitDbLoaded = true
|
||||
stats.pubKey = await orbitDb.key.getPublic('hex')
|
||||
Object.values(window.dbs || {}).forEach(db => {
|
||||
let writeable = db.access.write.indexOf('*') >= 0 || db.access.write.indexOf(stats.pubKey) >= 0
|
||||
const dbInfo = {
|
||||
opLogLength: db._oplog.length,
|
||||
access: {
|
||||
admin: db.access.admin,
|
||||
read: db.access.read,
|
||||
write: db.access.write,
|
||||
writeable
|
||||
},
|
||||
peers: []
|
||||
}
|
||||
const subscription = orbitDb._pubsub._subscriptions[db.address]
|
||||
if (subscription && subscription.room) {
|
||||
dbInfo.peers = [...(subscription.room._peers || [])]
|
||||
}
|
||||
dbs[db.address] = dbInfo
|
||||
})
|
||||
} else {
|
||||
stats.orbitDbLoaded = false
|
||||
}
|
||||
stats.dbs = dbs
|
||||
return stats
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
|
||||
export function getBoardAddress(hash, name) {
|
||||
return '/orbitdb/' + hash + '/' + name
|
||||
}
|
||||
|
||||
export function shortenAddress(address) {
|
||||
return address.replace(/^\/orbitdb/, '/b')
|
||||
}
|
||||
|
||||
export function closeBoard(address) {
|
||||
const db = window.dbs[address]
|
||||
delete window.dbs[address]
|
||||
if (db && db.close) db.close()
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
|
||||
export function save(){
|
||||
const obj = {
|
||||
addresses: Object.keys(window.dbs || {})
|
||||
}
|
||||
localStorage.setItem('ipfs-boards-v0', JSON.stringify(obj))
|
||||
}
|
||||
|
||||
export function load(){
|
||||
const str = localStorage.getItem('ipfs-boards-v0')
|
||||
try {
|
||||
return JSON.parse(str) || {};
|
||||
} catch (error) {
|
||||
return {}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user