1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 17:56:25 +00:00

refactor file structure

This commit is contained in:
Rokt33r
2015-12-25 05:41:10 +09:00
parent 152e4129b2
commit 325ae00eeb
53 changed files with 10 additions and 386 deletions

View File

@@ -0,0 +1,95 @@
import React from 'react'
import ReactDOM from 'react-dom'
import modes from 'boost/vars/modes'
import _ from 'lodash'
var ace = window.ace
module.exports = React.createClass({
propTypes: {
code: React.PropTypes.string,
mode: React.PropTypes.string,
className: React.PropTypes.string,
onChange: React.PropTypes.func,
readOnly: React.PropTypes.bool
},
getDefaultProps: function () {
return {
readOnly: false
}
},
componentWillReceiveProps: function (nextProps) {
if (nextProps.readOnly !== this.props.readOnly) {
this.editor.setReadOnly(!!nextProps.readOnly)
}
},
componentDidMount: function () {
var el = ReactDOM.findDOMNode(this.refs.target)
var editor = this.editor = ace.edit(el)
editor.$blockScrolling = Infinity
editor.setValue(this.props.code)
editor.renderer.setShowGutter(true)
editor.setTheme('ace/theme/xcode')
editor.clearSelection()
editor.moveCursorTo(0, 0)
editor.commands.addCommand({
name: 'Emacs cursor up',
bindKey: {mac: 'Ctrl-P'},
exec: function (editor) {
editor.navigateUp(1)
if (editor.getCursorPosition().row < editor.getFirstVisibleRow()) editor.scrollToLine(editor.getCursorPosition().row, false, false)
},
readOnly: true
})
editor.setReadOnly(!!this.props.readOnly)
var session = editor.getSession()
let mode = _.findWhere(modes, {name: this.props.mode})
let syntaxMode = mode != null
? mode.mode
: 'text'
session.setMode('ace/mode/' + syntaxMode)
session.setUseSoftTabs(true)
session.setOption('useWorker', false)
session.setUseWrapMode(true)
session.on('change', function (e) {
if (this.props.onChange != null) {
var value = editor.getValue()
this.props.onChange(e, value)
}
}.bind(this))
},
componentDidUpdate: function (prevProps) {
if (this.editor.getValue() !== this.props.code) {
this.editor.setValue(this.props.code)
this.editor.clearSelection()
}
if (prevProps.mode !== this.props.mode) {
var session = this.editor.getSession()
let mode = _.findWhere(modes, {name: this.props.mode})
let syntaxMode = mode != null
? mode.mode
: 'text'
session.setMode('ace/mode/' + syntaxMode)
}
},
getFirstVisibleRow: function () {
return this.editor.getFirstVisibleRow()
},
getCursorPosition: function () {
return this.editor.getCursorPosition()
},
moveCursorTo: function (row, col) {
this.editor.moveCursorTo(row, col)
},
scrollToLine: function (num) {
this.editor.scrollToLine(num, false, false)
},
render: function () {
return (
<div ref='target' className={this.props.className == null ? 'CodeEditor' : 'CodeEditor ' + this.props.className}></div>
)
}
})

View File

@@ -0,0 +1,20 @@
import React, { PropTypes } from 'react'
const electron = require('electron')
const shell = electron.shell
export default class ExternalLink extends React.Component {
handleClick (e) {
shell.openExternal(this.props.href)
e.preventDefault()
}
render () {
return (
<a onClick={e => this.handleClick(e)} {...this.props}/>
)
}
}
ExternalLink.propTypes = {
href: PropTypes.string
}

View File

@@ -0,0 +1,52 @@
import React, { PropTypes } from 'react'
const BLUE = '#3460C7'
const LIGHTBLUE = '#2BA5F7'
const ORANGE = '#FF8E00'
const YELLOW = '#E8D252'
const GREEN = '#3FD941'
const DARKGREEN = '#1FAD85'
const RED = '#E10051'
const PURPLE = '#B013A4'
function getColorByIndex (index) {
switch (index % 8) {
case 0:
return RED
case 1:
return ORANGE
case 2:
return YELLOW
case 3:
return GREEN
case 4:
return DARKGREEN
case 5:
return LIGHTBLUE
case 6:
return BLUE
case 7:
return PURPLE
default:
return DARKGREEN
}
}
export default class FolderMark extends React.Component {
render () {
let color = getColorByIndex(this.props.color)
let className = 'FolderMark fa fa-square fa-fw'
if (this.props.className != null) {
className += ' active'
}
return (
<i className={className} style={{color: color}}/>
)
}
}
FolderMark.propTypes = {
color: PropTypes.number,
className: PropTypes.string
}

View File

@@ -0,0 +1,57 @@
var React = require('react')
var { PropTypes } = React
import markdown from 'boost/markdown'
var ReactDOM = require('react-dom')
const electron = require('electron')
const shell = electron.shell
function handleAnchorClick (e) {
shell.openExternal(e.target.href)
e.preventDefault()
}
export default class MarkdownPreview extends React.Component {
componentDidMount () {
this.addListener()
}
componentDidUpdate () {
this.addListener()
}
componentWillUnmount () {
this.removeListener()
}
componentWillUpdate () {
this.removeListener()
}
addListener () {
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a')
for (var i = 0; i < anchors.length; i++) {
anchors[i].addEventListener('click', handleAnchorClick)
}
}
removeListener () {
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a')
for (var i = 0; i < anchors.length; i++) {
anchors[i].removeEventListener('click', handleAnchorClick)
}
}
render () {
return (
<div className={'MarkdownPreview' + (this.props.className != null ? ' ' + this.props.className : '')} dangerouslySetInnerHTML={{__html: ' ' + markdown(this.props.content)}}/>
)
}
}
MarkdownPreview.propTypes = {
className: PropTypes.string,
content: PropTypes.string
}

View File

@@ -0,0 +1,82 @@
import React, { PropTypes } from 'react'
export default class ModeIcon extends React.Component {
getClassName () {
var mode = this.props.mode
switch (mode) {
// Script
case 'javascript':
return 'devicon-javascript-plain'
case 'jsx':
return 'devicon-react-original'
case 'coffee':
return 'devicon-coffeescript-original'
case 'ruby':
return 'devicon-ruby-plain'
case 'erlang':
return 'devicon-erlang-plain'
case 'php':
return 'devicon-php-plain'
// HTML
case 'html':
return 'devicon-html5-plain'
// Stylesheet
case 'css':
return 'devicon-css3-plain'
case 'less':
return 'devicon-less-plain-wordmark'
case 'sass':
case 'scss':
return 'devicon-sass-original'
// Compile
case 'c':
return 'devicon-c-plain'
case 'cpp':
return 'devicon-cplusplus-plain'
case 'csharp':
return 'devicon-csharp-plain'
case 'objc':
return 'devicon-apple-original'
case 'golang':
return 'devicon-go-plain'
case 'java':
return 'devicon-java-plain'
// Framework
case 'django':
return 'devicon-django-plain'
// Config
case 'dockerfile':
return 'devicon-docker-plain'
case 'gitignore':
return 'devicon-git-plain'
// Shell
case 'sh':
case 'batchfile':
case 'powershell':
return 'fa fa-fw fa-terminal'
case 'text':
case 'markdown':
return 'fa fa-fw fa-file-text-o'
}
return 'fa fa-fw fa-code'
}
render () {
let className = `ModeIcon ${this.getClassName()} ${this.props.className}`
return (
<i className={className}/>
)
}
}
ModeIcon.propTypes = {
className: PropTypes.string,
mode: PropTypes.string
}

View File

@@ -0,0 +1,190 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ModeIcon from 'boost/components/ModeIcon'
import modes from 'boost/vars/modes'
import _ from 'lodash'
const IDLE_MODE = 'IDLE_MODE'
const EDIT_MODE = 'EDIT_MODE'
export default class ModeSelect extends React.Component {
constructor (props) {
super(props)
this.state = {
mode: IDLE_MODE,
search: '',
focusIndex: 0
}
}
componentDidMount () {
this.blurHandler = e => {
let searchElement = ReactDOM.findDOMNode(this.refs.search)
if (this.state.mode === EDIT_MODE && document.activeElement !== searchElement) {
this.handleBlur()
}
}
window.addEventListener('click', this.blurHandler)
}
componentWillUnmount () {
window.removeEventListener('click', this.blurHandler)
let searchElement = ReactDOM.findDOMNode(this.refs.search)
if (searchElement != null && this.searchKeyDownListener != null) {
searchElement.removeEventListener('keydown', this.searchKeyDownListener)
}
}
handleIdleSelectClick (e) {
this.setState({mode: EDIT_MODE})
}
componentDidUpdate (prevProps, prevState) {
if (prevState.mode !== this.state.mode && this.state.mode === EDIT_MODE) {
let searchElement = ReactDOM.findDOMNode(this.refs.search)
searchElement.focus()
if (this.searchKeyDownListener == null) {
this.searchKeyDownListener = e => this.handleSearchKeyDown
}
searchElement.addEventListener('keydown', this.searchKeyDownListener)
}
}
componentWillUpdate (nextProps, nextState) {
if (nextProps.mode !== this.state.mode && nextState.mode === IDLE_MODE) {
let searchElement = ReactDOM.findDOMNode(this.refs.search)
if (searchElement != null && this.searchKeyDownListener != null) {
searchElement.removeEventListener('keydown', this.searchKeyDownListener)
}
}
}
handleModeOptionClick (modeName) {
return e => {
this.props.onChange(modeName)
this.setState({
mode: IDLE_MODE,
search: '',
focusIndex: 0
})
}
}
handleSearchKeyDown (e) {
switch (e.keyCode) {
// up
case 38:
e.preventDefault()
if (this.state.focusIndex > 0) this.setState({focusIndex: this.state.focusIndex - 1})
break
// down
case 40:
e.preventDefault()
{
let filteredModes = modes
.filter(mode => {
let search = this.state.search
let nameMatched = mode.name.match(search)
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
return nameMatched || aliasMatched
})
if (filteredModes.length === this.state.focusIndex + 1) this.setState({focusIndex: filteredModes.length - 1})
else this.setState({focusIndex: this.state.focusIndex + 1})
}
break
// enter
case 13:
e.preventDefault()
{
let filteredModes = modes
.filter(mode => {
let search = this.state.search
let nameMatched = mode.name.match(search)
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
return nameMatched || aliasMatched
})
let targetMode = filteredModes[this.state.focusIndex]
if (targetMode != null) {
this.props.onChange(targetMode.name)
this.handleBlur()
}
}
break
// esc
case 27:
e.preventDefault()
e.stopPropagation()
this.handleBlur()
break
case 9:
this.handleBlur()
}
}
handleSearchChange (e) {
this.setState({
search: e.target.value,
focusIndex: 0
})
}
handleBlur () {
if (this.state.mode === EDIT_MODE) {
this.setState({
mode: IDLE_MODE,
search: '',
focusIndex: 0
})
}
if (this.props.onBlur != null) this.props.onBlur()
}
render () {
let className = this.props.className != null
? `ModeSelect ${this.props.className}`
: this.props.className
if (this.state.mode === IDLE_MODE) {
let mode = _.findWhere(modes, {name: this.props.value})
let modeName = mode != null ? mode.name : 'text'
let modeLabel = mode != null ? mode.label : 'Plain text'
return (
<div className={className + ' idle'} onClick={e => this.handleIdleSelectClick(e)}>
<ModeIcon mode={modeName}/>
<span className='modeLabel'>{modeLabel}</span>
</div>
)
}
let filteredOptions = modes
.filter(mode => {
let search = this.state.search
let nameMatched = mode.name.match(_.escapeRegExp(search))
let aliasMatched = _.some(mode.alias, alias => alias.match(_.escapeRegExp(search)))
return nameMatched || aliasMatched
})
.map((mode, index) => {
return (
<div key={mode.name} className={index === this.state.focusIndex ? 'option active' : 'option'} onClick={e => this.handleModeOptionClick(mode.name)(e)}><ModeIcon mode={mode.name}/>{mode.label}</div>
)
})
return (
<div className={className + ' edit'}>
<input onKeyDown={e => this.handleSearchKeyDown(e)} ref='search' onChange={e => this.handleSearchChange(e)} value={this.state.search} type='text'/>
<div ref='options' className='modeOptions hide'>
{filteredOptions}
</div>
</div>
)
}
}
ModeSelect.propTypes = {
className: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
onBlur: PropTypes.func
}

View File

@@ -0,0 +1,24 @@
import React, { PropTypes } from 'react'
import md5 from 'md5'
export default class ProfileImage extends React.Component {
render () {
let className = this.props.className == null ? 'ProfileImage' : 'ProfileImage ' + this.props.className
let email = this.props.email != null ? this.props.email : ''
let src = 'http://www.gravatar.com/avatar/' + md5(email.trim().toLowerCase()) + '?s=' + this.props.size
return (
<img
className={className}
width={this.props.size}
height={this.props.size}
src={src}/>
)
}
}
ProfileImage.propTypes = {
email: PropTypes.string,
size: PropTypes.string,
className: PropTypes.string
}

View File

