mirror of
https://github.com/BoostIo/Boostnote
synced 2026-01-01 19:19:19 +00:00
refactor file structure
This commit is contained in:
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
168
browser/main/actions.js
Normal 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 |
@@ -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>
|
||||
108
browser/main/modal/CreateNewFolder.js
Normal file
108
browser/main/modal/CreateNewFolder.js
Normal 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
|
||||
255
browser/main/modal/CreateNewTeam.js
Normal file
255
browser/main/modal/CreateNewTeam.js
Normal 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
|
||||
41
browser/main/modal/EditedAlert.js
Normal file
41
browser/main/modal/EditedAlert.js
Normal 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
|
||||
}
|
||||
146
browser/main/modal/Preference/AppSettingTab.js
Normal file
146
browser/main/modal/Preference/AppSettingTab.js
Normal 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
|
||||
}
|
||||
123
browser/main/modal/Preference/ContactTab.js
Normal file
123
browser/main/modal/Preference/ContactTab.js
Normal 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
|
||||
187
browser/main/modal/Preference/FolderRow.js
Normal file
187
browser/main/modal/Preference/FolderRow.js
Normal 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
|
||||
99
browser/main/modal/Preference/FolderSettingTab.js
Normal file
99
browser/main/modal/Preference/FolderSettingTab.js
Normal 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
|
||||
11
browser/main/modal/Preference/HelpTab.js
Normal file
11
browser/main/modal/Preference/HelpTab.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
106
browser/main/modal/Preference/MemberRow.js
Normal file
106
browser/main/modal/Preference/MemberRow.js
Normal 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
|
||||
})
|
||||
}
|
||||
149
browser/main/modal/Preference/MemberSettingTab.js
Normal file
149
browser/main/modal/Preference/MemberSettingTab.js
Normal 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
|
||||
}
|
||||
171
browser/main/modal/Preference/TeamSettingTab.js
Normal file
171
browser/main/modal/Preference/TeamSettingTab.js
Normal 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
|
||||
122
browser/main/modal/Preferences.js
Normal file
122
browser/main/modal/Preferences.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
115
browser/main/modal/Tutorial.js
Normal file
115
browser/main/modal/Tutorial.js
Normal 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
273
browser/main/reducer.js
Normal 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
6
browser/main/store.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import reducer from './reducer'
|
||||
import { createStore } from 'redux'
|
||||
|
||||
let store = createStore(reducer)
|
||||
|
||||
export default store
|
||||
Reference in New Issue
Block a user