1
0
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:
Enrico Fasoli 2019-10-22 22:08:59 +02:00
parent e90e840a60
commit bdcc6c1274
No known key found for this signature in database
GPG Key ID: 1238873C5F27DB4D
44 changed files with 2200 additions and 20573 deletions

13
.gitignore vendored
View File

@ -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*

View File

@ -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
View 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

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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 &rarr;</h3>
<p>Learn more about Next.js in the documentation.</p>
</a>
<a href='https://nextjs.org/learn' className='card'>
<h3>Next.js Learn &rarr;</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 &rarr;</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

View File

@ -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>

View File

@ -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"
}

View File

@ -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'

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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>
}

View File

@ -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 })
}
}

View File

@ -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

View File

@ -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>
}

View File

@ -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>
}

View File

@ -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>
}
}

View File

@ -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>
}

View File

@ -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>
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)
}

View File

@ -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} />
}
}
}

View File

@ -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)

View File

@ -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',
};

View File

@ -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
})
})
}

View File

@ -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;
}
}

View File

@ -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
})

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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();
});
}
}

View File

@ -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()
})
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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;

View File

@ -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
}

View File

@ -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()
}

View File

@ -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 {}
}
}