@@ -0,0 +1,18 @@
import React, { PropTypes } from 'react'
import store from '../store'
import { setTagFilter } from '../actions'
export default class TagLink extends React.Component {
handleClick (e) {
store.dispatch(setTagFilter(this.props.tag))
}
render () {
return (
<a onClick={e => this.handleClick(e)}>{this.props.tag}</a>
)
}
}
TagLink.propTypes = {
tag: PropTypes.string
}

View File

@@ -0,0 +1,168 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import _ from 'lodash'
import linkState from 'boost/linkState'
function isNotEmptyString (str) {
return _.isString(str) && str.length > 0
}
export default class TagSelect extends React.Component {
constructor (props) {
super(props)
this.state = {
input: '',
isInputFocused: false
}
}
componentDidMount () {
this.blurInputBlurHandler = e => {
if (ReactDOM.findDOMNode(this.refs.tagInput) !== document.activeElement) {
this.setState({isInputFocused: false})
}
}
window.addEventListener('click', this.blurInputBlurHandler)
}
componentWillUnmount (e) {
window.removeEventListener('click', this.blurInputBlurHandler)
}
// Suggestは必ずInputの下に位置するようにする
componentDidUpdate () {
if (this.shouldShowSuggest()) {
let inputRect = ReactDOM.findDOMNode(this.refs.tagInput).getBoundingClientRect()
let suggestElement = ReactDOM.findDOMNode(this.refs.suggestTags)
if (suggestElement != null) {
suggestElement.style.top = inputRect.top + 20 + 'px'
suggestElement.style.left = inputRect.left + 'px'
}
}
}
shouldShowSuggest () {
return this.state.isInputFocused && isNotEmptyString(this.state.input)
}
addTag (tag, clearInput = true) {
let tags = this.props.tags.slice(0)
let newTag = tag.trim()
if (newTag.length === 0 && clearInput) {
this.setState({input: ''})
return
}
tags.push(newTag)
tags = _.uniq(tags)
if (_.isFunction(this.props.onChange)) {
this.props.onChange(newTag, tags)
}
if (clearInput) this.setState({input: ''})
}
handleKeyDown (e) {
switch (e.keyCode) {
case 8:
{
if (this.state.input.length > 0) break
e.preventDefault()
let tags = this.props.tags.slice(0)
tags.pop()
this.props.onChange(null, tags)
}
break
case 13:
{
e.preventDefault()
this.addTag(this.state.input)
}
}
}
handleThisClick (e) {
ReactDOM.findDOMNode(this.refs.tagInput).focus()
}
handleInputFocus (e) {
this.setState({isInputFocused: true})
}
handleItemRemoveButton (tag) {
return e => {
e.stopPropagation()
let tags = this.props.tags.slice(0)
tags.splice(tags.indexOf(tag), 1)
if (_.isFunction(this.props.onChange)) {
this.props.onChange(null, tags)
}
}
}
handleSuggestClick (tag) {
return e => {
this.addTag(tag)
}
}
render () {
let { tags, suggestTags } = this.props
let tagElements = _.isArray(tags)
? this.props.tags.map(tag => (
<span key={tag} className='tagItem'>
<button onClick={e => this.handleItemRemoveButton(tag)(e)} className='tagRemoveBtn'><i className='fa fa-fw fa-times'/></button>
<span className='tagLabel'>{tag}</span>
</span>))
: null
let suggestElements = this.shouldShowSuggest() ? suggestTags
.filter(tag => {
return tag.match(this.state.input)
})
.map(tag => {
return <button onClick={e => this.handleSuggestClick(tag)(e)} key={tag}>{tag}</button>
})
: null
return (
<div className='TagSelect' onClick={e => this.handleThisClick(e)}>
<div className='tags'>
{tagElements}
<input
type='text'
onKeyDown={e => this.handleKeyDown(e)}
ref='tagInput'
valueLink={this.linkState('input')}
placeholder='Click here to add tags'
className='tagInput'
onFocus={e => this.handleInputFocus(e)}
/>
</div>
{suggestElements != null && suggestElements.length > 0
? (
<div ref='suggestTags' className='suggestTags'>
{suggestElements}
</div>
)
: null
}
</div>
)
}
}
TagSelect.propTypes = {
tags: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func,
suggestTags: PropTypes.array
}
TagSelect.prototype.linkState = linkState

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,43 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Boostnote Finder</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
<link rel="stylesheet" href="../../node_modules/font-awesome/css/font-awesome.min.css" media="screen" charset="utf-8">
<link rel="stylesheet" href="../../node_modules/devicon/devicon.min.css">
<link rel="stylesheet" href="../../node_modules/highlight.js/styles/xcode.css">
<link rel="shortcut icon" href="favicon.ico">
<style>
@font-face {
font-family: 'Lato';
src: url('../../resources/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
url('../../resources/Lato-Regular.woff') format('woff'), /* Modern Browsers */
url('../../resources/Lato-Regular.ttf') format('truetype');
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
}
</style>
</head>
<body>
<div id="content"></div>
<script src="../../submodules/ace/src-min/ace.js"></script>
<script>
const electron = require('electron')
electron.webFrame.setZoomLevelLimits(1, 1)
const _ = require('lodash')
var scriptUrl = _.find(electron.remote.process.argv, a => a === '--hot')
? 'http://localhost:8080/assets/finder.js'
: '../../compiled/finder.js'
var scriptEl=document.createElement('script')
scriptEl.setAttribute("type","text/javascript")
scriptEl.setAttribute("src", scriptUrl)
document.getElementsByTagName("head")[0].appendChild(scriptEl)
</script>
</body>
</html>

View File

@@ -0,0 +1,137 @@
import _ from 'lodash'
import moment from 'moment'
import dataStore from 'boost/dataStore'
import { request, SERVER_URL } from 'boost/api'
import clientKey from 'boost/clientKey'
const electron = require('electron')
const version = electron.remote.app.getVersion()
function isSameDate (a, b) {
a = moment(a).utcOffset(+540).format('YYYYMMDD')
b = moment(b).utcOffset(+540).format('YYYYMMDD')
return a === b
}
export function init () {
let records = getAllRecords()
if (records == null) {
saveAllRecords([])
}
emit(null)
postRecords()
if (window != null) {
window.addEventListener('online', postRecords)
window.setInterval(postRecords, 1000 * 60 * 60 * 24)
}
}
export function getAllRecords () {
return JSON.parse(localStorage.getItem('activityRecords'))
}
export function saveAllRecords (records) {
localStorage.setItem('activityRecords', JSON.stringify(records))
}
/*
Post all records(except today)
and remove all posted records
*/
export function postRecords (data) {
if (process.env.BOOST_ENV === 'development') {
console.log('post failed - on development')
return
}
let records = getAllRecords()
records = records.filter(record => {
return !isSameDate(new Date(), record.date)
})
if (records.length === 0) {
console.log('No records to post')
return
}
console.log('posting...', records)
let input = {
clientKey: clientKey.get(),
records
}
return request.post(SERVER_URL + 'apis/activity')
.send(input)
.then(res => {
let records = getAllRecords()
let todayRecord = _.find(records, record => {
return isSameDate(new Date(), record.date)
})
if (todayRecord != null) saveAllRecords([todayRecord])
else saveAllRecords([])
})
.catch(err => {
console.error(err)
})
}
export function emit (type, data = {}) {
let records = getAllRecords()
let index = _.findIndex(records, record => {
return isSameDate(new Date(), record.date)
})
let todayRecord
if (index < 0) {
todayRecord = {date: new Date()}
records.push(todayRecord)
}
else todayRecord = records[index]
switch (type) {
case 'ARTICLE_CREATE':
case 'ARTICLE_UPDATE':
case 'ARTICLE_DESTROY':
case 'FOLDER_CREATE':
case 'FOLDER_UPDATE':
case 'FOLDER_DESTROY':
case 'FINDER_OPEN':
case 'FINDER_COPY':
case 'MAIN_DETAIL_COPY':
case 'ARTICLE_SHARE':
todayRecord[type] = todayRecord[type] == null
? 1
: todayRecord[type] + 1
break
}
// Count ARTICLE_CREATE and ARTICLE_UPDATE again by syntax
if ((type === 'ARTICLE_CREATE' || type === 'ARTICLE_UPDATE') && data.mode != null) {
let recordKey = type + '_BY_SYNTAX'
if (todayRecord[recordKey] == null) todayRecord[recordKey] = {}
todayRecord[recordKey][data.mode] = todayRecord[recordKey][data.mode] == null
? 1
: todayRecord[recordKey][data.mode] + 1
}
let storeData = dataStore.getData()
todayRecord.FOLDER_COUNT = _.isArray(storeData.folders) ? storeData.folders.length : 0
todayRecord.ARTICLE_COUNT = _.isArray(storeData.articles) ? storeData.articles.length : 0
todayRecord.CLIENT_VERSION = version
todayRecord.SYNTAX_COUNT = storeData.articles.reduce((sum, article) => {
if (sum[article.mode] == null) sum[article.mode] = 1
else sum[article.mode]++
return sum
}, {})
saveAllRecords(records)
}
export default {
init,
emit,
postRecords
}

21
browser/lib/api.js Normal file
View File

@@ -0,0 +1,21 @@
import superagent from 'superagent'
import superagentPromise from 'superagent-promise'
export const SERVER_URL = 'https://b00st.io/'
// export const SERVER_URL = 'http://localhost:3333/'
export const request = superagentPromise(superagent, Promise)
export function shareViaPublicURL (input) {
return request
.post(SERVER_URL + 'apis/share')
// .set({
// Authorization: 'Bearer ' + auth.token()
// })
.send(input)
}
export default {
SERVER_URL,
shareViaPublicURL
}

23
browser/lib/clientKey.js Normal file
View File

@@ -0,0 +1,23 @@
import _ from 'lodash'
import keygen from 'boost/keygen'
function getClientKey () {
let clientKey = localStorage.getItem('clientKey')
if (!_.isString(clientKey) || clientKey.length !== 40) {
clientKey = keygen()
setClientKey(clientKey)
}
return clientKey
}
function setClientKey (newKey) {
localStorage.setItem('clientKey', newKey)
}
getClientKey()
export default {
get: getClientKey,
set: setClientKey
}

141
browser/lib/dataStore.js Normal file
View File

@@ -0,0 +1,141 @@
import keygen from 'boost/keygen'
import _ from 'lodash'
const electron = require('electron')
const remote = electron.remote
const jetpack = require('fs-jetpack')
const path = require('path')
let defaultContent = 'Boost is a brand new note App for programmers.\n\n> 下に日本語版があります。\n\n# \u25CEfeature\n\nBoost has some preponderant functions for efficient engineer\'s task.See some part of it.\n\n1. classify information by\u300CFolders\u300D\n2. deal with great variety of syntax\n3. Finder function\n\n\uFF0A\u3000\uFF0A\u3000\uFF0A\u3000\uFF0A\n\n# 1. classify information by \u300CFolders\u300D- access the information you needed easily.\n\n\u300CFolders\u300D which on the left side bar. Press plus button now. flexible way of classification.\n- Create Folder every language or flamework\n- Make Folder for your own casual memos\n\n# 2. Deal with a great variety of syntax \u2013 instead of your brain\nSave handy all information related with programming\n- Use markdown and gather api specification\n- Well using module and snippet\n\nSave them on Boost, you don\'t need to rewrite or re-search same code again.\n\n# 3. Load Finder function \u2013 now you don\'t need to spell command by hand typing.\n\n**Shift +ctrl+tab** press buttons at same time.\nThen, the window will show up for search Boost contents that instant.\n\nUsing cursor key to chose, press enter, cmd+v to paste and\u2026 please check it out by your own eye.\n\n- Such command spl or linux which programmers often use but troublesome to hand type\n\n- (Phrases commonly used for e-mail or customer support)\n\nWe support preponderant efficiency\n\n\uFF0A\u3000\uFF0A\u3000\uFF0A\u3000\uFF0A\n\n## \u25CEfor more information\nFrequently updated with this blog ( http:\/\/blog-jp.b00st.io )\n\nHave wonderful programmer life!\n\n## Hack your memory**\n\n\n\n# 日本語版\n\n**Boost**は全く新しいエンジニアライクのノートアプリです。\n\n# ◎特徴\nBoostはエンジニアの仕事を圧倒的に効率化するいくつかの機能を備えています。\nその一部をご紹介します。\n1. Folderで情報を分類\n2. 豊富なsyantaxに対応\n3. Finder機能\n\n\n   \n\n# 1. Folderで情報を分類、欲しい情報にすぐアクセス。\n左側のバーに存在する「Folders」。\n今すぐプラスボタンを押しましょう。\n分類の仕方も自由自在です。\n- 言語やフレームワークごとにFolderを作成\n- 自分用のカジュアルなメモをまとめる場としてFolderを作成\n\n\n# 2. 豊富なsyntaxに対応、自分の脳の代わりに。\nプログラミングに関する情報を全て、手軽に保存しましょう。\n- mdで、apiの仕様をまとめる\n- よく使うモジュールやスニペット\n\nBoostに保存しておくことで、何度も同じコードを書いたり調べたりする必要がなくなります。\n\n# 3. Finder機能を搭載、もうコマンドを手打ちする必要はありません。\n**「shift+ctrl+tab」** を同時に押してみてください。\nここでは、一瞬でBoostの中身を検索するウィンドウを表示させることができます。\n\n矢印キーで選択、Enterを押し、cmd+vでペーストすると…続きはご自身の目でお確かめください。\n- sqlやlinux等の、よく使うが手打ちが面倒なコマンド\n- (メールやカスタマーサポート等でよく使うフレーズ)\n\n私たちは、圧倒的な効率性を支援します。\n\   \n\n\n## ◎詳しくは\nこちらのブログ( http://blog-jp.b00st.io )にて随時更新しています。\n\nそれでは素晴らしいエンジニアライフを\n\n## Hack your memory'
function getLocalPath () {
return path.join(remote.app.getPath('userData'), 'local.json')
}
function forgeInitialRepositories () {
let defaultRepo = {
key: keygen(),
name: 'local',
type: 'userData',
user: {
name: 'New user'
}
}
if (process.platform === 'darwin') {
defaultRepo.user.name = remote.process.env.USER
} else if (process.platform === 'win32') {
defaultRepo.user.name = remote.process.env.USERNAME
}
return [defaultRepo]
}
function getRepositories () {
let raw = localStorage.getItem('repositories')
try {
let parsed = JSON.parse(raw)
if (!_.isArray(parsed)) {
throw new Error('repositories data is currupte. re-init data.')
}
return parsed
} catch (e) {
console.log(e)
let newRepos = forgeInitialRepositories()
saveRepositories(newRepos)
return newRepos
}
}
function saveRepositories (repos) {
localStorage.setItem('repositories', JSON.stringify(repos))
}
export function getUser (repoName) {
if (repoName == null) {
return getRepositories()[0]
}
return null
}
export function saveUser (repoName, user) {
let repos = getRepositories()
if (repoName == null) {
Object.assign(repos[0].user, user)
}
saveRepositories(repos)
}
export function init () {
// set repositories info
getRepositories()
// set local.json
let data = jetpack.read(getLocalPath(), 'json')
if (data == null) {
// for 0.4.1 -> 0.4.2
if (localStorage.getItem('local') != null) {
data = JSON.parse(localStorage.getItem('local'))
jetpack.write(getLocalPath(), data)
localStorage.removeItem('local')
console.log('update 0.4.1 => 0.4.2')
return
}
let defaultFolder = {
name: 'default',
key: keygen()
}
let defaultArticle = {
title: 'About Boost',
tags: ['boost', 'intro'],
content: defaultContent,
mode: 'markdown',
key: keygen(),
FolderKey: defaultFolder.key
}
data = {
articles: [defaultArticle],
folders: [defaultFolder],
version: '0.4'
}
jetpack.write(getLocalPath(), data)
}
}
export function getData () {
return jetpack.read(getLocalPath(), 'json')
}
export function setArticles (articles) {
let data = getData()
data.articles = articles
jetpack.write(getLocalPath(), data)
}
export function setFolders (folders) {
let data = getData()
data.folders = folders
jetpack.write(getLocalPath(), data)
}
function isFinderCalled () {
var argv = process.argv.slice(1)
return argv.some(arg => arg.match(/--finder/))
}
export default (function () {
if (!isFinderCalled()) {
init()
}
return {
getUser,
saveUser,
init,
getData,
setArticles,
setFolders
}
})()

7
browser/lib/keygen.js Normal file
View File

@@ -0,0 +1,7 @@
var crypto = require('crypto')
module.exports = function () {
var shasum = crypto.createHash('sha1')
shasum.update(((new Date()).getTime() + Math.round(Math.random()*1000)).toString())
return shasum.digest('hex')
}

36
browser/lib/linkState.js Normal file
View File

@@ -0,0 +1,36 @@
function getIn (object, path) {
let stack = path.split('.')
while (stack.length > 1) {
object = object[stack.shift()]
}
return object[stack.shift()]
}
function updateIn (object, path, value) {
let current = object
let stack = path.split('.')
while (stack.length > 1) {
current = current[stack.shift()]
}
current[stack.shift()] = value
return object
}
function setPartialState (component, path, value) {
component.setState(
updateIn(component.state, path, value))
}
export default function linkState (path) {
return {
value: getIn(this.state, path),
requestChange: setPartialState.bind(null, this, path)
}
}
export function linkState2 (el, path) {
return {
value: getIn(el.state, path),
requestChange: setPartialState.bind(null, el, path)
}
}

38
browser/lib/markdown.js Normal file
View File

@@ -0,0 +1,38 @@
import markdownit from 'markdown-it'
import hljs from 'highlight.js'
import emoji from 'markdown-it-emoji'
var md = markdownit({
typographer: true,
linkify: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(lang, str).value
} catch (__) {}
}
try {
return hljs.highlightAuto(str).value
} catch (__) {}
return ''
}
})
md.use(emoji)
let originalRenderToken = md.renderer.renderToken
md.renderer.renderToken = function renderToken (tokens, idx, options) {
let token = tokens[idx]
let result = originalRenderToken.call(md.renderer, tokens, idx, options)
if (token.map != null) {
return result + '<a class=\'lineAnchor\' data-key=\'' + token.map[0] + '\'></a>'
}
return result
}
export default function markdown (content) {
if (content == null) content = ''
return md.render(content.toString())
}

