1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 09:46:22 +00:00

prepare alpha.5 (remain work: MD preview, keybind)

This commit is contained in:
Rokt33r
2015-10-31 13:05:22 +09:00
parent d9442aa23c
commit 3d0b79f674
18 changed files with 427 additions and 403 deletions

View File

@@ -44,10 +44,8 @@ class HomePage extends React.Component {
} }
function remap (state) { function remap (state) {
let status = state.status let { folders, articles, status } = state
// Fetch articles
let data = JSON.parse(localStorage.getItem('local'))
let { folders, articles } = data
if (articles == null) articles = [] if (articles == null) articles = []
articles.sort((a, b) => { articles.sort((a, b) => {
return new Date(b.updatedAt) - new Date(a.updatedAt) return new Date(b.updatedAt) - new Date(a.updatedAt)
@@ -112,7 +110,13 @@ function remap (state) {
// 1. team have one folder at least // 1. team have one folder at least
// or Change IDLE MODE // or Change IDLE MODE
if (status.mode === CREATE_MODE) { if (status.mode === CREATE_MODE) {
var newArticle = _.findWhere(articles, {status: 'NEW'}) let newArticle = _.findWhere(articles, {status: 'NEW'})
let FolderKey = folders[0].key
if (folderFilters.length > 0) {
let targetFolder = _.findWhere(folders, {name: folderFilters[0].value})
if (targetFolder != null) FolderKey = targetFolder.key
}
if (newArticle == null) { if (newArticle == null) {
newArticle = { newArticle = {
id: null, id: null,
@@ -121,7 +125,7 @@ function remap (state) {
content: '', content: '',
mode: 'markdown', mode: 'markdown',
tags: [], tags: [],
FolderKey: folders[0].key, FolderKey: FolderKey,
status: NEW status: NEW
} }
articles.unshift(newArticle) articles.unshift(newArticle)
@@ -131,14 +135,12 @@ function remap (state) {
status.mode = IDLE_MODE status.mode = IDLE_MODE
} }
let props = { return {
folders, folders,
status, status,
articles, articles,
activeArticle activeArticle
} }
return props
} }
HomePage.propTypes = { HomePage.propTypes = {

View File

@@ -101,8 +101,8 @@ export default class ArticleDetail extends React.Component {
<div className='left'> <div className='left'>
<div className='info'> <div className='info'>
<FolderMark color={folder.color}/> {folder.name}&nbsp; <FolderMark color={folder.color}/> {folder.name}&nbsp;
Created {moment(activeArticle.createdAt).format('YYYY/MM/DD')}&nbsp; Created : {moment(activeArticle.createdAt).format('YYYY/MM/DD')}&nbsp;
Updated {moment(activeArticle.updatedAt).format('YYYY/MM/DD')} Updated : {moment(activeArticle.updatedAt).format('YYYY/MM/DD')}
</div> </div>
<div className='tags'><i className='fa fa-fw fa-tags'/>{tags}</div> <div className='tags'><i className='fa fa-fw fa-tags'/>{tags}</div>
</div> </div>
@@ -183,7 +183,6 @@ export default class ArticleDetail extends React.Component {
<option key={folder.key} value={folder.key}>{folder.name}</option> <option key={folder.key} value={folder.key}>{folder.name}</option>
) )
}) })
console.log('edit rendered')
return ( return (
<div className='ArticleDetail edit'> <div className='ArticleDetail edit'>

View File

@@ -1,5 +1,4 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
import ProfileImage from 'boost/components/ProfileImage'
import { findWhere } from 'lodash' import { findWhere } from 'lodash'
import { setSearchFilter, switchFolder, switchMode, CREATE_MODE } from 'boost/actions' import { setSearchFilter, switchFolder, switchMode, CREATE_MODE } from 'boost/actions'
import { openModal } from 'boost/modal' import { openModal } from 'boost/modal'
@@ -7,6 +6,8 @@ import FolderMark from 'boost/components/FolderMark'
import Preferences from 'boost/components/modal/Preferences' import Preferences from 'boost/components/modal/Preferences'
import CreateNewFolder from 'boost/components/modal/CreateNewFolder' import CreateNewFolder from 'boost/components/modal/CreateNewFolder'
import remote from 'remote'
export default class ArticleNavigator extends React.Component { export default class ArticleNavigator extends React.Component {
handlePreferencesButtonClick (e) { handlePreferencesButtonClick (e) {
openModal(Preferences) openModal(Preferences)
@@ -50,10 +51,12 @@ export default class ArticleNavigator extends React.Component {
) )
}) })
let userName = remote.getGlobal('process').env.USER
return ( return (
<div className='ArticleNavigator'> <div className='ArticleNavigator'>
<div className='userInfo'> <div className='userInfo'>
<div className='userProfileName'>{process.env.USER}</div> <div className='userProfileName'>{userName}</div>
<div className='userName'>local</div> <div className='userName'>local</div>
<button onClick={e => this.handlePreferencesButtonClick(e)} className='settingBtn'><i className='fa fa-fw fa-chevron-down'/></button> <button onClick={e => this.handlePreferencesButtonClick(e)} className='settingBtn'><i className='fa fa-fw fa-chevron-down'/></button>
</div> </div>

View File

@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ExternalLink from 'boost/components/ExternalLink' import ExternalLink from 'boost/components/ExternalLink'
import { setSearchFilter } from 'boost/actions' import { setSearchFilter } from 'boost/actions'
@@ -8,6 +9,12 @@ export default class ArticleTopBar extends React.Component {
dispatch(setSearchFilter(e.target.value)) dispatch(setSearchFilter(e.target.value))
} }
handleSearchClearButton (e) {
let { dispatch } = this.props
dispatch(setSearchFilter(''))
ReactDOM.findDOMNode(this.refs.searchInput).focus()
}
render () { render () {
return ( return (
@@ -15,7 +22,12 @@ export default class ArticleTopBar extends React.Component {
<div className='left'> <div className='left'>
<div className='search'> <div className='search'>
<i className='fa fa-search fa-fw' /> <i className='fa fa-search fa-fw' />
<input value={this.props.status.search} onChange={e => this.handleSearchChange(e)} placeholder='Search' type='text'/> <input ref='searchInput' value={this.props.status.search} onChange={e => this.handleSearchChange(e)} placeholder='Search' type='text'/>
{
this.props.status.search != null && this.props.status.search.length > 0
? <button onClick={e => this.handleSearchClearButton(e)} className='searchClearBtn'><i className='fa fa-times'/></button>
: null
}
</div> </div>
</div> </div>
<div className='right'> <div className='right'>

View File

@@ -6,15 +6,9 @@ import MainPage from './MainPage'
import HomePage from './HomePage' import HomePage from './HomePage'
// import auth from 'boost/auth' // import auth from 'boost/auth'
import store from 'boost/store' import store from 'boost/store'
let ReactDOM = require('react-dom') import ReactDOM from 'react-dom'
require('../styles/main/index.styl') require('../styles/main/index.styl')
function onlyUser (state, replaceState) {
// let currentUser = auth.user()
// if (currentUser == null) return replaceState('login', '/login')
// if (state.location.pathname === '/') return replaceState('user', '/users/' + currentUser.id)
}
let routes = ( let routes = (
<Route path='/' component={MainPage}> <Route path='/' component={MainPage}>
<IndexRoute name='home' component={HomePage}/> <IndexRoute name='home' component={HomePage}/>
@@ -22,7 +16,6 @@ let routes = (
) )
let el = document.getElementById('content') let el = document.getElementById('content')
ReactDOM.render(( ReactDOM.render((
<div> <div>
<Provider store={store}> <Provider store={store}>

View File

@@ -100,6 +100,7 @@ iptFocusBorderColor = #369DCD
color white color white
margin 0 2px margin 0 2px
padding 0 padding 0
border 1px solid darken(brandColor, 10%)
button.tagRemoveBtn button.tagRemoveBtn
color white color white
border-radius 2px border-radius 2px
@@ -117,13 +118,13 @@ iptFocusBorderColor = #369DCD
font-size 12px font-size 12px
line-height 12px line-height 12px
input.tagInput input.tagInput
background-color white background-color transparent
outline none outline none
margin 0 2px
border-radius 2px border-radius 2px
border 1px solid borderColor border none
transition 0.1s transition 0.1s
&:focus height 18px
border-color iptFocusBorderColor
.right .right
@@ -165,8 +166,7 @@ iptFocusBorderColor = #369DCD
border none border none
background-color transparent background-color transparent
line-height 60px line-height 60px
font-size 32px font-size 24px
font-weight bold
outline none outline none
&.idle &.idle
.detailInfo .detailInfo
@@ -217,9 +217,9 @@ iptFocusBorderColor = #369DCD
absolute top bottom absolute top bottom
left 45px left 45px
right 15px right 15px
font-size 32px font-size 24px
line-height 60px line-height 60px
font-weight bold
white-space nowrap white-space nowrap
overflow-x auto overflow-x auto
overflow-y hidden overflow-y hidden

View File

@@ -39,6 +39,7 @@ articleNavBgColor = #353535
.controlSection .controlSection
height 88px height 88px
padding 22px 15px padding 22px 15px
margin-bottom 44px
.newPostBtn .newPostBtn
border none border none
background-color brandColor background-color brandColor
@@ -80,6 +81,9 @@ articleNavBgColor = #353535
border-color brandColor border-color brandColor
.folders .folders
margin-bottom 15px margin-bottom 15px
.folderList
height 340px
overflow-y auto
.folderList button .folderList button
height 33px height 33px
width 199px width 199px

View File

@@ -2,8 +2,9 @@ bgColor = #E6E6E6
inputBgColor = white inputBgColor = white
iptFocusBorderColor = #369DCD iptFocusBorderColor = #369DCD
refreshBtColor = #B3B3B3 topBarBtnColor = #B3B3B3
refreshBtnActiveColor = #3A3A3A topBarBtnBgColor = #B3B3B3
topBarBtnBgActiveColor = #3A3A3A
infoBtnColor = bgColor infoBtnColor = bgColor
infoBtnBgColor = #B3B3B3 infoBtnBgColor = #B3B3B3
@@ -41,7 +42,7 @@ infoBtnActiveBgColor = #3A3A3A
z-index 0 z-index 0
&:focus &:focus
border-color iptFocusBorderColor border-color iptFocusBorderColor
i.fa i.fa.fa-search
position absolute position absolute
display block display block
top 0 top 0
@@ -49,6 +50,23 @@ infoBtnActiveBgColor = #3A3A3A
line-height 33px line-height 33px
z-index 1 z-index 1
pointer-events none pointer-events none
.searchClearBtn
position absolute
top 6px
right 10px
width 20px
height 20px
border-radius 10px
border none
background-color transparent
color topBarBtnColor
transition 0.1s
line-height 20px
text-align center
padding 0
&:hover
color white
background-color topBarBtnBgColor
&>.refreshBtn &>.refreshBtn
float left float left
width 33px width 33px

View File

@@ -2,6 +2,7 @@
export const ARTICLE_UPDATE = 'ARTICLE_UPDATE' export const ARTICLE_UPDATE = 'ARTICLE_UPDATE'
export const ARTICLE_DESTROY = 'ARTICLE_DESTROY' export const ARTICLE_DESTROY = 'ARTICLE_DESTROY'
export const FOLDER_CREATE = 'FOLDER_CREATE' export const FOLDER_CREATE = 'FOLDER_CREATE'
export const FOLDER_UPDATE = 'FOLDER_UPDATE'
export const FOLDER_DESTROY = 'FOLDER_DESTROY' export const FOLDER_DESTROY = 'FOLDER_DESTROY'
export const SWITCH_FOLDER = 'SWITCH_FOLDER' export const SWITCH_FOLDER = 'SWITCH_FOLDER'
@@ -28,10 +29,10 @@ export function updateArticle (article) {
} }
} }
export function destroyArticle (articleKey) { export function destroyArticle (key) {
return { return {
type: ARTICLE_DESTROY, type: ARTICLE_DESTROY,
data: { articleKey } data: { key }
} }
} }
@@ -42,6 +43,13 @@ export function createFolder (folder) {
} }
} }
export function updateFolder (folder) {
return {
type: FOLDER_UPDATE,
data: { folder }
}
}
export function destroyFolder (key) { export function destroyFolder (key) {
return { return {
type: FOLDER_DESTROY, type: FOLDER_DESTROY,

View File

@@ -8,6 +8,7 @@ const GREEN = '#02FF26'
const DARKGREEN = '#008A59' const DARKGREEN = '#008A59'
const RED = '#E10051' const RED = '#E10051'
const PURPLE = '#B013A4' const PURPLE = '#B013A4'
const BRAND_COLOR = '#2BAC8F'
function getColorByIndex (index) { function getColorByIndex (index) {
switch (index % 8) { switch (index % 8) {
@@ -28,7 +29,7 @@ function getColorByIndex (index) {
case 7: case 7:
return PURPLE return PURPLE
default: default:
return 'gray' return BRAND_COLOR
} }
} }

View File

@@ -62,7 +62,7 @@ export default class TagSelect extends React.Component {
onKeyDown={e => this.handleKeyDown(e)} onKeyDown={e => this.handleKeyDown(e)}
ref='tagInput' ref='tagInput'
valueLink={this.linkState('input')} valueLink={this.linkState('input')}
placeholder='new tag' placeholder='Click here to add tags'
className='tagInput'/> className='tagInput'/>
</div> </div>
) )

View File

@@ -1,6 +1,5 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
import linkState from 'boost/linkState' import linkState from 'boost/linkState'
import keygen from 'boost/keygen'
import { createFolder } from 'boost/actions' import { createFolder } from 'boost/actions'
import store from 'boost/store' import store from 'boost/store'
@@ -20,15 +19,9 @@ export default class CreateNewFolder extends React.Component {
handleConfirmButton (e) { handleConfirmButton (e) {
let { close } = this.props let { close } = this.props
let key = keygen()
let name = this.state.name let name = this.state.name
let input = { let input = {
name, name
key,
createAt: new Date(),
updatedAt: new Date(),
// random number (0-7)
color: Math.round(Math.random() * 7)
} }
store.dispatch(createFolder(input)) store.dispatch(createFolder(input))

View File

@@ -1,7 +1,8 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
import api from 'boost/api'
import linkState from 'boost/linkState' import linkState from 'boost/linkState'
import FolderMark from 'boost/components/FolderMark' import FolderMark from 'boost/components/FolderMark'
import store from 'boost/store'
import { updateFolder, destroyFolder } from 'boost/actions'
const IDLE = 'IDLE' const IDLE = 'IDLE'
const EDIT = 'EDIT' const EDIT = 'EDIT'
@@ -12,22 +13,21 @@ export default class FolderRow extends React.Component {
super(props) super(props)
this.state = { this.state = {
mode: IDLE, mode: IDLE
name: props.folder.name,
public: props.folder.public
} }
} }
handleCancelButtonClick (e) { handleCancelButtonClick (e) {
this.setState({ this.setState({
mode: IDLE, mode: IDLE
name: this.props.folder.name,
public: this.props.folder.public
}) })
} }
handleEditButtonClick (e) { handleEditButtonClick (e) {
this.setState({mode: EDIT}) this.setState({
mode: EDIT,
name: this.props.folder.name
})
} }
handleDeleteButtonClick (e) { handleDeleteButtonClick (e) {
@@ -39,31 +39,21 @@ export default class FolderRow extends React.Component {
} }
handleSaveButtonClick (e) { handleSaveButtonClick (e) {
let { folder } = this.props
let input = { let input = {
name: this.state.name, name: this.state.name
public: !!parseInt(this.state.public, 10)
} }
Object.assign(folder, input)
api.updateFolder(this.props.folder.id, input) store.dispatch(updateFolder(folder))
.then(res => { this.setState({
console.log(res.body) mode: IDLE
this.setState({mode: IDLE}) })
})
.catch(err => {
if (err.status != null) throw err
else console.error(err)
})
} }
handleDeleteConfirmButtonClick (e) { handleDeleteConfirmButtonClick (e) {
api.destroyFolder(this.props.folder.id) let { folder } = this.props
.then(res => { store.dispatch(destroyFolder(folder.key))
console.log(res.body)
})
.catch(err => {
if (err.status != null) throw err
else console.error(err)
})
} }
render () { render () {
@@ -76,12 +66,6 @@ export default class FolderRow extends React.Component {
<div className='folderName'> <div className='folderName'>
<input valueLink={this.linkState('name')} type='text'/> <input valueLink={this.linkState('name')} type='text'/>
</div> </div>
<div className='folderPublic'>
<select value={this.state.public} onChange={e => this.handleFolderPublicChange(e)}>
<option value='0'>Private</option>
<option value='1'>Public</option>
</select>
</div>
<div className='folderControl'> <div className='folderControl'>
<button onClick={e => this.handleSaveButtonClick(e)} className='primary'>Save</button> <button onClick={e => this.handleSaveButtonClick(e)} className='primary'>Save</button>
<button onClick={e => this.handleCancelButtonClick(e)}>Cancel</button> <button onClick={e => this.handleCancelButtonClick(e)}>Cancel</button>
@@ -102,8 +86,7 @@ export default class FolderRow extends React.Component {
default: default:
return ( return (
<div className='FolderRow'> <div className='FolderRow'>
<div className='folderName'><FolderMark id={folder.id}/> {folder.name}</div> <div className='folderName'><FolderMark color={folder.color}/> {folder.name}</div>
<div className='folderPublic'>{folder.public ? 'Public' : 'Private'}</div>
<div className='folderControl'> <div className='folderControl'>
<button onClick={e => this.handleEditButtonClick(e)}><i className='fa fa-fw fa-edit'/></button> <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> <button onClick={e => this.handleDeleteButtonClick(e)}><i className='fa fa-fw fa-close'/></button>

View File

@@ -1,87 +1,44 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
import _ from 'lodash'
import FolderRow from './FolderRow' import FolderRow from './FolderRow'
import linkState from 'boost/linkState' import linkState from 'boost/linkState'
import api from 'boost/api' import { createFolder } from 'boost/actions'
export default class FolderSettingTab extends React.Component { export default class FolderSettingTab extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
this.state = { this.state = {
name: '', name: ''
public: 0
} }
} }
getCurrentTeam (props) {
if (props == null) props = this.props
return _.findWhere(props.teams, {id: props.currentTeamId})
}
handleTeamSelectChange (e) {
this.props.switchTeam(e.target.value)
}
handleFolderPublicChange (e) {
this.setState({public: e.target.value})
}
handleSaveButtonClick (e) { handleSaveButtonClick (e) {
let team = this.getCurrentTeam() if (this.state.name.trim().length === 0) return false
let input = {
UserId: team.id,
name: this.state.name,
public: !!parseInt(this.state.public, 10)
}
api.createFolder(input) let { dispatch } = this.props
.then(res => {
console.log(res.body)
this.setState({
name: '',
public: 0
})
})
.catch(err => {
if (err.status != null) throw err
else console.error(err)
})
}
renderTeamOptions () { dispatch(createFolder({
return this.props.teams.map(team => { name: this.state.name
return ( }))
<option key={'team-' + team.id} value={team.id}>{team.name}</option>)
}) this.setState({name: ''})
} }
render () { render () {
let team = this.getCurrentTeam() let { folders } = this.props
console.log(team.Folders) let folderElements = folders.map(folder => {
let folderElements = team.Folders.map(folder => {
return ( return (
<FolderRow key={'folder-' + folder.id} folder={folder}/> <FolderRow key={'folder-' + folder.key} folder={folder}/>
) )
}) })
return ( return (
<div className='FolderSettingTab content'> <div className='FolderSettingTab 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='section'>
<div className='sectionTitle'>Folders</div> <div className='sectionTitle'>Manage folder</div>
<div className='folderTable'> <div className='folderTable'>
<div className='folderHeader'> <div className='folderHeader'>
<div className='folderName'>Folder name</div> <div className='folderName'>Folder name</div>
<div className='folderPublic'>Public/Private</div>
<div className='folderControl'>Edit/Delete</div> <div className='folderControl'>Edit/Delete</div>
</div> </div>
{folderElements} {folderElements}
@@ -89,12 +46,6 @@ export default class FolderSettingTab extends React.Component {
<div className='folderName'> <div className='folderName'>
<input valueLink={this.linkState('name')} type='text' placeholder='New Folder'/> <input valueLink={this.linkState('name')} type='text' placeholder='New Folder'/>
</div> </div>
<div className='folderPublic'>
<select value={this.state.public} onChange={e => this.handleFolderPublicChange(e)}>
<option value='0'>Private</option>
<option value='1'>Public</option>
</select>
</div>
<div className='folderControl'> <div className='folderControl'>
<button onClick={e => this.handleSaveButtonClick(e)} className='primary'>Add</button> <button onClick={e => this.handleSaveButtonClick(e)} className='primary'>Add</button>
</div> </div>
@@ -107,9 +58,8 @@ export default class FolderSettingTab extends React.Component {
} }
FolderSettingTab.propTypes = { FolderSettingTab.propTypes = {
currentTeamId: PropTypes.number, folders: PropTypes.array,
teams: PropTypes.array, dispatch: PropTypes.func
switchTeam: PropTypes.func
} }
FolderSettingTab.prototype.linkState = linkState FolderSettingTab.prototype.linkState = linkState

View File

@@ -1,22 +1,14 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
import { connect, Provider } from 'react-redux' import { connect, Provider } from 'react-redux'
import linkState from 'boost/linkState' import linkState from 'boost/linkState'
import api from 'boost/api'
import store from 'boost/store' import store from 'boost/store'
import AppSettingTab from './Preference/AppSettingTab' import AppSettingTab from './Preference/AppSettingTab'
import HelpTab from './Preference/HelpTab' import HelpTab from './Preference/HelpTab'
import TeamSettingTab from './Preference/TeamSettingTab'
import MemberSettingTab from './Preference/MemberSettingTab'
import FolderSettingTab from './Preference/FolderSettingTab' import FolderSettingTab from './Preference/FolderSettingTab'
import { closeModal } from 'boost/modal' import { closeModal } from 'boost/modal'
var { findDOMNode } = require('react-dom')
const PROFILE = 'PROFILE'
const APP = 'APP' const APP = 'APP'
const HELP = 'HELP' const HELP = 'HELP'
const TEAM = 'TEAM'
const MEMBER = 'MEMBER'
const FOLDER = 'FOLDER' const FOLDER = 'FOLDER'
class Preferences extends React.Component { class Preferences extends React.Component {
@@ -42,8 +34,8 @@ class Preferences extends React.Component {
let content = this.renderContent() let content = this.renderContent()
let tabs = [ let tabs = [
{target: APP, label: 'Preferences'} {target: APP, label: 'Preferences'},
// {target: FOLDER, label: 'Manage folder'} {target: FOLDER, label: 'Manage folder'}
] ]
let navButtons = tabs.map(tab => ( let navButtons = tabs.map(tab => (
@@ -67,181 +59,190 @@ class Preferences extends React.Component {
} }
renderContent () { renderContent () {
let { folders, dispatch } = this.props
switch (this.state.currentTab) { switch (this.state.currentTab) {
case HELP: case HELP:
return (<HelpTab/>) return (<HelpTab/>)
case FOLDER:
return (
<FolderSettingTab
dispatch={dispatch}
folders={folders}
/>
)
case APP: case APP:
default: default:
return (<AppSettingTab/>) return (<AppSettingTab/>)
} }
} }
handleProfileSaveButtonClick (e) { // handleProfileSaveButtonClick (e) {
let profileState = this.state.profile // let profileState = this.state.profile
profileState.userInfo.alert = { // profileState.userInfo.alert = {
type: 'info', // type: 'info',
message: 'Sending...' // message: 'Sending...'
} // }
this.setState({profile: profileState}, () => { // this.setState({profile: profileState}, () => {
let input = { // let input = {
profileName: profileState.userInfo.profileName, // profileName: profileState.userInfo.profileName,
email: profileState.userInfo.email // email: profileState.userInfo.email
} // }
api.updateUserInfo(input) // api.updateUserInfo(input)
.then(res => { // .then(res => {
let profileState = this.state.profile // let profileState = this.state.profile
profileState.userInfo.alert = { // profileState.userInfo.alert = {
type: 'success', // type: 'success',
message: 'Successfully done!' // message: 'Successfully done!'
} // }
this.setState({profile: profileState}) // this.setState({profile: profileState})
}) // })
.catch(err => { // .catch(err => {
var message // var message
if (err.status != null) { // if (err.status != null) {
message = err.response.body.message // message = err.response.body.message
} else if (err.code === 'ECONNREFUSED') { // } else if (err.code === 'ECONNREFUSED') {
message = 'Can\'t connect to API server.' // message = 'Can\'t connect to API server.'
} else throw err // } else throw err
let profileState = this.state.profile // let profileState = this.state.profile
profileState.userInfo.alert = { // profileState.userInfo.alert = {
type: 'error', // type: 'error',
message: message // message: message
} // }
this.setState({profile: profileState}) // this.setState({profile: profileState})
}) // })
}) // })
} // }
handlePasswordSaveButton (e) { // handlePasswordSaveButton (e) {
let profileState = this.state.profile // let profileState = this.state.profile
if (profileState.password.newPassword !== profileState.password.confirmation) { // if (profileState.password.newPassword !== profileState.password.confirmation) {
profileState.password.alert = { // profileState.password.alert = {
type: 'error', // type: 'error',
message: 'Confirmation doesn\'t match' // message: 'Confirmation doesn\'t match'
} // }
this.setState({profile: profileState}) // this.setState({profile: profileState})
return // return
} // }
profileState.password.alert = { // profileState.password.alert = {
type: 'info', // type: 'info',
message: 'Sending...' // message: 'Sending...'
} // }
this.setState({profile: profileState}, () => { // this.setState({profile: profileState}, () => {
let input = { // let input = {
password: profileState.password.currentPassword, // password: profileState.password.currentPassword,
newPassword: profileState.password.newPassword // newPassword: profileState.password.newPassword
} // }
api.updatePassword(input) // api.updatePassword(input)
.then(res => { // .then(res => {
let profileState = this.state.profile // let profileState = this.state.profile
profileState.password.alert = { // profileState.password.alert = {
type: 'success', // type: 'success',
message: 'Successfully done!' // message: 'Successfully done!'
} // }
profileState.password.currentPassword = '' // profileState.password.currentPassword = ''
profileState.password.newPassword = '' // profileState.password.newPassword = ''
profileState.password.confirmation = '' // profileState.password.confirmation = ''
this.setState({profile: profileState}) // this.setState({profile: profileState})
}) // })
.catch(err => { // .catch(err => {
var message // var message
if (err.status != null) { // if (err.status != null) {
message = err.response.body.message // message = err.response.body.message
} else if (err.code === 'ECONNREFUSED') { // } else if (err.code === 'ECONNREFUSED') {
message = 'Can\'t connect to API server.' // message = 'Can\'t connect to API server.'
} else throw err // } else throw err
let profileState = this.state.profile // let profileState = this.state.profile
profileState.password.alert = { // profileState.password.alert = {
type: 'error', // type: 'error',
message: message // message: message
} // }
profileState.password.currentPassword = '' // profileState.password.currentPassword = ''
profileState.password.newPassword = '' // profileState.password.newPassword = ''
profileState.password.confirmation = '' // profileState.password.confirmation = ''
this.setState({profile: profileState}, () => { // this.setState({profile: profileState}, () => {
if (this.refs.currentPassword != null) findDOMNode(this.refs.currentPassword).focus() // if (this.refs.currentPassword != null) findDOMNode(this.refs.currentPassword).focus()
}) // })
}) // })
}) // })
} // }
renderProfile () { // renderProfile () {
let profileState = this.state.profile // let profileState = this.state.profile
return ( // return (
<div className='content profile'> // <div className='content profile'>
<div className='section userSection'> // <div className='section userSection'>
<div className='sectionTitle'>User Info</div> // <div className='sectionTitle'>User Info</div>
<div className='sectionInput'> // <div className='sectionInput'>
<label>Profile Name</label> // <label>Profile Name</label>
<input valueLink={this.linkState('profile.userInfo.profileName')} type='text'/> // <input valueLink={this.linkState('profile.userInfo.profileName')} type='text'/>
</div> // </div>
<div className='sectionInput'> // <div className='sectionInput'>
<label>E-mail</label> // <label>E-mail</label>
<input valueLink={this.linkState('profile.userInfo.email')} type='text'/> // <input valueLink={this.linkState('profile.userInfo.email')} type='text'/>
</div> // </div>
<div className='sectionConfirm'> // <div className='sectionConfirm'>
<button onClick={e => this.handleProfileSaveButtonClick(e)}>Save</button> // <button onClick={e => this.handleProfileSaveButtonClick(e)}>Save</button>
{this.state.profile.userInfo.alert != null // {this.state.profile.userInfo.alert != null
? ( // ? (
<div className={'alert ' + profileState.userInfo.alert.type}>{profileState.userInfo.alert.message}</div> // <div className={'alert ' + profileState.userInfo.alert.type}>{profileState.userInfo.alert.message}</div>
) // )
: null} // : null}
</div> // </div>
</div> // </div>
<div className='section passwordSection'> // <div className='section passwordSection'>
<div className='sectionTitle'>Password</div> // <div className='sectionTitle'>Password</div>
<div className='sectionInput'> // <div className='sectionInput'>
<label>Current Password</label> // <label>Current Password</label>
<input ref='currentPassword' valueLink={this.linkState('profile.password.currentPassword')} type='password' placeholder='Current Password'/> // <input ref='currentPassword' valueLink={this.linkState('profile.password.currentPassword')} type='password' placeholder='Current Password'/>
</div> // </div>
<div className='sectionInput'> // <div className='sectionInput'>
<label>New Password</label> // <label>New Password</label>
<input valueLink={this.linkState('profile.password.newPassword')} type='password' placeholder='New Password'/> // <input valueLink={this.linkState('profile.password.newPassword')} type='password' placeholder='New Password'/>
</div> // </div>
<div className='sectionInput'> // <div className='sectionInput'>
<label>Confirmation</label> // <label>Confirmation</label>
<input valueLink={this.linkState('profile.password.confirmation')} type='password' placeholder='Confirmation'/> // <input valueLink={this.linkState('profile.password.confirmation')} type='password' placeholder='Confirmation'/>
</div> // </div>
<div className='sectionConfirm'> // <div className='sectionConfirm'>
<button onClick={e => this.handlePasswordSaveButton(e)}>Save</button> // <button onClick={e => this.handlePasswordSaveButton(e)}>Save</button>
{profileState.password.alert != null // {profileState.password.alert != null
? ( // ? (
<div className={'alert ' + profileState.password.alert.type}>{profileState.password.alert.message}</div> // <div className={'alert ' + profileState.password.alert.type}>{profileState.password.alert.message}</div>
) // )
: null} // : null}
</div> // </div>
</div> // </div>
</div> // </div>
) // )
} // }
} }
Preferences.propTypes = { Preferences.propTypes = {
currentUser: PropTypes.shape(), folders: PropTypes.array,
close: PropTypes.func dispatch: PropTypes.func
} }
Preferences.prototype.linkState = linkState Preferences.prototype.linkState = linkState
function remap (state) { function remap (state) {
let currentUser = state.currentUser let { folders, status } = state
let status = state.status console.log(state)
return { return {
currentUser, folders,
status status
} }
} }

64
lib/dataStore.js Normal file
View File

@@ -0,0 +1,64 @@
import keygen from 'boost/keygen'
let defaultContent = '**Boost**は全く新しいエンジニアライクのノートアプリです。\n\n# ◎特徴\nBoostはエンジニアの仕事を圧倒的に効率化するいくつかの機能を備えています。\nその一部をご紹介します。\n1. Folderで情報を分類\n2. 豊富なsyantaxに対応\n3. Finder機能\n4. チーム機能(リアルタイム搭載)\n\n   \n\n# 1. Folderで情報を分類、欲しい情報にすぐアクセス。\n左側のバーに存在する「Folders」。\n今すぐプラスボタンを押しましょう。\n分類の仕方も自由自在です。\n- 言語やフレームワークごとにFolderを作成\n- 自分用のカジュアルなメモをまとめる場としてFolderを作成\n\n\n# 2. 豊富なsyantaxに対応、自分の脳の代わりに。\nプログラミングに関する情報を全て、手軽に保存しましょう。\n- mdで、apiの仕様をまとめる\n- よく使うモジュールやスニペット\n\nBoostに保存しておくことで、何度も同じコードを書いたり調べたりする必要がなくなります。\n\n# 3. Finder機能を搭載、もうコマンドを手打ちする必要はありません。\n**「shift+cmd+tab」** を同時に押してみてください。\nここでは、一瞬でBoostの中身を検索するウィンドウを表示させることができます。\n\n矢印キーで選択、Enterを押し、cmd+vでペーストすると…続きはご自身の目でお確かめください。\n- sqlやlinux等の、よく使うが手打ちが面倒なコマンド\n- (メールやカスタマーサポート等でよく使うフレーズ)\n\n私たちは、圧倒的な効率性を支援します。\n\n# 4. チーム機能を搭載、シームレスな情報共有の場を実現。\n開発の設計思想やmdファイルの共有等、チームによって用途は様々ですが、Boostは多くの情報共有の課題について解決策を投げかけます。\n魅力を感じたら、左下のプラスボタンを今すぐクリック。\n\n\n   \n\n\n## ◎詳しくは\nこちらのブログ( http://blog-jp.b00st.io )にて随時更新しています。\n\nそれでは素晴らしいエンジニアライフを\n\n## Hack your memory'
export function init () {
console.log('initialize data store')
let data = JSON.parse(localStorage.getItem('local'))
if (data == null) {
let defaultFolder = {
name: 'default',
key: keygen()
}
let defaultArticle = {
title: 'Boostとは',
tags: ['boost', 'intro'],
content: defaultContent,
mode: 'markdown',
key: keygen(),
FolderKey: defaultFolder.key
}
data = {
articles: [defaultArticle],
folders: [defaultFolder],
version: '0.4'
}
localStorage.setItem('local', JSON.stringify(data))
}
}
function getKey (teamId) {
return teamId == null
? 'local'
: `team-${teamId}`
}
export function getData (teamId) {
let key = getKey(teamId)
return JSON.parse(localStorage.getItem(key))
}
export function setArticles (teamId, articles) {
let key = getKey(teamId)
let data = JSON.parse(localStorage.getItem(key))
data.articles = articles
localStorage.setItem(key, JSON.stringify(data))
}
export function setFolders (teamId, folders) {
let key = getKey(teamId)
let data = JSON.parse(localStorage.getItem(key))
data.folders = folders
localStorage.setItem(key, JSON.stringify(data))
}
export default (function () {
init()
return {
init,
getData,
setArticles,
setFolders
}
})()

View File

@@ -1,56 +1,114 @@
import { combineReducers } from 'redux' import { combineReducers } from 'redux'
import _ from 'lodash' import _ from 'lodash'
import { SWITCH_FOLDER, SWITCH_MODE, SWITCH_ARTICLE, SET_SEARCH_FILTER, SET_TAG_FILTER, USER_UPDATE, ARTICLE_UPDATE, ARTICLE_DESTROY, FOLDER_CREATE, FOLDER_DESTROY, IDLE_MODE, CREATE_MODE } from './actions' import { SWITCH_FOLDER, SWITCH_MODE, SWITCH_ARTICLE, SET_SEARCH_FILTER, SET_TAG_FILTER, ARTICLE_UPDATE, ARTICLE_DESTROY, FOLDER_CREATE, FOLDER_UPDATE, FOLDER_DESTROY, IDLE_MODE, CREATE_MODE } from './actions'
import auth from 'boost/auth' import dataStore from 'boost/dataStore'
import keygen from 'boost/keygen' import keygen from 'boost/keygen'
let defaultContent = '**Boost**は全く新しいエンジニアライクのノートアプリです。\n\n# ◎特徴\nBoostはエンジニアの仕事を圧倒的に効率化するいくつかの機能を備えています。\nその一部をご紹介します。\n1. Folderで情報を分類\n2. 豊富なsyantaxに対応\n3. Finder機能\n4. チーム機能(リアルタイム搭載)\n\n   \n\n# 1. Folderで情報を分類、欲しい情報にすぐアクセス。\n左側のバーに存在する「Folders」。\n今すぐプラスボタンを押しましょう。\n分類の仕方も自由自在です。\n- 言語やフレームワークごとにFolderを作成\n- 自分用のカジュアルなメモをまとめる場としてFolderを作成\n\n\n# 2. 豊富なsyantaxに対応、自分の脳の代わりに。\nプログラミングに関する情報を全て、手軽に保存しましょう。\n- mdで、apiの仕様をまとめる\n- よく使うモジュールやスニペット\n\nBoostに保存しておくことで、何度も同じコードを書いたり調べたりする必要がなくなります。\n\n# 3. Finder機能を搭載、もうコマンドを手打ちする必要はありません。\n**「shift+cmd+tab」** を同時に押してみてください。\nここでは、一瞬でBoostの中身を検索するウィンドウを表示させることができます。\n\n矢印キーで選択、Enterを押し、cmd+vでペーストすると…続きはご自身の目でお確かめください。\n- sqlやlinux等の、よく使うが手打ちが面倒なコマンド\n- (メールやカスタマーサポート等でよく使うフレーズ)\n\n私たちは、圧倒的な効率性を支援します。\n\n# 4. チーム機能を搭載、シームレスな情報共有の場を実現。\n開発の設計思想やmdファイルの共有等、チームによって用途は様々ですが、Boostは多くの情報共有の課題について解決策を投げかけます。\n魅力を感じたら、左下のプラスボタンを今すぐクリック。\n\n\n   \n\n\n## ◎詳しくは\nこちらのブログ( http://blog-jp.b00st.io )にて随時更新しています。\n\nそれでは素晴らしいエンジニアライフを\n\n## Hack your memory'
const initialStatus = { const initialStatus = {
mode: IDLE_MODE, mode: IDLE_MODE,
search: '' search: ''
} }
function getInitialArticles () { let data = dataStore.getData()
let data = JSON.parse(localStorage.getItem('local')) let initialArticles = data.articles
if (data == null) { let initialFolders = data.folders
let defaultFolder = {
name: 'default',
key: keygen()
}
let defaultArticle = {
title: 'Boostとは',
tags: ['boost', 'intro'],
content: defaultContent,
mode: 'markdown',
key: keygen(),
FolderKey: defaultFolder.key
}
data = { function folders (state = initialFolders, action) {
articles: [defaultArticle], state = state.slice()
folders: [defaultFolder],
version: require('remote').require('app').getVersion()
}
localStorage.setItem('local', JSON.stringify(data))
}
return data.articles
}
function currentUser (state, action) {
switch (action.type) { switch (action.type) {
case USER_UPDATE: case FOLDER_CREATE:
let user = action.data.user {
let newFolder = action.data.folder
Object.assign(newFolder, {
key: keygen(),
createAt: new Date(),
updatedAt: new Date(),
// random number (0-7)
color: Math.round(Math.random() * 7)
})
return auth.user(user) let conflictFolder = _.findWhere(state, {name: newFolder.name})
if (conflictFolder != null) throw new Error('name conflicted!')
state.push(newFolder)
dataStore.setFolders(null, state)
return state
}
case FOLDER_UPDATE:
{
let folder = action.data.folder
let targetFolder = _.findWhere(state, {key: folder.key})
// Folder existence check
if (targetFolder == null) throw new Error('Folder doesnt exist')
// Name conflict check
if (targetFolder.name !== folder.name) {
let conflictFolder = _.findWhere(state, {name: folder.name})
if (conflictFolder != null) throw new Error('Name conflicted')
}
Object.assign(targetFolder, folder, {
updatedAt: new Date()
})
dataStore.setFolders(null, state)
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(null, state)
return state
}
default: default:
if (state == null) return auth.user()
return state return state
} }
} }
function status (state, action) { function articles (state = initialArticles, action) {
state = state.slice()
switch (action.type) {
case ARTICLE_UPDATE:
{
let article = action.data.article
let targetIndex = _.findIndex(state, _article => article.key === _article.key)
if (targetIndex < 0) state.unshift(article)
else state.splice(targetIndex, 1, article)
dataStore.setArticles(null, state)
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(null, state)
return state
}
case FOLDER_DESTROY:
{
let folderKey = action.data.key
state = state.filter(article => article.FolderKey !== folderKey)
dataStore.setArticles(null, state)
return state
}
default:
return state
}
}
function status (state = initialStatus, action) {
switch (action.type) { switch (action.type) {
case SWITCH_FOLDER: case SWITCH_FOLDER:
state.mode = IDLE_MODE state.mode = IDLE_MODE
@@ -78,77 +136,12 @@ function status (state, action) {
return state return state
default: default:
if (state == null) return initialStatus
return state
}
}
function articles (state, action) {
switch (action.type) {
case ARTICLE_UPDATE:
{
console.log(action)
let data = JSON.parse(localStorage.getItem('local'))
let { articles } = data
let article = action.data.article
let targetIndex = _.findIndex(articles, _article => article.key === _article.key)
if (targetIndex < 0) articles.unshift(article)
else articles.splice(targetIndex, 1, article)
localStorage.setItem('local', JSON.stringify(data))
state.articles = articles
}
return state
case ARTICLE_DESTROY:
{
let data = JSON.parse(localStorage.getItem('local'))
let { articles } = data
let articleKey = action.data.articleKey
let targetIndex = _.findIndex(articles, _article => articleKey === _article.key)
if (targetIndex >= 0) articles.splice(targetIndex, 1)
state.articles = articles
localStorage.setItem('local', JSON.stringify(data))
}
return state
case FOLDER_CREATE:
{
let data = JSON.parse(localStorage.getItem('local'))
let { folders } = data
let newFolder = action.data.folder
let conflictFolder = _.findWhere(folders, {name: newFolder.name})
if (conflictFolder != null) throw new Error('name conflicted!')
folders.push(newFolder)
localStorage.setItem('local', JSON.stringify(data))
state.folders = folders
}
return state
case FOLDER_DESTROY:
{
let data = JSON.parse(localStorage.getItem('local'))
let { folderKey } = action.data
let articles = data.articles
articles = articles.filter(article => article.FolderKey !== folderKey)
let folders = data.folders
folders = folders.filter(folder => folder.key !== folderKey)
localStorage.setItem('local', JSON.stringify(data))
state.folders = folders
state.articles = articles
}
return state
default:
if (state == null) return getInitialArticles()
return state return state
} }
} }
export default combineReducers({ export default combineReducers({
currentUser, folders,
status, articles,
articles status
}) })

View File

@@ -58,7 +58,7 @@
"nib": "^1.1.0", "nib": "^1.1.0",
"react": "^0.14.0", "react": "^0.14.0",
"react-dom": "^0.14.0", "react-dom": "^0.14.0",
"react-redux": "^3.1.0", "react-redux": "^4.0.0",
"react-router": "^1.0.0-rc1", "react-router": "^1.0.0-rc1",
"react-select": "^0.8.1", "react-select": "^0.8.1",
"react-transform-catch-errors": "^1.0.0", "react-transform-catch-errors": "^1.0.0",