46
browser/lib/modal.js Normal file
View File

@@ -0,0 +1,46 @@
import React from 'react'
import ReactDOM from 'react-dom'
class ModalBase extends React.Component {
constructor (props) {
super(props)
this.state = {
component: null,
componentProps: {},
isHidden: true
}
}
close () {
if (modalBase != null) modalBase.setState({component: null, componentProps: null, isHidden: true})
}
render () {
return (
<div className={'ModalBase' + (this.state.isHidden ? ' hide' : '')}>
<div onClick={e => this.close(e)} className='modalBack'/>
{this.state.component == null ? null : (
<this.state.component {...this.state.componentProps} close={this.close}/>
)}
</div>
)
}
}
let el = document.createElement('div')
document.body.appendChild(el)
let modalBase = ReactDOM.render(<ModalBase/>, el)
export function openModal (component, props) {
if (modalBase == null) { return }
modalBase.setState({component: component, componentProps: props, isHidden: false})
}
export function closeModal () {
if (modalBase == null) { return }
modalBase.setState({component: null, componentProps: null, isHidden: true})
}
export function isModalOpen () {
return !modalBase.state.isHidden
}

756
browser/lib/modes.js Normal file
View File

@@ -0,0 +1,756 @@
const modes = [
// Major
{
name: 'text',
label: 'Plain text',
mode: 'text'
},
{
name: 'markdown',
label: 'Markdown',
alias: ['md'],
mode: 'markdown'
},
{
name: 'javascript',
label: 'JavaScript',
alias: ['js', 'jscript', 'babel', 'es'],
mode: 'javascript'
},
{
name: 'html',
label: 'HTML',
alias: [],
mode: 'html'
},
{
name: 'css',
label: 'CSS',
alias: ['cascade', 'stylesheet'],
mode: 'css'
},
{
name: 'php',
label: 'PHP',
alias: [],
mode: 'php'
},
{
name: 'python',
label: 'Python',
alias: ['py'],
mode: 'python'
},
{
name: 'ruby',
label: 'Ruby',
alias: ['rb'],
mode: 'ruby'
},
{
name: 'java',
label: 'Java',
alias: [],
mode: 'java'
},
{
name: 'c',
label: 'C',
alias: ['c', 'h', 'clang', 'clang'],
mode: 'c_cpp'
},
{
name: 'cpp',
label: 'C++',
alias: ['cc', 'cpp', 'cxx', 'hh', 'c++', 'cplusplus'],
mode: 'c_cpp'
},
{
name: 'csharp',
label: 'C#',
alias: ['cs', 'c#'],
mode: 'csharp'
},
{
name: 'swift',
label: 'Swift',
alias: [],
mode: 'swift'
},
{
name: 'golang',
label: 'Go',
alias: ['go'],
mode: 'golang'
},
// Minor
{
name: 'abap',
label: 'ABAP',
alias: [],
mode: 'abap'
},
{
name: 'abc',
label: 'ABC',
alias: [],
mode: 'abc'
},
{
name: 'actionscript',
label: 'ActionScript',
alias: ['as'],
mode: 'actionscript'
},
{
name: 'ada',
label: 'Ada',
alias: [],
mode: 'ada'
},
{
name: 'apache_conf',
label: 'Apache config',
alias: ['apache', 'conf'],
mode: 'apache_conf'
},
{
name: 'applescript',
label: 'AppleScript',
alias: ['scpt'],
mode: 'applescript'
},
{
name: 'asciidoc',
label: 'AsciiDoc',
alias: ['ascii', 'doc', 'txt'],
mode: 'asciidoc'
},
{
name: 'assembly_x86',
label: 'Assembly x86',
alias: ['assembly', 'x86', 'asm'],
mode: 'assembly_x86'
},
{
name: 'autohotkey',
label: 'AutoHotkey',
alias: ['ahk'],
mode: 'autohotkey'
},
{
name: 'batchfile',
label: 'Batch file',
alias: ['dos', 'windows', 'bat', 'cmd', 'btm'],
mode: 'batchfile'
},
{
name: 'cirru',
label: 'Cirru',
alias: [],
mode: 'cirru'
},
{
name: 'clojure',
label: 'Clojure',
alias: ['clj', 'cljs', 'cljc', 'edn'],
mode: 'clojure'
},
{
name: 'cobol',
label: 'COBOL',
alias: ['cbl', 'cob', 'cpy'],
mode: 'cobol'
},
{
name: 'coffee',
label: 'CoffeeScript',
alias: ['coffee'],
mode: 'coffee'
},
{
name: 'coldfusion',
label: 'ColdFusion',
alias: ['cfm', 'cfc'],
mode: 'coldfusion'
},
{
name: 'curly',
label: 'Curly',
alias: [],
mode: 'curly'
},
{
name: 'd',
label: 'D',
alias: ['dlang'],
mode: 'd'
},
{
name: 'dockerfile',
label: 'DockerFile',
alias: ['docker'],
mode: 'docker'
},
{
name: 'dart',
label: 'Dart',
alias: [],
mode: 'dart'
},
{
name: 'diff',
label: 'Diff',
alias: [],
mode: 'diff'
},
{
name: 'django',
label: 'Django',
alias: [],
mode: 'djt'
},
{
name: 'dot',
label: 'DOT',
alias: ['gv'],
mode: 'dot'
},
{
name: 'eiffel',
label: 'Eiffel',
alias: [],
mode: 'eiffel'
},
{
name: 'ejs',
label: 'EJS',
alias: [],
mode: 'ejs'
},
{
name: 'elixir',
label: 'Elixir',
alias: ['ex', 'exs'],
mode: 'elixir'
},
{
name: 'elm',
label: 'Elm',
alias: [],
mode: 'elm'
},
{
name: 'erlang',
label: 'Erlang',
alias: ['erl', 'hrl'],
mode: 'erlang'
},
{
name: 'forth',
label: 'Forth',
alias: ['fs', 'fth'],
mode: 'forth'
},
{
name: 'freemaker',
label: 'Freemaker',
alias: ['ftl'],
mode: 'ftl'
},
{
name: 'gcode',
label: 'G-code',
alias: ['mpt', 'mpf', 'nc'],
mode: 'gcode'
},
{
name: 'gherkin',
label: 'Gherkin',
alias: ['cucumber'],
mode: 'gherkin'
},
{
name: 'gitignore',
label: 'Gitignore',
alias: ['git'],
mode: 'gitignore'
},
{
name: 'glsl',
label: 'GLSL',
alias: ['opengl', 'shading'],
mode: 'glsl'
},
{
name: 'groovy',
label: 'Groovy',
alias: [],
mode: 'grooby'
},
{
name: 'haml',
label: 'Haml',
alias: [],
mode: 'haml'
},
{
name: 'handlebars',
label: 'Handlebars',
alias: ['hbs'],
mode: 'handlebars'
},
{
name: 'haskell',
label: 'Haskell',
alias: ['hs', 'lhs'],
mode: 'haskell'
},
{
name: 'haxe',
label: 'Haxe',
alias: ['hx', 'hxml'],
mode: 'haxe'
},
{
name: 'html_ruby',
label: 'HTML (Ruby)',
alias: ['erb', 'rhtml'],
mode: 'html_ruby'
},
{
name: 'jsx',
label: 'JSX',
alias: ['es', 'babel', 'js', 'jsx', 'react'],
mode: 'jsx'
},
{
name: 'typescript',
label: 'TypeScript',
alias: ['ts'],
mode: 'typescript'
},
{
name: 'ini',
label: 'INI file',
alias: [],
mode: 'ini'
},
{
name: 'io',
label: 'Io',
alias: [],
mode: 'io'
},
{
name: 'jack',
label: 'Jack',
alias: [],
mode: 'jack'
},
{
name: 'jade',
label: 'Jade',
alias: [],
mode: 'jade'
},
{
name: 'json',
label: 'JSON',
alias: [],
mode: 'json'
},
{
name: 'jsoniq',
label: 'JSONiq',
alias: ['query'],
mode: 'jsoniq'
},
{
name: 'jsp',
label: 'JSP',
alias: [],
mode: 'jsp'
},
{
name: 'julia',
label: 'Julia',
alias: [],
mode: 'julia'
},
{
name: 'latex',
label: 'Latex',
alias: ['tex'],
mode: 'latex'
},
{
name: 'lean',
label: 'Lean',
alias: [],
mode: 'lean'
},
{
name: 'less',
label: 'Less',
alias: [],
mode: 'less'
},
{
name: 'liquid',
label: 'Liquid',
alias: [],
mode: 'liquid'
},
{
name: 'lisp',
label: 'Lisp',
alias: ['lsp'],
mode: 'lisp'
},
{
name: 'livescript',
label: 'LiveScript',
alias: ['ls'],
mode: 'livescript'
},
{
name: 'logiql',
label: 'LogiQL',
alias: [],
mode: 'logiql'
},
{
name: 'lsl',
label: 'LSL',
alias: [],
mode: 'lsl'
},
{
name: 'lua',
label: 'Lua',
alias: [],
mode: 'lua'
},
{
name: 'luapage',
label: 'Luapage',
alias: [],
mode: 'luapage'
},
{
name: 'lucene',
label: 'Lucene',
alias: [],
mode: 'lucene'
},
{
name: 'makefile',
label: 'Makefile',
alias: [],
mode: 'makefile'
},
{
name: 'mask',
label: 'Mask',
alias: [],
mode: 'mask'
},
{
name: 'matlab',
label: 'MATLAB',
alias: [],
mode: 'matlab'
},
{
name: 'maze',
label: 'Maze',
alias: [],
mode: 'maze'
},
{
name: 'mel',
label: 'MEL',
alias: [],
mode: 'mel'
},
{
name: 'mipsassembler',
label: 'MIPS assembly',
alias: [],
mode: 'mipsassembler'
},
{
name: 'mushcode',
label: 'MUSHCode',
alias: [],
mode: 'mushcode'
},
{
name: 'mysql',
label: 'MySQL',
alias: [],
mode: 'mysql'
},
{
name: 'nix',
label: 'Nix',
alias: [],
mode: 'nix'
},
{
name: 'objectivec',
label: 'Objective C',
alias: ['objc'],
mode: 'objectivec'
},
{
name: 'ocaml',
label: 'OCaml',
alias: [],
mode: 'ocaml'
},
{
name: 'pascal',
label: 'Pascal',
alias: [],
mode: 'pascal'
},
{
name: 'perl',
label: 'Perl',
alias: [],
mode: 'perl'
},
{
name: 'pgsql',
label: 'Postgres SQL',
alias: ['postgres'],
mode: 'pgsql'
},
{
name: 'powershell',
label: 'PowerShell',
alias: ['ps1'],
mode: 'powershell'
},
{
name: 'praat',
label: 'Praat',
alias: [],
mode: 'praat'
},
{
name: 'prolog',
label: 'Prolog',
alias: ['pl', 'pro'],
mode: 'prolog'
},
{
name: 'properties',
label: 'Properties',
alias: [],
mode: 'properties'
},
{
name: 'protobuf',
label: 'Protocol Buffers',
alias: ['protocol', 'buffers'],
mode: 'protobuf'
},
{
name: 'r',
label: 'R',
alias: ['rlang'],
mode: 'r'
},
{
name: 'rdoc',
label: 'RDoc',
alias: [],
mode: 'rdoc'
},
{
name: 'rust',
label: 'Rust',
alias: [],
mode: 'rust'
},
{
name: 'sass',
label: 'Sass',
alias: [],
mode: 'sass'
},
{
name: 'scad',
label: 'SCAD',
alias: [],
mode: 'scad'
},
{
name: 'scala',
label: 'Scala',
alias: [],
mode: 'scala'
},
{
name: 'scheme',
label: 'Scheme',
alias: ['scm', 'ss'],
mode: 'scheme'
},
{
name: 'scss',
label: 'Scss',
alias: [],
mode: 'scss'
},
{
name: 'sh',
label: 'Shell',
alias: ['shell'],
mode: 'sh'
},
{
name: 'sjs',
label: 'StratifiedJS',
alias: ['stratified'],
mode: 'sjs'
},
{
name: 'smarty',
label: 'Smarty',
alias: [],
mode: 'smarty'
},
{
name: 'snippets',
label: 'Snippets',
alias: [],
mode: 'snippets'
},
{
name: 'soy_template',
label: 'Soy Template',
alias: ['soy'],
mode: 'soy_template'
},
{
name: 'space',
label: 'Space',
alias: [],
mode: 'space'
},
{
name: 'sql',
label: 'SQL',
alias: [],
mode: 'sql'
},
{
name: 'sqlserver',
label: 'SQL Server',
alias: [],
mode: 'sqlserver'
},
{
name: 'stylus',
label: 'Stylus',
alias: [],
mode: 'stylus'
},
{
name: 'svg',
label: 'SVG',
alias: [],
mode: 'svg'
},
{
name: 'swig',
label: 'SWIG',
alias: [],
mode: 'swig'
},
{
name: 'tcl',
label: 'Tcl',
alias: [],
mode: 'tcl'
},
{
name: 'tex',
label: 'TeX',
alias: [],
mode: 'tex'
},
{
name: 'textile',
label: 'Textile',
alias: [],
mode: 'textile'
},
{
name: 'toml',
label: 'TOML',
alias: [],
mode: 'toml'
},
{
name: 'twig',
label: 'Twig',
alias: [],
mode: 'twig'
},
{
name: 'vala',
label: 'Vala',
alias: [],
mode: 'vala'
},
{
name: 'vbscript',
label: 'VBScript',
alias: ['vbs', 'vbe'],
mode: 'vbscript'
},
{
name: 'velocity',
label: 'Velocity',
alias: [],
mode: 'velocity'
},
{
name: 'verilog',
label: 'Verilog',
alias: [],
mode: 'verilog'
},
{
name: 'vhdl',
label: 'VHDL',
alias: [],
mode: 'vhdl'
},
{
name: 'xml',
label: 'XML',
alias: [],
mode: 'xml'
},
{
name: 'xquery',
label: 'XQuery',
alias: [],
mode: 'xquery'
},
{
name: 'yaml',
label: 'YAML',
alias: [],
mode: 'yaml'
}
]
export default modes

View File

@@ -0,0 +1,7 @@
const electron = require('electron')
const shell = electron.shell
export default function (e) {
shell.openExternal(e.currentTarget.href)
e.preventDefault()
}

40
browser/lib/search.js Normal file
View File

@@ -0,0 +1,40 @@
'use strict'
var _ = require('lodash')
const TEXT_FILTER = 'TEXT_FILTER'
const FOLDER_FILTER = 'FOLDER_FILTER'
const TAG_FILTER = 'TAG_FILTER'
export default function search (articles, search) {
let filters = search.split(' ').map(key => key.trim()).filter(key => key.length > 0 && !key.match(/^#$/)).map(key => {
if (key.match(/^in:.+$/)) {
return {type: FOLDER_FILTER, value: key.match(/^in:(.+)$/)[1]}
}
if (key.match(/^#(.+)/)) {
return {type: TAG_FILTER, value: key.match(/^#(.+)$/)[1]}
}
return {type: TEXT_FILTER, value: key}
})
// let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER)
let textFilters = filters.filter(filter => filter.type === TEXT_FILTER)
let tagFilters = filters.filter(filter => filter.type === TAG_FILTER)
if (textFilters.length > 0) {
articles = textFilters.reduce((articles, textFilter) => {
return articles.filter(article => {
return article.title.match(new RegExp(textFilter.value, 'i')) || article.content.match(new RegExp(textFilter.value, 'i'))
})
}, articles)
}
if (tagFilters.length > 0) {
articles = tagFilters.reduce((articles, tagFilter) => {
return articles.filter(article => {
return _.find(article.Tags, tag => tag.name.match(new RegExp(tagFilter.value, 'i')))
})
}, articles)
}
return articles
}

View File

@@ -1,93 +0,0 @@
import React, { PropTypes } from 'react'
import { Link } from 'react-router'
import linkState from 'boost/linkState'
import { login } from 'boost/api'
import auth from 'boost/auth'
export default class LoginPage extends React.Component {
constructor (props) {
super(props)
this.state = {
user: {},
isSending: false,
error: null
}
this.linkState = linkState
}
handleSubmit (e) {
e.preventDefault()
this.setState({
isSending: true,
error: null
}, function () {
login(this.state.user)
.then(res => {
let { user, token } = res.body
auth.user(user, token)
this.props.history.pushState('home')
})
.catch(err => {
console.error(err)
if (err.code === 'ECONNREFUSED') {
return this.setState({
error: {
name: 'CunnectionRefused',
message: 'Can\'t cznnect to API server.'
},
isSending: false
})
} else if (err.status != null) {
return this.setState({
error: {
name: err.response.body.name,
message: err.response.body.message
},
isSending: false
})
}
else throw err
})
})
}
render () {
return (
<div className='LoginContainer'>
<img className='logo' src='../../resources/favicon-230x230.png'/>
<nav className='authNavigator text-center'>
<Link to='/login' activeClassName='active'>Log In</Link> / <Link to='/signup' activeClassName='active'>Sign Up</Link>
</nav>
<form onSubmit={e => this.handleSubmit(e)}>
<div className='formField'>
<input valueLink={this.linkState('user.email')} type='text' placeholder='E-mail'/>
</div>
<div className='formField'>
<input valueLink={this.linkState('user.password')} onChange={this.handleChange} type='password' placeholder='Password'/>
</div>
{this.state.isSending
? (
<p className='alertInfo'>Logging in...</p>
) : null}
{this.state.error != null ? <p className='alertError'>{this.state.error.message}</p> : null}
<div className='formField'>
<button className='logInButton' type='submit'>Log In</button>
</div>
</form>
</div>
)
}
}
LoginPage.propTypes = {
history: PropTypes.shape({
pushState: PropTypes.func
})
}

View File

@@ -1,104 +0,0 @@
import React, { PropTypes } from 'react'
import { Link } from 'react-router'
import linkState from 'boost/linkState'
import openExternal from 'boost/openExternal'
import { signup } from 'boost/api'
import auth from 'boost/auth'
export default class SignupContainer extends React.Component {
constructor (props) {
super(props)
this.state = {
user: {},
connectionFailed: false,
emailConflicted: false,
nameConflicted: false,
validationFailed: false,
isSending: false,
error: null
}
this.linkState = linkState
this.openExternal = openExternal
}
handleSubmit (e) {
this.setState({
isSending: true,
error: null
}, function () {
signup(this.state.user)
.then(res => {
let { user, token } = res.body
auth.user(user, token)
this.props.history.pushState('home')
})
.catch(err => {
console.error(err)
if (err.code === 'ECONNREFUSED') {
return this.setState({
error: {
name: 'CunnectionRefused',
message: 'Can\'t connect to API server.'
},
isSending: false
})
} else if (err.status != null) {
return this.setState({
error: {
name: err.response.body.name,
message: err.response.body.message
},
isSending: false
})
}
else throw err
})
})
e.preventDefault()
}
render () {
return (
<div className='SignupContainer'>
<img className='logo' src='../../resources/favicon-230x230.png'/>
<nav className='authNavigator text-center'><Link to='/login' activeClassName='active'>Log In</Link> / <Link to='/signup' activeClassName='active'>Sign Up</Link></nav>
<form onSubmit={e => this.handleSubmit(e)}>
<div className='formField'>
<input valueLink={this.linkState('user.email')} type='text' placeholder='E-mail'/>
</div>
<div className='formField'>
<input valueLink={this.linkState('user.password')} type='password' placeholder='Password'/>
</div>
<div className='formField'>
<input valueLink={this.linkState('user.name')} type='text' placeholder='name'/>
</div>
<div className='formField'>
<input valueLink={this.linkState('user.profileName')} type='text' placeholder='Profile name'/>
</div>
{this.state.isSending ? (
<p className='alertInfo'>Signing up...</p>
) : null}
{this.state.error != null ? <p className='alertError'>{this.state.error.message}</p> : null}
<div className='formField'>
<button className='logInButton' type='submit'>Sign Up</button>
</div>
</form>
<p className='alert'>会員登録することで<a onClick={this.openExternal} href='http://boostio.github.io/regulations.html'>当サイトの利用規約</a>及び<a onClick={this.openExternal} href='http://boostio.github.io/privacypolicies.html'>Cookieの使用を含むデータに関するポリシー</a></p>
</div>
)
}
}
SignupContainer.propTypes = {
history: PropTypes.shape({
pushState: PropTypes.func
})
}

168
browser/main/actions.js Normal file
View File

@@ -0,0 +1,168 @@
// Action types
export const USER_UPDATE = 'USER_UPDATE'
export const CLEAR_NEW_ARTICLE = 'CLEAR_NEW_ARTICLE'
export const ARTICLE_UPDATE = 'ARTICLE_UPDATE'
export const ARTICLE_DESTROY = 'ARTICLE_DESTROY'
export const FOLDER_CREATE = 'FOLDER_CREATE'
export const FOLDER_UPDATE = 'FOLDER_UPDATE'
export const FOLDER_DESTROY = 'FOLDER_DESTROY'
export const FOLDER_REPLACE = 'FOLDER_REPLACE'
export const SWITCH_FOLDER = 'SWITCH_FOLDER'
export const SWITCH_MODE = 'SWITCH_MODE'
export const SWITCH_ARTICLE = 'SWITCH_ARTICLE'
export const SET_SEARCH_FILTER = 'SET_SEARCH_FILTER'
export const SET_TAG_FILTER = 'SET_TAG_FILTER'
export const CLEAR_SEARCH = 'CLEAR_SEARCH'
export const LOCK_STATUS = 'LOCK_STATUS'
export const UNLOCK_STATUS = 'UNLOCK_STATUS'
export const TOGGLE_TUTORIAL = 'TOGGLE_TUTORIAL'
// Status - mode
export const IDLE_MODE = 'IDLE_MODE'
export const EDIT_MODE = 'EDIT_MODE'
// Article status
export const NEW = 'NEW'
export function updateUser (input) {
return {
type: USER_UPDATE,
data: input
}
}
// DB
export function clearNewArticle () {
return {
type: CLEAR_NEW_ARTICLE
}
}
export function updateArticle (article) {
return {
type: ARTICLE_UPDATE,
data: { article }
}
}
export function destroyArticle (key) {
return {
type: ARTICLE_DESTROY,
data: { key }
}
}
export function createFolder (folder) {
return {
type: FOLDER_CREATE,
data: { folder }
}
}
export function updateFolder (folder) {
return {
type: FOLDER_UPDATE,
data: { folder }
}
}
export function destroyFolder (key) {
return {
type: FOLDER_DESTROY,
data: { key }
}
}
export function replaceFolder (a, b) {
return {
type: FOLDER_REPLACE,
data: {
a,
b
}
}
}
export function switchFolder (folderName) {
return {
type: SWITCH_FOLDER,
data: folderName
}
}
export function switchMode (mode) {
return {
type: SWITCH_MODE,
data: mode
}
}
export function switchArticle (articleKey, isNew) {
return {
type: SWITCH_ARTICLE,
data: {
key: articleKey,
isNew: isNew
}
}
}
export function setSearchFilter (search) {
return {
type: SET_SEARCH_FILTER,
data: search
}
}
export function setTagFilter (tag) {
return {
type: SET_TAG_FILTER,
data: tag
}
}
export function clearSearch () {
return {
type: CLEAR_SEARCH
}
}
export function lockStatus () {
return {
type: LOCK_STATUS
}
}
export function unlockStatus () {
return {
type: UNLOCK_STATUS
}
}
export function toggleTutorial () {
return {
type: TOGGLE_TUTORIAL
}
}
export default {
updateUser,
clearNewArticle,
updateArticle,
destroyArticle,
createFolder,
updateFolder,
destroyFolder,
replaceFolder,
switchFolder,
switchMode,
switchArticle,
setSearchFilter,
setTagFilter,
clearSearch,
lockStatus,
unlockStatus,
toggleTutorial
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,71 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
<link rel="stylesheet" href="../../node_modules/font-awesome/css/font-awesome.min.css" media="screen" charset="utf-8">
<link rel="stylesheet" href="../../node_modules/devicon/devicon.min.css">
<link rel="stylesheet" href="../../node_modules/highlight.js/styles/xcode.css">
<link rel="shortcut icon" href="favicon.ico">
<title>Boostnote</title>
<style>
@font-face {
font-family: 'Lato';
src: url('../../resources/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
url('../../resources/Lato-Regular.woff') format('woff'), /* Modern Browsers */
url('../../resources/Lato-Regular.ttf') format('truetype');
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
}
#loadingCover{
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
box-sizing: border-box;
padding: 65px 0;
font-family: sans-serif;
}
#loadingCover img{
display: block;
margin: 75px auto 5px;
width: 160px;
height: 160px;
}
#loadingCover .message{
font-size: 30px;
text-align: center;
line-height: 1.6;
font-weight: 100;
color: #888;
}
</style>
</head>
<body>
<div id="loadingCover">
<img src="../../resources/app.png">
<div class='message'>Loading...</div>
</div>
<div id="content"></div>
<script src="../../submodules/ace/src-min/ace.js"></script>
<script type='text/javascript'>
const electron = require('electron')
electron.webFrame.setZoomLevelLimits(1, 1)
var version = electron.remote.app.getVersion()
const _ = require('lodash')
var scriptUrl = _.find(electron.remote.process.argv, a => a === '--hot')
? 'http://localhost:8080/assets/main.js'
: '../../compiled/main.js'
var scriptEl = document.createElement('script')
scriptEl.setAttribute("type","text/javascript")
scriptEl.setAttribute("src", scriptUrl)
document.getElementsByTagName("head")[0].appendChild(scriptEl)
</script>
</body>
</html>

View File

@@ -0,0 +1,108 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import linkState from 'boost/linkState'
import { createFolder } from 'boost/actions'
import store from 'boost/store'
import FolderMark from 'boost/components/FolderMark'
export default class CreateNewFolder extends React.Component {
constructor (props) {
super(props)
this.state = {
name: '',
color: Math.round(Math.random() * 7),
alert: null
}
}
componentDidMount () {
ReactDOM.findDOMNode(this.refs.folderName).focus()
}
handleCloseButton (e) {
this.props.close()
}
handleConfirmButton (e) {
this.setState({alert: null}, () => {
let { close } = this.props
let { name, color } = this.state
let input = {
name,
color
}
try {
store.dispatch(createFolder(input))
} catch (e) {
this.setState({alert: {
type: 'error',
message: e.message
}})
return
}
close()
})
}
handleColorClick (colorIndex) {
return e => {
this.setState({
color: colorIndex
})
}
}
handleKeyDown (e) {
if (e.keyCode === 13) {
this.handleConfirmButton()
}
}
render () {
let alert = this.state.alert
let alertElement = alert != null ? (
<p className={`alert ${alert.type}`}>
{alert.message}
</p>
) : null
let colorIndexes = []
for (let i = 0; i < 8; i++) {
colorIndexes.push(i)
}
let colorElements = colorIndexes.map(index => {
let className = 'option'
if (index === this.state.color) className += ' active'
return (
<span className={className} key={index} onClick={e => this.handleColorClick(index)(e)}>
<FolderMark color={index}/>
</span>
)
})
return (
<div className='CreateNewFolder modal'>
<button onClick={e => this.handleCloseButton(e)} className='closeBtn'><i className='fa fa-fw fa-times'/></button>
<div className='title'>Create new folder</div>
<input ref='folderName' onKeyDown={e => this.handleKeyDown(e)} className='ipt' type='text' valueLink={this.linkState('name')} placeholder='Enter folder name'/>
<div className='colorSelect'>
{colorElements}
</div>
{alertElement}
<button onClick={e => this.handleConfirmButton(e)} className='confirmBtn'>Create</button>
</div>
)
}
}
CreateNewFolder.propTypes = {
close: PropTypes.func
}
CreateNewFolder.prototype.linkState = linkState

View File

@@ -0,0 +1,255 @@
import React, { PropTypes } from 'react'
import ProfileImage from 'boost/components/ProfileImage'
import { searchUser, createTeam, setMember, deleteMember } from 'boost/api'
import linkState from 'boost/linkState'
import Select from 'react-select'
function getUsers (input, cb) {
searchUser(input)
.then(function (res) {
let users = res.body
cb(null, {
options: users.map(user => {
return { value: user.name, label: user.name }
}),
complete: false
})
})
.catch(function (err) {
console.error(err)
})
}
export default class CreateNewTeam extends React.Component {
constructor (props) {
super(props)
this.state = {
create: {
name: '',
alert: null
},
select: {
team: null,
newMember: null,
alert: null
},
currentTab: 'create',
currentUser: JSON.parse(localStorage.getItem('currentUser'))
}
}
handleCloseClick (e) {
this.props.close()
}
handleContinueClick (e) {
let createState = this.state.create
createState.isSending = true
createState.alert = {
type: 'info',
message: 'sending...'
}
this.setState({create: createState})
function onTeamCreate (res) {
let createState = this.state.create
createState.isSending = false
createState.alert = null
let selectState = this.state.select
selectState.team = res.body
this.setState({
currentTab: 'select',
create: createState,
select: {
team: res.body
}
})
}
function onError (err) {
let errorMessage = err.response != null ? err.response.body.message : 'Can\'t connect to API server.'
let createState = this.state.create
createState.isSending = false
createState.alert = {
type: 'error',
message: errorMessage
}
this.setState({
create: createState
})
}
createTeam({name: this.state.create.name})
.then(onTeamCreate.bind(this))
.catch(onError.bind(this))
}
renderCreateTab () {
let createState = this.state.create
let alertEl = createState.alert != null ? (
<p className={['alert'].concat([createState.alert.type]).join(' ')}>{createState.alert.message}</p>
) : null
return (
<div className='createTab'>
<div className='title'>Create new team</div>
<input valueLink={this.linkState('create.name')} className='ipt' type='text' placeholder='Enter your team name'/>
{alertEl}
<button onClick={e => this.handleContinueClick(e)} disabled={createState.isSending} className='confirmBtn'>Continue <i className='fa fa-arrow-right fa-fw'/></button>
</div>
)
}
handleNewMemberChange (value) {
let selectState = this.state.select
selectState.newMember = value
this.setState({select: selectState})
}
handleClickAddMemberButton (e) {
let selectState = this.state.select
let input = {
name: selectState.newMember,
role: 'member'
}
setMember(selectState.team.id, input)
.then(res => {
let selectState = this.state.select
let team = res.body
team.Members = team.Members.sort((a, b) => {
return new Date(a._pivot_createdAt) - new Date(b._pivot_createdAt)
})
selectState.team = team
selectState.newMember = ''
this.setState({select: selectState})
})
.catch(err => {
if (err.status != null) throw err
else console.error(err)
})
}
handleMemberDeleteButtonClick (name) {
let selectState = this.state.select
let input = {
name: name
}
return e => {
deleteMember(selectState.team.id, input)
.then(res => {
let selectState = this.state.select
let team = res.body
team.Members = team.Members.sort((a, b) => {
return new Date(a._pivot_createdAt) - new Date(b._pivot_createdAt)
})
selectState.team = team
selectState.newMember = ''
this.setState({select: selectState})
})
.catch(err => {
console.log(err, err.response)
if (err.status != null) throw err
else console.error(err)
})
}
}
handleMemberRoleChange (name) {
return function (e) {
let selectState = this.state.select
let input = {
name: name,
role: e.target.value
}
setMember(selectState.team.id, input)
.then(res => {
console.log(res.body)
})
.catch(err => {
if (err.status != null) throw err
else console.error(err)
})
}.bind(this)
}
renderSelectTab () {
let selectState = this.state.select
let membersEl = selectState.team.Members.map(member => {
let isCurrentUser = this.state.currentUser.id === member.id
return (
<li key={'user-' + member.id}>
<ProfileImage className='userPhoto' email={member.email} size='30'/>
<div className='userInfo'>
<div className='userName'>{`${member.profileName} (${member.name})`}</div>
<div className='userEmail'>{member.email}</div>
</div>
<div className='userControl'>
<select onChange={e => this.handleMemberRoleChange(member.name)(e)} disabled={isCurrentUser} value={member._pivot_role} className='userRole'>
<option value='owner'>Owner</option>
<option value='member'>Member</option>
</select>
<button onClick={e => this.handleMemberDeleteButtonClick(member.name)(e)} disabled={isCurrentUser}><i className='fa fa-times fa-fw'/></button>
</div>
</li>
)
})
return (
<div className='selectTab'>
<div className='title'>Select member</div>
<div className='memberForm'>
<Select
className='memberName'
autoload={false}
asyncOptions={getUsers}
onChange={val => this.handleNewMemberChange(val)}
value={selectState.newMember}
/>
<button onClick={e => this.handleClickAddMemberButton(e)} className='addMemberBtn'>add</button>
</div>
<ul className='memberList'>
{membersEl}
</ul>
<button onClick={e => this.props.close(e)}className='confirmBtn'>Done</button>
</div>
)
}
render () {
let currentTab = this.state.currentTab === 'create'
? this.renderCreateTab()
: this.renderSelectTab()
return (
<div className='CreateNewTeam modal'>
<button onClick={e => this.handleCloseClick(e)} className='closeBtn'><i className='fa fa-fw fa-times'/></button>
{currentTab}
<div className='tabNav'>
<i className={'fa fa-circle fa-fw' + (this.state.currentTab === 'create' ? ' active' : '')}/>
<i className={'fa fa-circle fa-fw' + (this.state.currentTab === 'select' ? ' active' : '')}/>
</div>
</div>
)
}
}
CreateNewTeam.propTypes = {
close: PropTypes.func
}
CreateNewTeam.prototype.linkState = linkState

View File

@@ -0,0 +1,41 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import store from 'boost/store'
import { unlockStatus, clearNewArticle } from 'boost/actions'
export default class EditedAlert extends React.Component {
componentDidMount () {
ReactDOM.findDOMNode(this.refs.no).focus()
}
handleNoButtonClick (e) {
this.props.close()
}
handleYesButtonClick (e) {
store.dispatch(unlockStatus())
store.dispatch(this.props.action)
store.dispatch(clearNewArticle())
this.props.close()
}
render () {
return (
<div className='EditedAlert modal'>
<div className='title'>Your article is still editing!</div>
<div className='message'>Do you really want to leave without finishing?</div>
<div className='control'>
<button ref='no' onClick={e => this.handleNoButtonClick(e)}><i className='fa fa-fw fa-close'/> No</button>
<button ref='yes' onClick={e => this.handleYesButtonClick(e)} className='primary'><i className='fa fa-fw fa-check'/> Yes</button>
</div>
</div>
)
}
}
EditedAlert.propTypes = {
action: PropTypes.object,
close: PropTypes.func
}

View File

@@ -0,0 +1,146 @@
import React, { PropTypes } from 'react'
import linkState from 'boost/linkState'
import { updateUser } from 'boost/actions'
const electron = require('electron')
const ipc = electron.ipcRenderer
const remote = electron.remote
export default class AppSettingTab extends React.Component {
constructor (props) {
super(props)
let keymap = remote.getGlobal('keymap')
let userName = props.user != null ? props.user.name : null
this.state = {
user: {
name: userName,
alert: null
},
userAlert: null,
keymap: {
toggleFinder: keymap.toggleFinder
},
keymapAlert: null
}
}
componentDidMount () {
this.handleSettingDone = () => {
this.setState({keymapAlert: {
type: 'success',
message: 'Successfully done!'
}})
}
this.handleSettingError = err => {
this.setState({keymapAlert: {
type: 'error',
message: err.message != null ? err.message : 'Error occurs!'
}})
}
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
ipc.addListener('APP_SETTING_ERROR', this.handleSettingError)
}
componentWillUnmount () {
ipc.removeListener('APP_SETTING_DONE', this.handleSettingDone)
ipc.removeListener('APP_SETTING_ERROR', this.handleSettingError)
}
submitHotKey () {
ipc.send('hotkeyUpdated', {
toggleFinder: this.state.keymap.toggleFinder
})
}
handleSaveButtonClick (e) {
this.submitHotKey()
}
handleKeyDown (e) {
if (e.keyCode === 13) {
this.submitHotKey()
}
}
handleNameSaveButtonClick (e) {
let { dispatch } = this.props
dispatch(updateUser({name: this.state.user.name}))
this.setState({
userAlert: {
type: 'success',
message: 'Successfully done!'
}
})
}
render () {
let keymapAlert = this.state.keymapAlert
let keymapAlertElement = keymapAlert != null
? (
<p className={`alert ${keymapAlert.type}`}>
{keymapAlert.message}
</p>
) : null
let userAlert = this.state.userAlert
let userAlertElement = userAlert != null
? (
<p className={`alert ${userAlert.type}`}>
{userAlert.message}
</p>
) : null
return (
<div className='AppSettingTab content'>
<div className='section'>
<div className='sectionTitle'>User's info</div>
<div className='sectionInput'>
<label>User name</label>
<input valueLink={this.linkState('user.name')} type='text'/>
</div>
<div className='sectionConfirm'>
<button onClick={e => this.handleNameSaveButtonClick(e)}>Save</button>
{userAlertElement}
</div>
</div>
<div className='section'>
<div className='sectionTitle'>Hotkey</div>
<div className='sectionInput'>
<label>Toggle Finder(popup)</label>
<input onKeyDown={e => this.handleKeyDown(e)} valueLink={this.linkState('keymap.toggleFinder')} type='text'/>
</div>
<div className='sectionConfirm'>
<button onClick={e => this.handleSaveButtonClick(e)}>Save</button>
{keymapAlertElement}
</div>
<div className='description'>
<ul>
<li><code>0</code> to <code>9</code></li>
<li><code>A</code> to <code>Z</code></li>
<li><code>F1</code> to <code>F24</code></li>
<li>Punctuations like <code>~</code>, <code>!</code>, <code>@</code>, <code>#</code>, <code>$</code>, etc.</li>
<li><code>Plus</code></li>
<li><code>Space</code></li>
<li><code>Backspace</code></li>
<li><code>Delete</code></li>
<li><code>Insert</code></li>
<li><code>Return</code> (or <code>Enter</code> as alias)</li>
<li><code>Up</code>, <code>Down</code>, <code>Left</code> and <code>Right</code></li>
<li><code>Home</code> and <code>End</code></li>
<li><code>PageUp</code> and <code>PageDown</code></li>
<li><code>Escape</code> (or <code>Esc</code> for short)</li>
<li><code>VolumeUp</code>, <code>VolumeDown</code> and <code>VolumeMute</code></li>
<li><code>MediaNextTrack</code>, <code>MediaPreviousTrack</code>, <code>MediaStop</code> and <code>MediaPlayPause</code></li>
</ul>
</div>
</div>
</div>
)
}
}
AppSettingTab.prototype.linkState = linkState
AppSettingTab.propTypes = {
dispatch: PropTypes.func
}

View File

@@ -0,0 +1,123 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { getClientKey } from 'boost/activityRecord'
import linkState from 'boost/linkState'
import _ from 'lodash'
import { request, WEB_URL } from 'boost/api'
const FORM_MODE = 'FORM_MODE'
const DONE_MODE = 'DONE_MODE'
export default class ContactTab extends React.Component {
constructor (props) {
super(props)
this.state = {
title: '',
content: '',
email: '',
mode: FORM_MODE,
alert: null
}
}
componentDidMount () {
let titleInput = ReactDOM.findDOMNode(this.refs.title)
if (titleInput != null) titleInput.focus()
}
handleBackButtonClick (e) {
this.setState({
mode: FORM_MODE
})
}
handleSendButtonClick (e) {
let input = _.pick(this.state, ['title', 'content', 'email'])
input.clientKey = getClientKey()
this.setState({
alert: {
type: 'info',
message: 'Sending...'
}
}, () => {
request.post(WEB_URL + 'apis/inquiry')
.send(input)
.then(res => {
console.log('sent')
this.setState({
title: '',
content: '',
mode: DONE_MODE,
alert: null
})
})
.catch(err => {
if (err.code === 'ECONNREFUSED') {
this.setState({
alert: {
type: 'error',
message: 'Can\'t connect to API server.'
}
})
} else {
console.error(err)
this.setState({
alert: {
type: 'error',
message: err.message
}
})
}
})
})
}
render () {
switch (this.state.mode) {
case DONE_MODE:
return (
<div className='ContactTab content done'>
<div className='message'>
<i className='checkIcon fa fa-check-circle'/><br/>
Your message has been sent successfully!!
</div>
<div className='control'>
<button onClick={e => this.handleBackButtonClick(e)}>Back to Contact form</button>
</div>
</div>
)
case FORM_MODE:
default:
let alertElement = this.state.alert != null
? (
<div className={'alert ' + this.state.alert.type}>{this.state.alert.message}</div>
)
: null
return (
<div className='ContactTab content form'>
<div className='title'>Contact form</div>
<div className='description'>
Your feedback is highly appreciated and will help us to improve our app. :D
</div>
<div className='iptGroup'>
<input ref='title' valueLink={this.linkState('title')} placeholder='Title' type='text'/>
</div>
<div className='iptGroup'>
<textarea valueLink={this.linkState('content')} placeholder='Content'/>
</div>
<div className='iptGroup'>
<input valueLink={this.linkState('email')} placeholder='E-mail (Optional)' type='email'/>
</div>
<div className='formControl'>
<button onClick={e => this.handleSendButtonClick(e)} className='primary'>Send</button>
{alertElement}
</div>
</div>
)
}
}
}
ContactTab.prototype.linkState = linkState

View File

@@ -0,0 +1,187 @@
import React, { PropTypes } from 'react'
import linkState from 'boost/linkState'
import FolderMark from 'boost/components/FolderMark'
import store from 'boost/store'
import { updateFolder, destroyFolder, replaceFolder } from 'boost/actions'
const IDLE = 'IDLE'
const EDIT = 'EDIT'
const DELETE = 'DELETE'
export default class FolderRow extends React.Component {
constructor (props) {
super(props)
this.state = {
mode: IDLE
}
}
handleUpClick (e) {
let { index } = this.props
if (index > 0) {
store.dispatch(replaceFolder(index, index - 1))
}
}
handleDownClick (e) {
let { index, count } = this.props
if (index < count - 1) {
store.dispatch(replaceFolder(index, index + 1))
}
}
handleCancelButtonClick (e) {
this.setState({
mode: IDLE
})
}
handleEditButtonClick (e) {
this.setState({
mode: EDIT,
name: this.props.folder.name,
color: this.props.folder.color,
isColorEditing: false
})
}
handleDeleteButtonClick (e) {
this.setState({mode: DELETE})
}
handleNameInputKeyDown (e) {
if (e.keyCode === 13) {
this.handleSaveButtonClick()
}
}
handleColorSelectClick (e) {
this.setState({
isColorEditing: true
})
}
handleColorButtonClick (index) {
return e => {
this.setState({
color: index,
isColorEditing: false
})
}
}
handleSaveButtonClick (e) {
let { folder, setAlert } = this.props
setAlert(null, () => {
let input = {
name: this.state.name,
color: this.state.color
}
folder = Object.assign({}, folder, input)
try {
store.dispatch(updateFolder(folder))
this.setState({
mode: IDLE
})
} catch (e) {
console.error(e)
setAlert({
type: 'error',
message: e.message
})
}
})
}
handleDeleteConfirmButtonClick (e) {
let { folder } = this.props
store.dispatch(destroyFolder(folder.key))
}
render () {
let folder = this.props.folder
switch (this.state.mode) {
case EDIT:
let colorIndexes = []
for (let i = 0; i < 8; i++) {
colorIndexes.push(i)
}
let colorOptions = colorIndexes.map(index => {
let className = this.state.color === index
? 'active'
: null
return (
<button onClick={e => this.handleColorButtonClick(index)(e)} className={className} key={index}>
<FolderMark color={index}/>
</button>
)
})
return (
<div className='FolderRow edit'>
<div className='folderColor'>
<button onClick={e => this.handleColorSelectClick(e)} className='select'>
<FolderMark color={this.state.color}/>
</button>
{this.state.isColorEditing
? (
<div className='options'>
<div className='label'>Color select</div>
{colorOptions}
</div>
)
: null
}
</div>
<div className='folderName'>
<input onKeyDown={e => this.handleNameInputKeyDown(e)} valueLink={this.linkState('name')} type='text'/>
</div>
<div className='folderControl'>
<button onClick={e => this.handleSaveButtonClick(e)} className='primary'>Save</button>
<button onClick={e => this.handleCancelButtonClick(e)}>Cancel</button>
</div>
</div>
)
case DELETE:
return (
<div className='FolderRow delete'>
<div className='folderDeleteLabel'>Are you sure to delete <strong>{folder.name}</strong> folder?</div>
<div className='folderControl'>
<button onClick={e => this.handleDeleteConfirmButtonClick(e)} className='primary'>Sure</button>
<button onClick={e => this.handleCancelButtonClick(e)}>Cancel</button>
</div>
</div>
)
case IDLE:
default:
return (
<div className='FolderRow'>
<div className='sortBtns'>
<button onClick={e => this.handleUpClick(e)}><i className='fa fa-sort-up fa-fw'/></button>
<button onClick={e => this.handleDownClick(e)}><i className='fa fa-sort-down fa-fw'/></button>
</div>
<div className='folderColor'><FolderMark color={folder.color}/></div>
<div className='folderName'>{folder.name}</div>
<div className='folderControl'>
<button onClick={e => this.handleEditButtonClick(e)}><i className='fa fa-fw fa-edit'/></button>
<button onClick={e => this.handleDeleteButtonClick(e)}><i className='fa fa-fw fa-close'/></button>
</div>
</div>
)
}
}
}
FolderRow.propTypes = {
folder: PropTypes.shape(),
index: PropTypes.number,
count: PropTypes.number,
setAlert: PropTypes.func
}
FolderRow.prototype.linkState = linkState

View File

@@ -0,0 +1,99 @@
import React, { PropTypes } from 'react'
import FolderRow from './FolderRow'
import linkState from 'boost/linkState'
import { createFolder } from 'boost/actions'
export default class FolderSettingTab extends React.Component {
constructor (props) {
super(props)
this.state = {
name: ''
}
}
handleNewFolderNameKeyDown (e) {
if (e.keyCode === 13) {
this.handleSaveButtonClick()
}
}
handleSaveButtonClick (e) {
this.setState({alert: null}, () => {
if (this.state.name.trim().length === 0) return false
let { dispatch } = this.props
try {
dispatch(createFolder({
name: this.state.name
}))
} catch (e) {
this.setState({alert: {
type: 'error',
message: e.message
}})
return
}
this.setState({name: ''})
})
}
setAlert (alert, cb) {
this.setState({alert: alert}, cb)
}
render () {
let { folders } = this.props
let folderElements = folders.map((folder, index) => {
return (
<FolderRow
key={'folder-' + folder.key}
folder={folder}
index={index}
count={folders.length}
setAlert={(alert, cb) => this.setAlert(alert, cb)}
/>
)
})
let alert = this.state.alert
let alertElement = alert != null ? (
<p className={`alert ${alert.type}`}>
{alert.message}
</p>
) : null
return (
<div className='FolderSettingTab content'>
<div className='section'>
<div className='sectionTitle'>Manage folder</div>
<div className='folderTable'>
<div className='folderHeader'>
<div className='folderName'>Folder</div>
<div className='folderControl'>Edit/Delete</div>
</div>
{folderElements}
<div className='newFolder'>
<div className='folderName'>
<input onKeyDown={e => this.handleNewFolderNameKeyDown(e)} valueLink={this.linkState('name')} type='text' placeholder='New Folder'/>
</div>
<div className='folderControl'>
<button onClick={e => this.handleSaveButtonClick(e)} className='primary'>Add</button>
</div>
</div>
{alertElement}
</div>
</div>
</div>
)
}
}
FolderSettingTab.propTypes = {
folders: PropTypes.array,
dispatch: PropTypes.func
}
FolderSettingTab.prototype.linkState = linkState

View File

@@ -0,0 +1,11 @@
import React, { PropTypes } from 'react'
export default class HelpTab extends React.Component {
render () {
return (
<div className='content help'>
Comming soon
</div>
)
}
}

View File

@@ -0,0 +1,106 @@
import React, { PropTypes } from 'react'
import ProfileImage from 'boost/components/ProfileImage'
import api from 'boost/api'
const IDLE = 'IDLE'
const DELETE = 'DELETE'
export default class MemberRow extends React.Component {
constructor (props) {
super(props)
this.state = {
mode: IDLE
}
}
handleMemberRoleChange (e) {
let input = {
name: this.props.member.name,
role: e.target.value
}
api.setMember(this.props.team.id, input)
.then(res => {
console.log(res.body)
})
.catch(err => {
if (err.status != null) throw err
else console.error(err)
})
}
handleDeleteButtonClick (e) {
this.setState({mode: DELETE})
}
handleCancelButtonClick (e) {
this.setState({mode: IDLE})
}
handleDeleteConfirmButtonClick (e) {
let input = {
name: this.props.member.name
}
api.deleteMember(this.props.team.id, input)
.then(res => {
console.log(res.body)
})
.catch(err => {
if (err.status != null) throw err
else console.error(err)
})
}
render () {
let member = this.props.member
let currentUser = this.props.currentUser
let isDisabled = (currentUser.id === member.id)
switch (this.state.mode) {
case DELETE:
return (
<li className='MemberRow edit'>
<div className='colDescription'>
Are you sure to remove <strong>{member.profileName}</strong> ?
</div>
<div className='colDeleteConfirm'>
<button className='deleteButton primary' onClick={e => this.handleDeleteConfirmButtonClick(e)}>Sure</button>
<button className='deleteButton' onClick={e => this.handleCancelButtonClick(e)}>Cancel</button>
</div>
</li>
)
case IDLE:
default:
return (
<li className='MemberRow'>
<div className='colUserName'>
<ProfileImage className='userPhoto' email={member.email} size='30'/>
<div className='userInfo'>
<div className='userName'>{`${member.profileName} (${member.name})`}</div>
<div className='userEmail'>{member.email}</div>
</div>
</div>
<div className='colRole'>
<select onChange={e => this.handleMemberRoleChange(e)} disabled={isDisabled} value={member._pivot_role} className='userRole'>
<option value='owner'>Owner</option>
<option value='member'>Member</option>
</select>
</div>
<div className='colDelete'>
<button className='deleteButton' onClick={e => this.handleDeleteButtonClick(e)} disabled={isDisabled}><i className='fa fa-times fa-fw'/></button>
</div>
</li>
)
}
}
}
MemberRow.propTypes = {
member: PropTypes.shape(),
currentUser: PropTypes.shape(),
team: PropTypes.shape({
id: PropTypes.number
})
}

View File

@@ -0,0 +1,149 @@
import React, { PropTypes } from 'react'
import ProfileImage from 'boost/components/ProfileImage'
import Select from 'react-select'
import api from 'boost/api'
import _ from 'lodash'
import MemberRow from './MemberRow'
function getUsers (input, cb) {
api.searchUser(input)
.then(function (res) {
let users = res.body
cb(null, {
options: users.map(user => {
return { value: user.name, label: user.name }
}),
complete: false
})
})
.catch(function (err) {
console.error(err)
})
}
export default class MemberSettingTab extends React.Component {
constructor (props) {
super(props)
this.state = {
newMember: ''
}
}
getCurrentTeam (props) {
if (props == null) props = this.props
return _.findWhere(props.teams, {id: props.currentTeamId})
}
handleTeamSelectChange (e) {
this.props.switchTeam(e.target.value)
}
handleNewMemberChange (value) {
this.setState({newMember: value})
}
handleClickAddMemberButton (e) {
let team = this.getCurrentTeam()
if (team == null || team.userType !== 'team') return null
let input = {
name: this.state.newMember,
role: 'member'
}
api.setMember(team.id, input)
.then(res => {
console.log(res.body)
})
.catch(err => {
if (err.status != null) throw err
else console.error(err)
})
}
renderTeamOptions () {
return this.props.teams.map(team => {
return (
<option key={'team-' + team.id} value={team.id}>{team.name}</option>)
})
}
render () {
console.log(this.props.teams)
let team = this.getCurrentTeam()
if (team == null || team.userType === 'person') {
return this.renderNoTeam()
}
let membersEl = team.Members.map(member => (
<MemberRow key={'user-' + member.id} member={member} team={team} currentUser={this.props.currentUser}/>
))
return (
<div className='MemberSettingTab content'>
<div className='header'>
<span>Setting of</span>
<select
value={this.props.currentTeamId}
onChange={e => this.handleTeamSelectChange(e)}
className='teamSelect'>
{this.renderTeamOptions()}
</select>
</div>
<div className='membersTableSection section'>
<div className='sectionTitle'>Members</div>
<div className='addMember'>
<div className='addMemberLabel'>Add member</div>
<div className='addMemberControl'>
<Select
className='memberName'
placeholder='Input username to add'
autoload={false}
asyncOptions={getUsers}
onChange={val => this.handleNewMemberChange(val)}
value={this.state.newMember}
/>
<button onClick={e => this.handleClickAddMemberButton(e)} className='addMemberBtn'>add</button>
</div>
</div>
<ul className='memberList'>
<li className='header'>
<div className='colUserName'>Username</div>
<div className='colRole'>Role</div>
<div className='colDelete'>Delete</div>
</li>
{membersEl}
</ul>
</div>
</div>
)
}
renderNoTeam () {
return (
<div className='TeamSettingTab content'>
<div className='header'>
<span>Setting of</span>
<select
value={this.props.currentTeamId}
onChange={e => this.handleTeamSelectChange(e)}
className='teamSelect'>
{this.renderTeamOptions()}
</select>
</div>
<div className='section'>Please select a team</div>
</div>
)
}
}
MemberSettingTab.propTypes = {
currentUser: PropTypes.shape(),
teams: PropTypes.array,
currentTeamId: PropTypes.number,
switchTeam: PropTypes.func
}

View File

@@ -0,0 +1,171 @@
import React, { PropTypes } from 'react'
import _ from 'lodash'
import linkState from 'boost/linkState'
import api from 'boost/api'
export default class TeamSettingTab extends React.Component {
constructor (props) {
super(props)
let team = this.getCurrentTeam(props)
this.state = {
teamName: team != null ? team.profileName : '',
deleteConfirm: false,
alert: null
}
}
componentWillReceiveProps (nextProps) {
let team = this.getCurrentTeam(nextProps)
this.setState({
teamName: team != null ? team.profileName : '',
deleteConfirm: false
})
}
getCurrentTeam (props) {
if (props == null) props = this.props
return _.findWhere(props.teams, {id: props.currentTeamId})
}
handleTeamSelectChange (e) {
this.props.switchTeam(e.target.value)
}
handleSaveButtonClick (e) {
let input = {
profileName: this.state.teamName
}
let alert = {
type: 'info',
message: 'Sending...'
}
this.setState({alert}, () => {
api.updateTeamInfo(this.props.currentTeamId, input)
.then(res => {
console.log(res.body)
let alert = {
type: 'success',
message: 'Successfully done!'
}
this.setState({alert})
})
.catch(err => {
var message
if (err.status != null) {
message = err.response.body.message
} else if (err.code === 'ECONNREFUSED') {
message = 'Can\'t connect to API server.'
} else throw err
let alert = {
type: 'error',
message: message
}
this.setState({alert})
})
})
}
handleDeleteConfirmButtonClick (e) {
api.destroyTeam(this.props.currentTeamId)
.then(res => {
console.log(res.body)
})
.catch(err => {
let message
if (err.status != null) {
message = err.response.body.message
} else if (err.code === 'ECONNREFUSED') {
message = 'Can\'t connect to API server.'
} else throw err
console.log(message)
})
}
renderTeamOptions () {
return this.props.teams.map(team => {
return (
<option key={'team-' + team.id} value={team.id}>{team.name}</option>)
})
}
render () {
let team = this.getCurrentTeam()
if (team == null || team.userType === 'person') {
return this.renderNoTeam()
}
return (
<div className='TeamSettingTab content'>
<div className='header'>
<span>Setting of</span>
<select
value={this.props.currentTeamId}
onChange={e => this.handleTeamSelectChange(e)}
className='teamSelect'>
{this.renderTeamOptions()}
</select>
</div>
<div className='section'>
<div className='sectionTitle'>Team profile</div>
<div className='sectionInput'>
<label className='label'>Team Name</label>
<input valueLink={this.linkState('teamName')} type='text'/>
</div>
<div className='sectionConfirm'>
<button onClick={e => this.handleSaveButtonClick(e)}>Save</button>
{this.state.alert != null
? (
<div className={'alert ' + this.state.alert.type}>{this.state.alert.message}</div>
)
: null}
</div>
</div>
{!this.state.deleteConfirm
? (
<div className='section teamDelete'>
<label>Delete this team</label>
<button onClick={e => this.setState({deleteConfirm: true})} className='deleteBtn'><i className='fa fa-fw fa-trash'/> Delete</button>
</div>
)
: (
<div className='section teamDeleteConfirm'>
<label>Are you sure to delete this team?</label>
<button onClick={e => this.setState({deleteConfirm: false})}>Cancel</button>
<button onClick={e => this.handleDeleteConfirmButtonClick(e)} className='deleteBtn'><i className='fa fa-fw fa-check'/> Sure</button>
</div>
)}
</div>
)
}
renderNoTeam () {
return (
<div className='TeamSettingTab content'>
<div className='header'>
<span>Setting of</span>
<select
value={this.props.currentTeamId}
onChange={e => this.handleTeamSelectChange(e)}
className='teamSelect'>
{this.renderTeamOptions()}
</select>
</div>
<div className='section'>Please select a team</div>
</div>
)
}
}
TeamSettingTab.propTypes = {
currentTeamId: PropTypes.number,
teams: PropTypes.array,
switchTeam: PropTypes.func
}
TeamSettingTab.prototype.linkState = linkState

View File

@@ -0,0 +1,122 @@
import React, { PropTypes } from 'react'
import { connect, Provider } from 'react-redux'
import linkState from 'boost/linkState'
import store from 'boost/store'
import AppSettingTab from './Preference/AppSettingTab'
import HelpTab from './Preference/HelpTab'
import FolderSettingTab from './Preference/FolderSettingTab'
import ContactTab from './Preference/ContactTab'
import { closeModal } from 'boost/modal'
const APP = 'APP'
const HELP = 'HELP'
const FOLDER = 'FOLDER'
const CONTACT = 'CONTACT'
class Preferences extends React.Component {
constructor (props) {
super(props)
this.state = {
currentTab: APP
}
}
switchTeam (teamId) {
this.setState({currentTeamId: teamId})
}
handleNavButtonClick (tab) {
return e => {
this.setState({currentTab: tab})
}
}
render () {
let content = this.renderContent()
let tabs = [
{target: APP, label: 'Preferences'},
{target: FOLDER, label: 'Manage folder'},
{target: CONTACT, label: 'Contact form'}
]
let navButtons = tabs.map(tab => (
<button key={tab.target} onClick={e => this.handleNavButtonClick(tab.target)(e)} className={this.state.currentTab === tab.target ? 'active' : ''}>{tab.label}</button>
))
return (
<div className='Preferences modal'>
<div className='header'>
<div className='title'>Setting</div>
<button onClick={e => closeModal()} className='closeBtn'>Done</button>
</div>
<div className='nav'>
{navButtons}
</div>
{content}
</div>
)
}
renderContent () {
let { user, folders, dispatch } = this.props
switch (this.state.currentTab) {
case HELP:
return (<HelpTab/>)
case FOLDER:
return (
<FolderSettingTab
dispatch={dispatch}
folders={folders}
/>
)
case CONTACT:
return (
<ContactTab/>
)
case APP:
default:
return (
<AppSettingTab
user={user}
dispatch={dispatch}
/>
)
}
}
}
Preferences.propTypes = {
user: PropTypes.shape({
name: PropTypes.string
}),
folders: PropTypes.array,
dispatch: PropTypes.func
}
Preferences.prototype.linkState = linkState
function remap (state) {
let { user, folders, status } = state
return {
user,
folders,
status
}
}
let RootComponent = connect(remap)(Preferences)
export default class PreferencesModal extends React.Component {
render () {
return (
<Provider store={store}>
<RootComponent/>
</Provider>
)
}
}

View File

@@ -0,0 +1,115 @@
import React, { PropTypes } from 'react'
import MarkdownPreview from 'boost/components/MarkdownPreview'
import CodeEditor from 'boost/components/CodeEditor'
export default class Tutorial extends React.Component {
constructor (props) {
super(props)
this.state = {
slideIndex: 0
}
}
handlePriorSlideClick () {
if (this.state.slideIndex > 0) this.setState({slideIndex: this.state.slideIndex - 1})
}
handleNextSlideClick () {
if (this.state.slideIndex < 4) this.setState({slideIndex: this.state.slideIndex + 1})
}
startButtonClick (e) {
this.props.close()
}
render () {
let content = this.renderContent(this.state.slideIndex)
let dotElements = []
for (let i = 0; i < 5; i++) {
dotElements.push(<i key={i} className={'fa fa-fw fa-circle' + (i === this.state.slideIndex ? ' active' : '')}/>)
}
return (
<div className='Tutorial modal'>
<button onClick={e => this.handlePriorSlideClick()} className={'priorBtn' + (this.state.slideIndex === 0 ? ' hide' : '')}>
<i className='fa fa-fw fa-angle-left'/>
</button>
<button onClick={e => this.handleNextSlideClick()} className={'nextBtn' + (this.state.slideIndex === 4 ? ' hide' : '')}>
<i className='fa fa-fw fa-angle-right'/>
</button>
{content}
<div className='dots'>
{dotElements}
</div>
</div>
)
}
renderContent (index) {
switch (index) {
case 0:
return (<div className='slide slide0'>
<div className='title'>Welcome to Boost</div>
<div className='content'>
Boost is a brand new note app for software<br/>
Don't waste time cleaning up your data.<br/>
devote that time to more creative work.<br/>
Hack your memory.
</div>
</div>)
case 1:
let content = '## Boost is a note app for engineer.\n\n - Write with markdown\n - Stylize beautiful'
return (<div className='slide slide1'>
<div className='title'>Write with Markdown</div>
<div className='content'>
Markdown is available.<br/>
Your notes will be stylized beautifully and quickly.
<div className='markdown'>
<pre className='left'>{content}</pre>
<MarkdownPreview className='right' content={content}/>
</div>
</div>
</div>)
case 2:
let code = 'import shell from \'shell\'\r\nvar React = require(\'react\')\r\nvar { PropTypes } = React\r\nimport markdown from \'boost\/markdown\'\r\nvar ReactDOM = require(\'react-dom\')\r\n\r\nfunction handleAnchorClick (e) {\r\n shell.openExternal(e.target.href)\r\n e.preventDefault()\r\n}\r\n\r\nexport default class MarkdownPreview extends React.Component {\r\n componentDidMount () {\r\n this.addListener()\r\n }\r\n\r\n componentDidUpdate () {\r\n this.addListener()\r\n }\r\n\r\n componentWillUnmount () {\r\n this.removeListener()\r\n }'
return (<div className='slide slide2'>
<div className='title'>Beautiful code highlighting</div>
<div className='content'>
Boost supports code syntax highlighting.<br/>
There are more than 100 different type of language.
<div className='code'>
<CodeEditor readOnly mode='jsx' code={code}/>
</div>
</div>
</div>)
case 3:
return (<div className='slide slide3'>
<div className='title'>Easy to access with Finder</div>
<div className='content'>
The Finder helps you organize all of the files and documents.<br/>
There is a short-cut key [control + shift + tab] to open the Finder.<br/>
It is available to save your articles on the Clipboard<br/>
by selecting your file with pressing Enter key,<br/>
and to paste the contents of the Clipboard with [{process.platform === 'darwin' ? 'Command' : 'Control'}-V]
<img width='480' src='../../resources/finder.png'/>
</div>
</div>)
case 4:
return (<div className='slide slide4'>
<div className='title'>Are you ready?</div>
<div className='content'>
<button onClick={e => this.startButtonClick(e)}>Start<br/>Boost</button>
</div>
</div>)
default:
return null
}
}
}
Tutorial.propTypes = {
close: PropTypes.func
}

273
browser/main/reducer.js Normal file
View File

@@ -0,0 +1,273 @@
import { combineReducers } from 'redux'
import _ from 'lodash'
import {
// Status action type
SWITCH_FOLDER,
SWITCH_MODE,
SWITCH_ARTICLE,
SET_SEARCH_FILTER,
SET_TAG_FILTER,
CLEAR_SEARCH,
LOCK_STATUS,
UNLOCK_STATUS,
TOGGLE_TUTORIAL,
// user
USER_UPDATE,
// Article action type
ARTICLE_UPDATE,
ARTICLE_DESTROY,
CLEAR_NEW_ARTICLE,
// Folder action type
FOLDER_CREATE,
FOLDER_UPDATE,
FOLDER_DESTROY,
FOLDER_REPLACE,
// view mode
IDLE_MODE
} from './actions'
import dataStore from 'boost/dataStore'
import keygen from 'boost/keygen'
import activityRecord from 'boost/activityRecord'
import { openModal } from 'boost/modal'
import EditedAlert from 'boost/components/modal/EditedAlert'
const initialStatus = {
mode: IDLE_MODE,
search: '',
isTutorialOpen: false,
isStatusLocked: false
}
let data = dataStore.getData()
let initialArticles = data.articles
let initialFolders = data.folders
let initialUser = dataStore.getUser().user
let isStatusLocked = false
let isCreatingNew = false
function user (state = initialUser, action) {
switch (action.type) {
case USER_UPDATE:
let updated = Object.assign(state, action.data)
dataStore.saveUser(null, updated)
return updated
default:
return state
}
}
function folders (state = initialFolders, action) {
state = state.slice()
switch (action.type) {
case FOLDER_CREATE:
{
let newFolder = action.data.folder
if (!_.isString(newFolder.name)) throw new Error('Folder name must be a string')
newFolder.name = newFolder.name.trim().replace(/\s/, '_')
Object.assign(newFolder, {
key: keygen(),
createdAt: new Date(),
updatedAt: new Date()
})
if (newFolder.name == null && newFolder.name.length === 0) throw new Error('Folder name is required')
if (newFolder.name.match(/\//)) throw new Error('`/` is not available for folder name')
let conflictFolder = _.findWhere(state, {name: newFolder.name})
if (conflictFolder != null) throw new Error(`${newFolder.name} already exists!`)
state.push(newFolder)
dataStore.setFolders(state)
activityRecord.emit('FOLDER_CREATE')
return state
}
case FOLDER_UPDATE:
{
let folder = action.data.folder
let targetFolder = _.findWhere(state, {key: folder.key})
if (!_.isString(folder.name)) throw new Error('Folder name must be a string')
folder.name = folder.name.trim().replace(/\s/, '_')
if (folder.name.length === 0) throw new Error('Folder name is required')
if (folder.name.match(/\//)) throw new Error('`/` is not available for folder name')
// Folder existence check
if (targetFolder == null) throw new Error('Folder doesnt exist')
// Name conflict check
if (targetFolder.name !== folder.name) {
let conflictFolder = _.find(state, _folder => {
return folder.name === _folder.name && folder.key !== _folder.key
})
if (conflictFolder != null) throw new Error('Name conflicted')
}
Object.assign(targetFolder, folder, {
updatedAt: new Date()
})
dataStore.setFolders(state)
activityRecord.emit('FOLDER_UPDATE')
return state
}
case FOLDER_DESTROY:
{
if (state.length < 2) throw new Error('Folder must exist more than one')
let targetKey = action.data.key
let targetIndex = _.findIndex(state, folder => folder.key === targetKey)
if (targetIndex >= 0) {
state.splice(targetIndex, 1)
}
dataStore.setFolders(state)
activityRecord.emit('FOLDER_DESTROY')
return state
}
case FOLDER_REPLACE:
{
let { a, b } = action.data
let folderA = state[a]
let folderB = state[b]
state.splice(a, 1, folderB)
state.splice(b, 1, folderA)
}
dataStore.setFolders(state)
return state
default:
return state
}
}
let isCleaned = true
function articles (state = initialArticles, action) {
state = state.slice()
if (!isCreatingNew && !isCleaned) {
state = state.filter(article => article.status !== 'NEW')
isCleaned = true
}
switch (action.type) {
case SWITCH_ARTICLE:
if (action.data.isNew) {
isCleaned = false
}
if (!isStatusLocked && !action.data.isNew) {
isCreatingNew = false
if (!isCleaned) {
state = state.filter(article => article.status !== 'NEW')
isCleaned = true
}
}
return state
case SWITCH_FOLDER:
case SET_SEARCH_FILTER:
case SET_TAG_FILTER:
case CLEAR_SEARCH:
if (!isStatusLocked) {
isCreatingNew = false
if (!isCleaned) {
state = state.filter(article => article.status !== 'NEW')
isCleaned = true
}
}
return state
case CLEAR_NEW_ARTICLE:
return state.filter(article => article.status !== 'NEW')
case ARTICLE_UPDATE:
{
let article = action.data.article
let targetIndex = _.findIndex(state, _article => article.key === _article.key)
if (targetIndex < 0) state.unshift(article)
else Object.assign(state[targetIndex], article)
if (article.status !== 'NEW') dataStore.setArticles(state)
else isCreatingNew = true
return state
}
case ARTICLE_DESTROY:
{
let articleKey = action.data.key
let targetIndex = _.findIndex(state, _article => articleKey === _article.key)
if (targetIndex >= 0) state.splice(targetIndex, 1)
dataStore.setArticles(state)
return state
}
case FOLDER_DESTROY:
{
let folderKey = action.data.key
state = state.filter(article => article.FolderKey !== folderKey)
dataStore.setArticles(state)
return state
}
default:
return state
}
}
function status (state = initialStatus, action) {
state = Object.assign({}, state)
switch (action.type) {
case TOGGLE_TUTORIAL:
state.isTutorialOpen = !state.isTutorialOpen
return state
case LOCK_STATUS:
isStatusLocked = state.isStatusLocked = true
return state
case UNLOCK_STATUS:
isStatusLocked = state.isStatusLocked = false
return state
}
// if status locked, status become unmutable
if (state.isStatusLocked) {
openModal(EditedAlert, {action})
return state
}
switch (action.type) {
case SWITCH_FOLDER:
state.mode = IDLE_MODE
state.search = `//${action.data} `
return state
case SWITCH_MODE:
state.mode = action.data
return state
case SWITCH_ARTICLE:
state.articleKey = action.data.key
state.mode = IDLE_MODE
return state
case SET_SEARCH_FILTER:
state.search = action.data
state.mode = IDLE_MODE
return state
case SET_TAG_FILTER:
state.search = `#${action.data}`
state.mode = IDLE_MODE
return state
case CLEAR_SEARCH:
state.search = ''
state.mode = IDLE_MODE
return state
default:
return state
}
}
export default combineReducers({
user,
folders,
articles,
status
})

6
browser/main/store.js Normal file
View File

@@ -0,0 +1,6 @@
import reducer from './reducer'
import { createStore } from 'redux'
let store = createStore(reducer)
export default store

View File

@@ -1,89 +0,0 @@
.LoginContainer, .SignupContainer
margin 0 auto
padding 105px 15px
box-sizing border-box
color inactiveTextColor
.logo
width 150px
height 150px
display block
margin 0 auto
.authNavigator
margin 15px 0 25px
a
font-size 1.5em
text-decoration none
color inactiveTextColor
&:hover, &.hover, &:active, &.active
color brandColor
.socialControl
text-align center
margin 25px 0
p
margin-bottom 25px
.facebookBtn, .githubBtn
margin 0 45px
width 50px
height 50px
line-height 50px
font-size 25px
text-align center
background-image none
color white
border none
border-radius 25px
cursor pointer
.facebookBtn
background-color facebookColor
&:hover, &.hover
background-color lighten(facebookColor, 25%)
.githubBtn
background-color githubBtn
font-size 30px
line-height 30px
&:hover, &.hover
background-color lighten(githubBtn, 25%)
.divider
.dividerLabel
text-align center
position relative
top -27px
font-size 1.3em
background-color backgroundColor
margin 0 auto
width 50px
form
width 400px
margin 0 auto 45px
.alertInfo, .alertError
margin-top 15px
margin-bottom 15px
padding 10px
border-radius 5px
line-height 1.6
text-align center
.alertInfo
alertInfo()
.alertError
alertError()
div.formField
input
stripInput()
height 33px
width 100%
margin-bottom 10px
text-align center
font-size 1.1em
&:last-child
margin-top 15px
button.logInButton
btnPrimary()
height 44px
border-radius 22px
display block
width 200px
font-size 1em
margin 0 auto
p.alert
text-align center
font-size 0.8em