mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 10:16:26 +00:00
Compare commits
382 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86cc9fb7d8 | ||
|
|
f98efe0b97 | ||
|
|
6c5345df64 | ||
|
|
2d97661d28 | ||
|
|
3efe0c1ce2 | ||
|
|
6099c91216 | ||
|
|
934e4d9607 | ||
|
|
1198f6112e | ||
|
|
777f7f9305 | ||
|
|
43fb37ab1d | ||
|
|
06734ec886 | ||
|
|
0b8ae93727 | ||
|
|
24cea97e08 | ||
|
|
18536e5db0 | ||
|
|
8c236cb5cb | ||
|
|
797c8ad7fa | ||
|
|
ba8d2dcb8b | ||
|
|
78a095d958 | ||
|
|
627172f6df | ||
|
|
a39f25961c | ||
|
|
e738ae5c8c | ||
|
|
749e85e8e6 | ||
|
|
cfa251b158 | ||
|
|
1ca38b8741 | ||
|
|
d7bc5a7088 | ||
|
|
3b9a2c3ee1 | ||
|
|
260ad77d39 | ||
|
|
5c508a0cd9 | ||
|
|
fd3d1607a4 | ||
|
|
abbc0fbcf1 | ||
|
|
00be41608d | ||
|
|
499c3f2e13 | ||
|
|
627845f6e4 | ||
|
|
07eea76057 | ||
|
|
fecc4e9b79 | ||
|
|
58e9302f15 | ||
|
|
86f649fab1 | ||
|
|
9ac0becfb2 | ||
|
|
eda547b868 | ||
|
|
f75e872415 | ||
|
|
aef0712165 | ||
|
|
bed4b7fd27 | ||
|
|
b53ff5daf3 | ||
|
|
bb0872b4fc | ||
|
|
b65101f4be | ||
|
|
593d242a4c | ||
|
|
db7f339c34 | ||
|
|
9f3575a874 | ||
|
|
1c9c59c512 | ||
|
|
127202b831 | ||
|
|
4f8a04ed21 | ||
|
|
63b2e0560b | ||
|
|
07291d71f2 | ||
|
|
d1ca1ec4d9 | ||
|
|
6907cf9972 | ||
|
|
d4f8d1498d | ||
|
|
0952e4a664 | ||
|
|
6a4e8c95ea | ||
|
|
983bfb7adf | ||
|
|
d7aaf5e210 | ||
|
|
50281132ad | ||
|
|
6a2b22015e | ||
|
|
2f90890f50 | ||
|
|
0fe83a0583 | ||
|
|
ce74e69480 | ||
|
|
ddea2aeb22 | ||
|
|
7bbe69cce9 | ||
|
|
e921e30d64 | ||
|
|
cd4f9d8bb4 | ||
|
|
a0553788b6 | ||
|
|
1a183d78af | ||
|
|
cabcaa892c | ||
|
|
01c9d62a2b | ||
|
|
ba76df863c | ||
|
|
81441a0895 | ||
|
|
da0222f213 | ||
|
|
fb8a2eb2e0 | ||
|
|
cde2e27e04 | ||
|
|
3758ea2cf4 | ||
|
|
e62fc11328 | ||
|
|
3cbfae83c1 | ||
|
|
57667654ef | ||
|
|
eadd66fa91 | ||
|
|
75cd94a39a | ||
|
|
7872bfe19d | ||
|
|
af008e69c2 | ||
|
|
a549abc20f | ||
|
|
116344737a | ||
|
|
93c03f4e88 | ||
|
|
445332c27c | ||
|
|
c42e1892d0 | ||
|
|
b6b526dd57 | ||
|
|
3ef7f19ffc | ||
|
|
9d0d851c2e | ||
|
|
adb35b5bef | ||
|
|
acead09377 | ||
|
|
714cf43f6a | ||
|
|
5df0755252 | ||
|
|
c14827b234 | ||
|
|
ff9ef2af41 | ||
|
|
91ef5edcc3 | ||
|
|
27302c6fcc | ||
|
|
4d975da176 | ||
|
|
5b58d8a1e8 | ||
|
|
3105958afb | ||
|
|
a505227d01 | ||
|
|
673503b76f | ||
|
|
384682421d | ||
|
|
2ddd6e6321 | ||
|
|
86739aa1ac | ||
|
|
45a46cbc7a | ||
|
|
567f453232 | ||
|
|
890f654971 | ||
|
|
572a0ac266 | ||
|
|
d26ffdbe1a | ||
|
|
0bfc9236ed | ||
|
|
32e6394b3f | ||
|
|
09735b7f47 | ||
|
|
ee280d5c7b | ||
|
|
c1b56e4cb6 | ||
|
|
6698d15f20 | ||
|
|
ef35fd02e5 | ||
|
|
8e70e20f9e | ||
|
|
9632bf5b93 | ||
|
|
dde0cab04b | ||
|
|
c8337c7287 | ||
|
|
15560a3bce | ||
|
|
2e3a60cf6e | ||
|
|
08b0c43382 | ||
|
|
4e0e11a611 | ||
|
|
ef41dfca4c | ||
|
|
cfbca4b0fd | ||
|
|
fdea9a68a1 | ||
|
|
47169e19aa | ||
|
|
0b03c8360b | ||
|
|
62f8af1455 | ||
|
|
0934d452bb | ||
|
|
f2f31790b4 | ||
|
|
cf6ecc17cc | ||
|
|
931f9bdce0 | ||
|
|
bec0528a3a | ||
|
|
670f2b1fc3 | ||
|
|
f2f6de717b | ||
|
|
f8ad2eddf3 | ||
|
|
c36a46cad6 | ||
|
|
00360c77d2 | ||
|
|
8a62cd386e | ||
|
|
450327f093 | ||
|
|
e87ec04058 | ||
|
|
f9d41de8f1 | ||
|
|
f80a1a5f6b | ||
|
|
f81caf962d | ||
|
|
d18fcf0a18 | ||
|
|
0187217c86 | ||
|
|
b820bdec09 | ||
|
|
adace2954e | ||
|
|
6eeb8eeba6 | ||
|
|
dd2a8202ef | ||
|
|
d1cfd627bc | ||
|
|
fb97b7443d | ||
|
|
48fcd45d7d | ||
|
|
5cfc418d77 | ||
|
|
f3fbe38247 | ||
|
|
a0a1c84db1 | ||
|
|
54d563f49e | ||
|
|
e8ee8b8a16 | ||
|
|
c6ac44ba14 | ||
|
|
e4d8438801 | ||
|
|
f9539ab50a | ||
|
|
59f83c2432 | ||
|
|
cd789136c0 | ||
|
|
54b5bc441e | ||
|
|
2537b6ba09 | ||
|
|
013a1b4f51 | ||
|
|
d2377bd7c3 | ||
|
|
c17314125e | ||
|
|
09a59480f3 | ||
|
|
63cc2ce70a | ||
|
|
4642e050ba | ||
|
|
27a442ed2e | ||
|
|
325ae00eeb | ||
|
|
152e4129b2 | ||
|
|
2ddcf84625 | ||
|
|
13314700cd | ||
|
|
a7a499a2b1 | ||
|
|
b646313b58 | ||
|
|
f3ce4ca803 | ||
|
|
93d99c0c47 | ||
|
|
ae1fc7572a | ||
|
|
1a527cca10 | ||
|
|
c625513924 | ||
|
|
3f58302a14 | ||
|
|
63b199c9c2 | ||
|
|
fc64c565db | ||
|
|
91e60fa82b | ||
|
|
0cc52c2206 | ||
|
|
2ffe4ba70b | ||
|
|
2afd7e3687 | ||
|
|
a0f8d13c4f | ||
|
|
2571ea021a | ||
|
|
6950e05b6a | ||
|
|
7eb767a268 | ||
|
|
8e64abc4bc | ||
|
|
52df793a74 | ||
|
|
8e44a421a2 | ||
|
|
7f4ccdcac8 | ||
|
|
03e8de2f62 | ||
|
|
8b04eecc90 | ||
|
|
16bcd86792 | ||
|
|
be3c519a57 | ||
|
|
8776cb1cea | ||
|
|
4c94503f9a | ||
|
|
48f57376d3 | ||
|
|
958469f526 | ||
|
|
2a774a7bb6 | ||
|
|
a872ad9d8b | ||
|
|
2499a05473 | ||
|
|
6b66893ea4 | ||
|
|
529c27aed5 | ||
|
|
70fc0afbc4 | ||
|
|
09f81fd0d6 | ||
|
|
af7f2d4d5e | ||
|
|
3bd5d6b9f6 | ||
|
|
57912b5a5a | ||
|
|
a05f5b9737 | ||
|
|
1963b586ac | ||
|
|
3b9ad59849 | ||
|
|
79e0e5668d | ||
|
|
0e8edf0c72 | ||
|
|
24e2544544 | ||
|
|
f3732c76ea | ||
|
|
a4c72a9a86 | ||
|
|
455610e586 | ||
|
|
634d58b3ca | ||
|
|
27bbd77e8c | ||
|
|
d8ae77ded7 | ||
|
|
0648c04728 | ||
|
|
57c26e3b4a | ||
|
|
b03afff994 | ||
|
|
77f9e60177 | ||
|
|
35bb792496 | ||
|
|
8a87304800 | ||
|
|
64bbe053f8 | ||
|
|
d3f420bf6d | ||
|
|
7fcaaa297a | ||
|
|
7c2d2044a9 | ||
|
|
aa32f59dc6 | ||
|
|
182af99e7c | ||
|
|
5b520a7a81 | ||
|
|
364917c910 | ||
|
|
ca7b9c786a | ||
|
|
15c2363098 | ||
|
|
1a11095121 | ||
|
|
2b384b1d15 | ||
|
|
a1d61edb9c | ||
|
|
96a8687896 | ||
|
|
0448773682 | ||
|
|
57998ba727 | ||
|
|
de83447cb3 | ||
|
|
eba19468d5 | ||
|
|
65c78df671 | ||
|
|
a7096aa89f | ||
|
|
15a50ef452 | ||
|
|
04036e5c87 | ||
|
|
2bbb5ef74e | ||
|
|
91eb7feb3c | ||
|
|
978d77142c | ||
|
|
e36478b9ac | ||
|
|
e1fe4dd693 | ||
|
|
b1ee949b1c | ||
|
|
a0e5f8e97e | ||
|
|
e9cfb2c4ee | ||
|
|
190b6edfb1 | ||
|
|
80a0c59f87 | ||
|
|
823fdec705 | ||
|
|
fe87dcced7 | ||
|
|
137eb44516 | ||
|
|
f60d957102 | ||
|
|
8f0b04504f | ||
|
|
2c39d8b1c8 | ||
|
|
d4d1c32288 | ||
|
|
e4f39d2b6a | ||
|
|
e5a2bfbcbd | ||
|
|
de3b76b31d | ||
|
|
53455496bf | ||
|
|
cc2a2f6dfb | ||
|
|
ee4ac7371c | ||
|
|
d5265407b9 | ||
|
|
954b3e9fc5 | ||
|
|
7d9894bef7 | ||
|
|
3b34698e8b | ||
|
|
263cb581c4 | ||
|
|
1c9cb4516c | ||
|
|
ac4ceccb4f | ||
|
|
e731b7882d | ||
|
|
84e0728ff3 | ||
|
|
666bc18e91 | ||
|
|
8f83124a0d | ||
|
|
ee91daad7e | ||
|
|
ee78c0d33b | ||
|
|
1318abd37e | ||
|
|
76a031a8c9 | ||
|
|
09482ebcf3 | ||
|
|
67424f2d3a | ||
|
|
51f530ffbe | ||
|
|
013f96a754 | ||
|
|
df6a018fb6 | ||
|
|
409eaf54c1 | ||
|
|
7e04fd342c | ||
|
|
1fe15bc6a5 | ||
|
|
ff1bffbb55 | ||
|
|
b28b18a19a | ||
|
|
bbc3c85212 | ||
|
|
26a08fac06 | ||
|
|
da9d7a4336 | ||
|
|
46c6555f94 | ||
|
|
3e980fd2d4 | ||
|
|
fb1462f669 | ||
|
|
41e1630aac | ||
|
|
ef84c4e3da | ||
|
|
61cc44cc83 | ||
|
|
c20cbe7d66 | ||
|
|
2f4af3223b | ||
|
|
e4b2c42897 | ||
|
|
746df9277c | ||
|
|
8428588a4c | ||
|
|
83a8f4b911 | ||
|
|
2736024cb7 | ||
|
|
9a32ca893e | ||
|
|
59d3c6c94f | ||
|
|
388027b731 | ||
|
|
8abdedc11d | ||
|
|
9758f5baa8 | ||
|
|
248262a597 | ||
|
|
cc0f2c7c7f | ||
|
|
72f6468d12 | ||
|
|
522c0edd90 | ||
|
|
d338f217fe | ||
|
|
ca79857386 | ||
|
|
60e551e273 | ||
|
|
954e148be3 | ||
|
|
3d0b79f674 | ||
|
|
d9442aa23c | ||
|
|
ba0daf4452 | ||
|
|
8d9cd5bbd1 | ||
|
|
186b877c09 | ||
|
|
5ed2dfccd1 | ||
|
|
911cfd8642 | ||
|
|
3539bd1e79 | ||
|
|
f56df7c16d | ||
|
|
c507dfa6c4 | ||
|
|
f6d2e898dc | ||
|
|
326c7a93fb | ||
|
|
58381b8062 | ||
|
|
0899cea4b4 | ||
|
|
7459e937b5 | ||
|
|
55db0bebbb | ||
|
|
0bdb8142c6 | ||
|
|
88ee94d4b6 | ||
|
|
1df4ed0fe9 | ||
|
|
2a339a2935 | ||
|
|
a1810e6023 | ||
|
|
832ca3347c | ||
|
|
9d2b64e82b | ||
|
|
9a5e4b3f54 | ||
|
|
e5e8032ba1 | ||
|
|
5356e68b51 | ||
|
|
cd94c625a7 | ||
|
|
972a3746a1 | ||
|
|
a9e12e4384 | ||
|
|
1690e6420f | ||
|
|
acdf61f7ab | ||
|
|
2e4fc557ea | ||
|
|
979dcead49 | ||
|
|
116ddf345d | ||
|
|
366805a64f | ||
|
|
1fee2a846a | ||
|
|
3f54eb52b2 | ||
|
|
a3847ce1c9 | ||
|
|
1e7415b692 | ||
|
|
ff950ef28a | ||
|
|
51bd12c6cf |
8
.babelrc
Normal file
8
.babelrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"presets": ["react", "es2015"],
|
||||
"env": {
|
||||
"development": {
|
||||
"presets": ["react-hmre"]
|
||||
}
|
||||
}
|
||||
}
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
build/
|
||||
node_modules/
|
||||
electron_build/
|
||||
.DS_Store
|
||||
.env
|
||||
dist/
|
||||
vendor/
|
||||
Desktop.ini
|
||||
Thumbs.db
|
||||
node_modules/*
|
||||
!node_modules/boost
|
||||
/dist
|
||||
/compiled
|
||||
/secret
|
||||
|
||||
8
.gitmodules
vendored
8
.gitmodules
vendored
@@ -1,6 +1,4 @@
|
||||
[submodule "browser/ace"]
|
||||
path = browser/ace
|
||||
[submodule "submodules/ace"]
|
||||
path = submodules/ace
|
||||
url = https://github.com/ajaxorg/ace-builds.git
|
||||
[submodule "browser/electron-stylus"]
|
||||
path = browser/electron-stylus
|
||||
url = https://github.com/Rokt33r/electron-stylus.git
|
||||
branch = master
|
||||
|
||||
16
LICENSE
Normal file
16
LICENSE
Normal file
@@ -0,0 +1,16 @@
|
||||
Boostnote - the simplest note app
|
||||
|
||||
Copyright (C) 2016 MAISIN&CO.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
11
appdmg.json
Normal file
11
appdmg.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"title": "Boostnote",
|
||||
"icon": "resources/dmg.icns",
|
||||
"background": "resources/boostnote-install.png",
|
||||
"icon-size": 80,
|
||||
"contents": [
|
||||
{ "x": 448, "y": 344, "type": "link", "path": "/Applications" },
|
||||
{ "x": 192, "y": 344, "type": "file", "path": "dist/Boostnote-darwin-x64/Boostnote.app" }
|
||||
]
|
||||
|
||||
}
|
||||
11
bower.json
11
bower.json
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"name": "codexen-app",
|
||||
"dependencies": {
|
||||
"react": "~0.13.3",
|
||||
"fontawesome": "~4.3.0",
|
||||
"react-router": "~0.13.3",
|
||||
"reflux": "~0.2.8",
|
||||
"moment": "~2.10.3",
|
||||
"markdown-it": "~4.3.1"
|
||||
}
|
||||
}
|
||||
Submodule browser/ace deleted from 0982db4853
269
browser/components/CodeEditor.js
Normal file
269
browser/components/CodeEditor.js
Normal file
@@ -0,0 +1,269 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import modes from '../lib/modes'
|
||||
import _ from 'lodash'
|
||||
import fetchConfig from '../lib/fetchConfig'
|
||||
|
||||
const electron = require('electron')
|
||||
const remote = electron.remote
|
||||
const ipc = electron.ipcRenderer
|
||||
|
||||
const ace = window.ace
|
||||
|
||||
let config = fetchConfig()
|
||||
ipc.on('config-apply', function (e, newConfig) {
|
||||
config = newConfig
|
||||
})
|
||||
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
|
||||
this.changeHandler = e => this.handleChange(e)
|
||||
this.blurHandler = (e) => {
|
||||
if (e.relatedTarget === null) {
|
||||
return
|
||||
}
|
||||
|
||||
let isFocusingToSearch = e.relatedTarget.className && e.relatedTarget.className.split(' ').some(clss => {
|
||||
return clss === 'ace_search_field' || clss === 'ace_searchbtn' || clss === 'ace_replacebtn' || clss === 'ace_searchbtn_close' || clss === 'ace_text-input'
|
||||
})
|
||||
if (isFocusingToSearch) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.props.onBlur) this.props.onBlur(e)
|
||||
}
|
||||
|
||||
this.killedBuffer = ''
|
||||
this.execHandler = (e) => {
|
||||
console.log(e.command.name)
|
||||
switch (e.command.name) {
|
||||
case 'gotolinestart':
|
||||
e.preventDefault()
|
||||
{
|
||||
let position = this.editor.getCursorPosition()
|
||||
this.editor.navigateTo(position.row, 0)
|
||||
}
|
||||
break
|
||||
case 'gotolineend':
|
||||
e.preventDefault()
|
||||
let position = this.editor.getCursorPosition()
|
||||
this.editor.navigateTo(position.row, this.editor.getSession().getLine(position.row).length)
|
||||
break
|
||||
case 'jumptomatching':
|
||||
e.preventDefault()
|
||||
this.editor.navigateUp()
|
||||
break
|
||||
case 'removetolineend':
|
||||
e.preventDefault()
|
||||
let range = this.editor.getSelectionRange()
|
||||
let session = this.editor.getSession()
|
||||
if (range.isEmpty()) {
|
||||
range.setEnd(range.start.row, session.getLine(range.start.row).length)
|
||||
this.killedBuffer = session.getTextRange(range)
|
||||
if (this.killedBuffer.length > 0) {
|
||||
console.log('remove to lineend')
|
||||
session.remove(range)
|
||||
} else {
|
||||
if (session.getLength() === range.start.row) {
|
||||
return
|
||||
}
|
||||
range.setStart(range.start.row, range.end.col)
|
||||
range.setEnd(range.start.row + 1, 0)
|
||||
this.killedBuffer = '\n'
|
||||
session.remove(range)
|
||||
}
|
||||
} else {
|
||||
this.killedBuffer = session.getTextRange(range)
|
||||
session.remove(range)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.afterExecHandler = (e) => {
|
||||
switch (e.command.name) {
|
||||
case 'find':
|
||||
Array.prototype.forEach.call(ReactDOM.findDOMNode(this).querySelectorAll('.ace_search_field, .ace_searchbtn, .ace_replacebtn, .ace_searchbtn_close'), el => {
|
||||
el.removeEventListener('blur', this.blurHandler)
|
||||
el.addEventListener('blur', this.blurHandler)
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
this.state = {
|
||||
fontSize: config['editor-font-size'],
|
||||
fontFamily: config['editor-font-family'],
|
||||
indentType: config['editor-indent-type'],
|
||||
indentSize: config['editor-indent-size'],
|
||||
themeSyntax: config['theme-syntax']
|
||||
}
|
||||
|
||||
this.silentChange = false
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.readOnly !== this.props.readOnly) {
|
||||
this.editor.setReadOnly(!!nextProps.readOnly)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
let { article } = this.props
|
||||
var el = ReactDOM.findDOMNode(this)
|
||||
var editor = this.editor = ace.edit(el)
|
||||
editor.$blockScrolling = Infinity
|
||||
editor.renderer.setShowGutter(true)
|
||||
editor.setTheme('ace/theme/' + this.state.themeSyntax)
|
||||
editor.moveCursorTo(0, 0)
|
||||
editor.setReadOnly(!!this.props.readOnly)
|
||||
editor.setFontSize(this.state.fontSize)
|
||||
|
||||
editor.on('blur', this.blurHandler)
|
||||
|
||||
editor.commands.addCommand({
|
||||
name: 'Emacs cursor up',
|
||||
bindKey: {mac: 'Ctrl-P'},
|
||||
exec: function (editor) {
|
||||
editor.navigateUp(1)
|
||||
if (editor.getCursorPosition().row < editor.getFirstVisibleRow()) editor.scrollToLine(editor.getCursorPosition().row, false, false)
|
||||
},
|
||||
readOnly: true
|
||||
})
|
||||
editor.commands.addCommand({
|
||||
name: 'Emacs cursor up',
|
||||
bindKey: {mac: 'Ctrl-Y'},
|
||||
exec: function (editor) {
|
||||
editor.insert(this.killedBuffer)
|
||||
}.bind(this),
|
||||
readOnly: true
|
||||
})
|
||||
editor.commands.addCommand({
|
||||
name: 'Focus title',
|
||||
bindKey: {win: 'Esc', mac: 'Esc'},
|
||||
exec: function (editor, e) {
|
||||
let currentWindow = remote.getCurrentWebContents()
|
||||
if (config['switch-preview'] === 'rightclick') {
|
||||
currentWindow.send('detail-preview')
|
||||
}
|
||||
currentWindow.send('list-focus')
|
||||
},
|
||||
readOnly: true
|
||||
})
|
||||
|
||||
editor.commands.on('exec', this.execHandler)
|
||||
editor.commands.on('afterExec', this.afterExecHandler)
|
||||
|
||||
var session = editor.getSession()
|
||||
let mode = _.findWhere(modes, {name: article.mode})
|
||||
let syntaxMode = mode != null
|
||||
? mode.mode
|
||||
: 'text'
|
||||
session.setMode('ace/mode/' + syntaxMode)
|
||||
|
||||
session.setUseSoftTabs(this.state.indentType === 'space')
|
||||
session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
|
||||
session.setOption('useWorker', false)
|
||||
session.setUseWrapMode(true)
|
||||
session.setValue(this.props.article.content)
|
||||
|
||||
session.on('change', this.changeHandler)
|
||||
|
||||
ipc.on('config-apply', this.configApplyHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
ipc.removeListener('config-apply', this.configApplyHandler)
|
||||
this.editor.getSession().removeListener('change', this.changeHandler)
|
||||
this.editor.removeListener('blur', this.blurHandler)
|
||||
this.editor.commands.removeListener('exec', this.execHandler)
|
||||
this.editor.commands.removeListener('afterExec', this.afterExecHandler)
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
var session = this.editor.getSession()
|
||||
if (this.props.article.key !== prevProps.article.key) {
|
||||
session.removeListener('change', this.changeHandler)
|
||||
session.setValue(this.props.article.content)
|
||||
session.getUndoManager().reset()
|
||||
session.on('change', this.changeHandler)
|
||||
}
|
||||
if (prevProps.article.mode !== this.props.article.mode) {
|
||||
let mode = _.findWhere(modes, {name: this.props.article.mode})
|
||||
let syntaxMode = mode != null
|
||||
? mode.mode
|
||||
: 'text'
|
||||
session.setMode('ace/mode/' + syntaxMode)
|
||||
}
|
||||
}
|
||||
|
||||
handleConfigApply (e, config) {
|
||||
this.setState({
|
||||
fontSize: config['editor-font-size'],
|
||||
fontFamily: config['editor-font-family'],
|
||||
indentType: config['editor-indent-type'],
|
||||
indentSize: config['editor-indent-size'],
|
||||
themeSyntax: config['theme-syntax']
|
||||
}, function () {
|
||||
var editor = this.editor
|
||||
editor.setTheme('ace/theme/' + this.state.themeSyntax)
|
||||
|
||||
var session = editor.getSession()
|
||||
session.setUseSoftTabs(this.state.indentType === 'space')
|
||||
session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
|
||||
})
|
||||
}
|
||||
handleChange (e) {
|
||||
if (this.props.onChange) {
|
||||
var value = this.editor.getValue()
|
||||
this.props.onChange(value)
|
||||
}
|
||||
}
|
||||
|
||||
getFirstVisibleRow () {
|
||||
return this.editor.getFirstVisibleRow()
|
||||
}
|
||||
|
||||
getCursorPosition () {
|
||||
return this.editor.getCursorPosition()
|
||||
}
|
||||
|
||||
moveCursorTo (row, col) {
|
||||
this.editor.moveCursorTo(row, col)
|
||||
}
|
||||
|
||||
scrollToLine (num) {
|
||||
this.editor.scrollToLine(num, false, false)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div
|
||||
className={this.props.className == null ? 'CodeEditor' : 'CodeEditor ' + this.props.className}
|
||||
style={{
|
||||
fontSize: this.state.fontSize,
|
||||
fontFamily: this.state.fontFamily.trim() + ', monospace'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CodeEditor.propTypes = {
|
||||
article: PropTypes.shape({
|
||||
content: PropTypes.string,
|
||||
mode: PropTypes.string,
|
||||
key: PropTypes.string
|
||||
}),
|
||||
className: PropTypes.string,
|
||||
onBlur: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
readOnly: PropTypes.bool
|
||||
}
|
||||
|
||||
CodeEditor.defaultProps = {
|
||||
readOnly: false
|
||||
}
|
||||
|
||||
export default CodeEditor
|
||||
20
browser/components/ExternalLink.js
Normal file
20
browser/components/ExternalLink.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
const electron = require('electron')
|
||||
const shell = electron.shell
|
||||
|
||||
export default class ExternalLink extends React.Component {
|
||||
handleClick (e) {
|
||||
shell.openExternal(this.props.href)
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<a onClick={e => this.handleClick(e)} {...this.props}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ExternalLink.propTypes = {
|
||||
href: PropTypes.string
|
||||
}
|
||||
52
browser/components/FolderMark.js
Normal file
52
browser/components/FolderMark.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
|
||||
const BLUE = '#3460C7'
|
||||
const LIGHTBLUE = '#2BA5F7'
|
||||
const ORANGE = '#FF8E00'
|
||||
const YELLOW = '#E8D252'
|
||||
const GREEN = '#3FD941'
|
||||
const DARKGREEN = '#1FAD85'
|
||||
const RED = '#E10051'
|
||||
const PURPLE = '#B013A4'
|
||||
|
||||
function getColorByIndex (index) {
|
||||
switch (index % 8) {
|
||||
case 0:
|
||||
return RED
|
||||
case 1:
|
||||
return ORANGE
|
||||
case 2:
|
||||
return YELLOW
|
||||
case 3:
|
||||
return GREEN
|
||||
case 4:
|
||||
return DARKGREEN
|
||||
case 5:
|
||||
return LIGHTBLUE
|
||||
case 6:
|
||||
return BLUE
|
||||
case 7:
|
||||
return PURPLE
|
||||
default:
|
||||
return DARKGREEN
|
||||
}
|
||||
}
|
||||
|
||||
export default class FolderMark extends React.Component {
|
||||
render () {
|
||||
let color = getColorByIndex(this.props.color)
|
||||
let className = 'FolderMark fa fa-square fa-fw'
|
||||
if (this.props.className != null) {
|
||||
className += ' active'
|
||||
}
|
||||
|
||||
return (
|
||||
<i className={className} style={{color: color}}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FolderMark.propTypes = {
|
||||
color: PropTypes.number,
|
||||
className: PropTypes.string
|
||||
}
|
||||
223
browser/components/MarkdownPreview.js
Normal file
223
browser/components/MarkdownPreview.js
Normal file
@@ -0,0 +1,223 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import markdown from '../lib/markdown'
|
||||
import ReactDOM from 'react-dom'
|
||||
import sanitizeHtml from '@rokt33r/sanitize-html'
|
||||
import _ from 'lodash'
|
||||
import fetchConfig from '../lib/fetchConfig'
|
||||
|
||||
const electron = require('electron')
|
||||
const shell = electron.shell
|
||||
const ipc = electron.ipcRenderer
|
||||
|
||||
const katex = window.katex
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
const sanitizeOpts = {
|
||||
allowedTags: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
||||
'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
|
||||
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'img', 'span', 'cite', 'del', 'u', 'sub', 'sup', 's', 'input', 'label' ],
|
||||
allowedClasses: {
|
||||
'a': ['lineAnchor'],
|
||||
'div': ['math'],
|
||||
'pre': ['hljs'],
|
||||
'span': ['math', 'hljs-*'],
|
||||
'code': ['language-*']
|
||||
},
|
||||
allowedAttributes: {
|
||||
a: ['href', 'data-key'],
|
||||
img: [ 'src' ],
|
||||
label: ['for'],
|
||||
input: ['checked', 'type'],
|
||||
'*': ['id', 'name']
|
||||
},
|
||||
transformTags: {
|
||||
'*': function (tagName, attribs) {
|
||||
let href = attribs.href
|
||||
if (tagName === 'input' && attribs.type !== 'checkbox') {
|
||||
return false
|
||||
}
|
||||
if (_.isString(href) && href.match(/^#.+$/)) attribs.href = href.replace(/^#/, '#md-anchor-')
|
||||
if (attribs.id) attribs.id = 'md-anchor-' + attribs.id
|
||||
if (attribs.name) attribs.name = 'md-anchor-' + attribs.name
|
||||
if (attribs.for) attribs.for = 'md-anchor-' + attribs.for
|
||||
return {
|
||||
tagName: tagName,
|
||||
attribs: attribs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleAnchorClick (e) {
|
||||
if (this.attributes.href && this.attributes.href.nodeValue.match(/^#.+/)) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
let href = this.href
|
||||
if (href && href.match(/^http:\/\/|https:\/\/|mailto:\/\//)) {
|
||||
shell.openExternal(href)
|
||||
}
|
||||
}
|
||||
|
||||
function stopPropagation (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
function math2Katex (display) {
|
||||
return function (el) {
|
||||
try {
|
||||
katex.render(el.innerHTML.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/&/g, '&'), el, {display: display})
|
||||
el.className = 'math-rendered'
|
||||
} catch (e) {
|
||||
el.innerHTML = e.message
|
||||
el.className = 'math-failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let config = fetchConfig()
|
||||
ipc.on('config-apply', function (e, newConfig) {
|
||||
config = newConfig
|
||||
})
|
||||
|
||||
export default class MarkdownPreview extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
|
||||
|
||||
this.state = {
|
||||
fontSize: config['preview-font-size'],
|
||||
fontFamily: config['preview-font-family']
|
||||
}
|
||||
}
|
||||
componentDidMount () {
|
||||
this.addListener()
|
||||
this.renderMath()
|
||||
ipc.on('config-apply', this.configApplyHandler)
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
this.addListener()
|
||||
this.renderMath()
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.removeListener()
|
||||
ipc.removeListener('config-apply', this.configApplyHandler)
|
||||
}
|
||||
|
||||
componentWillUpdate () {
|
||||
this.removeListener()
|
||||
}
|
||||
|
||||
renderMath () {
|
||||
let inline = ReactDOM.findDOMNode(this).querySelectorAll('span.math')
|
||||
Array.prototype.forEach.call(inline, math2Katex(false))
|
||||
let block = ReactDOM.findDOMNode(this).querySelectorAll('div.math')
|
||||
Array.prototype.forEach.call(block, math2Katex(true))
|
||||
}
|
||||
|
||||
addListener () {
|
||||
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a:not(.lineAnchor)')
|
||||
var inputs = ReactDOM.findDOMNode(this).querySelectorAll('input')
|
||||
|
||||
Array.prototype.forEach.call(anchors, anchor => {
|
||||
anchor.addEventListener('click', handleAnchorClick)
|
||||
anchor.addEventListener('mousedown', stopPropagation)
|
||||
anchor.addEventListener('mouseup', stopPropagation)
|
||||
})
|
||||
Array.prototype.forEach.call(inputs, input => {
|
||||
input.addEventListener('click', stopPropagation)
|
||||
})
|
||||
}
|
||||
|
||||
removeListener () {
|
||||
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a:not(.lineAnchor)')
|
||||
var inputs = ReactDOM.findDOMNode(this).querySelectorAll('input')
|
||||
|
||||
Array.prototype.forEach.call(anchors, anchor => {
|
||||
anchor.removeEventListener('click', handleAnchorClick)
|
||||
anchor.removeEventListener('mousedown', stopPropagation)
|
||||
anchor.removeEventListener('mouseup', stopPropagation)
|
||||
})
|
||||
Array.prototype.forEach.call(inputs, input => {
|
||||
input.removeEventListener('click', stopPropagation)
|
||||
})
|
||||
}
|
||||
|
||||
handleClick (e) {
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick(e)
|
||||
}
|
||||
}
|
||||
|
||||
handleDoubleClick (e) {
|
||||
if (this.props.onDoubleClick) {
|
||||
this.props.onDoubleClick(e)
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseDown (e) {
|
||||
if (this.props.onMouseDown) {
|
||||
this.props.onMouseDown(e)
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseUp (e) {
|
||||
if (this.props.onMouseUp) {
|
||||
this.props.onMouseUp(e)
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseMove (e) {
|
||||
if (this.props.onMouseMove) {
|
||||
this.props.onMouseMove(e)
|
||||
}
|
||||
}
|
||||
|
||||
handleConfigApply (e, config) {
|
||||
this.setState({
|
||||
fontSize: config['preview-font-size'],
|
||||
fontFamily: config['preview-font-family']
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
let isEmpty = this.props.content.trim().length === 0
|
||||
let content = isEmpty
|
||||
? '(Empty content)'
|
||||
: this.props.content
|
||||
content = markdown(content)
|
||||
content = sanitizeHtml(content, sanitizeOpts)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'MarkdownPreview' + (this.props.className != null ? ' ' + this.props.className : '') + (isEmpty ? ' empty' : '')}
|
||||
onClick={e => this.handleClick(e)}
|
||||
onDoubleClick={e => this.handleDoubleClick(e)}
|
||||
onMouseDown={e => this.handleMouseDown(e)}
|
||||
onMouseMove={e => this.handleMouseMove(e)}
|
||||
onMouseUp={e => this.handleMouseUp(e)}
|
||||
dangerouslySetInnerHTML={{__html: ' ' + content}}
|
||||
style={{
|
||||
fontSize: this.state.fontSize,
|
||||
fontFamily: this.state.fontFamily.trim() + (OSX ? '' : ', meiryo, \'Microsoft YaHei\'') + ', helvetica, arial, sans-serif'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MarkdownPreview.propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
onDoubleClick: PropTypes.func,
|
||||
onMouseUp: PropTypes.func,
|
||||
onMouseDown: PropTypes.func,
|
||||
onMouseMove: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
content: PropTypes.string
|
||||
}
|
||||
82
browser/components/ModeIcon.js
Normal file
82
browser/components/ModeIcon.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
|
||||
export default class ModeIcon extends React.Component {
|
||||
getClassName () {
|
||||
var mode = this.props.mode
|
||||
switch (mode) {
|
||||
// Script
|
||||
case 'javascript':
|
||||
return 'devicon-javascript-plain'
|
||||
case 'jsx':
|
||||
return 'devicon-react-original'
|
||||
case 'coffee':
|
||||
return 'devicon-coffeescript-original'
|
||||
case 'ruby':
|
||||
return 'devicon-ruby-plain'
|
||||
case 'erlang':
|
||||
return 'devicon-erlang-plain'
|
||||
case 'php':
|
||||
return 'devicon-php-plain'
|
||||
|
||||
// HTML
|
||||
case 'html':
|
||||
return 'devicon-html5-plain'
|
||||
|
||||
// Stylesheet
|
||||
case 'css':
|
||||
return 'devicon-css3-plain'
|
||||
case 'less':
|
||||
return 'devicon-less-plain-wordmark'
|
||||
case 'sass':
|
||||
case 'scss':
|
||||
return 'devicon-sass-original'
|
||||
|
||||
// Compile
|
||||
case 'c':
|
||||
return 'devicon-c-plain'
|
||||
case 'cpp':
|
||||
return 'devicon-cplusplus-plain'
|
||||
case 'csharp':
|
||||
return 'devicon-csharp-plain'
|
||||
case 'objc':
|
||||
return 'devicon-apple-original'
|
||||
case 'golang':
|
||||
return 'devicon-go-plain'
|
||||
case 'java':
|
||||
return 'devicon-java-plain'
|
||||
|
||||
// Framework
|
||||
case 'django':
|
||||
return 'devicon-django-plain'
|
||||
|
||||
// Config
|
||||
case 'dockerfile':
|
||||
return 'devicon-docker-plain'
|
||||
case 'gitignore':
|
||||
return 'devicon-git-plain'
|
||||
|
||||
// Shell
|
||||
case 'sh':
|
||||
case 'batchfile':
|
||||
case 'powershell':
|
||||
return 'fa fa-fw fa-terminal'
|
||||
|
||||
case 'text':
|
||||
case 'markdown':
|
||||
return 'fa fa-fw fa-file-text-o'
|
||||
}
|
||||
return 'fa fa-fw fa-code'
|
||||
}
|
||||
|
||||
render () {
|
||||
let className = `ModeIcon ${this.getClassName()} ${this.props.className}`
|
||||
return (
|
||||
<i className={className}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ModeIcon.propTypes = {
|
||||
className: PropTypes.string,
|
||||
mode: PropTypes.string
|
||||
}
|
||||
172
browser/components/ModeSelect.js
Normal file
172
browser/components/ModeSelect.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ModeIcon from './ModeIcon'
|
||||
import modes from '../lib/modes'
|
||||
import _ from 'lodash'
|
||||
|
||||
const IDLE_MODE = 'IDLE_MODE'
|
||||
const EDIT_MODE = 'EDIT_MODE'
|
||||
|
||||
export default class ModeSelect extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
mode: IDLE_MODE,
|
||||
search: '',
|
||||
focusIndex: 0
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.blurHandler = e => {
|
||||
let searchElement = ReactDOM.findDOMNode(this.refs.search)
|
||||
if (this.state.mode === EDIT_MODE && document.activeElement !== searchElement) {
|
||||
this.handleBlur(e)
|
||||
}
|
||||
}
|
||||
window.addEventListener('click', this.blurHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('click', this.blurHandler)
|
||||
let searchElement = ReactDOM.findDOMNode(this.refs.search)
|
||||
if (searchElement != null && this.searchKeyDownListener != null) {
|
||||
searchElement.removeEventListener('keydown', this.searchKeyDownListener)
|
||||
}
|
||||
}
|
||||
|
||||
handleIdleSelectClick (e) {
|
||||
this.setState({mode: EDIT_MODE, search: this.props.value}, () => {
|
||||
ReactDOM.findDOMNode(this.refs.search).select()
|
||||
})
|
||||
}
|
||||
|
||||
handleModeOptionClick (modeName) {
|
||||
return e => {
|
||||
this.props.onChange(modeName)
|
||||
this.setState({
|
||||
mode: IDLE_MODE,
|
||||
search: '',
|
||||
focusIndex: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
// up
|
||||
case 38:
|
||||
e.preventDefault()
|
||||
if (this.state.focusIndex > 0) this.setState({focusIndex: this.state.focusIndex - 1})
|
||||
break
|
||||
// down
|
||||
case 40:
|
||||
e.preventDefault()
|
||||
{
|
||||
let search = _.escapeRegExp(this.state.search)
|
||||
let filteredModes = modes
|
||||
.filter(mode => {
|
||||
let nameMatched = mode.name.match(search)
|
||||
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
|
||||
return nameMatched || aliasMatched
|
||||
})
|
||||
if (filteredModes.length === this.state.focusIndex + 1) this.setState({focusIndex: filteredModes.length - 1})
|
||||
else this.setState({focusIndex: this.state.focusIndex + 1})
|
||||
}
|
||||
break
|
||||
// enter
|
||||
case 13:
|
||||
e.preventDefault()
|
||||
{
|
||||
let search = _.escapeRegExp(this.state.search)
|
||||
let filteredModes = modes
|
||||
.filter(mode => {
|
||||
let nameMatched = mode.name.match(search)
|
||||
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
|
||||
return nameMatched || aliasMatched
|
||||
})
|
||||
let targetMode = filteredModes[this.state.focusIndex]
|
||||
if (targetMode != null) {
|
||||
this.props.onChange(targetMode.name)
|
||||
this.setIdle()
|
||||
}
|
||||
}
|
||||
break
|
||||
// esc
|
||||
case 27:
|
||||
case 9:
|
||||
e.stopPropagation()
|
||||
this.setIdle()
|
||||
}
|
||||
if (this.props.onKeyDown) this.props.onKeyDown(e)
|
||||
}
|
||||
|
||||
handleSearchChange (e) {
|
||||
this.setState({
|
||||
search: e.target.value,
|
||||
focusIndex: 0
|
||||
})
|
||||
}
|
||||
|
||||
handleBlur (e) {
|
||||
if (e.target !== ReactDOM.findDOMNode(this.refs.search)) {
|
||||
this.setIdle()
|
||||
}
|
||||
}
|
||||
|
||||
setIdle () {
|
||||
this.setState({
|
||||
mode: IDLE_MODE,
|
||||
search: '',
|
||||
focusIndex: 0
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
let className = this.props.className != null
|
||||
? `ModeSelect ${this.props.className}`
|
||||
: this.props.className
|
||||
|
||||
if (this.state.mode === IDLE_MODE) {
|
||||
let mode = _.findWhere(modes, {name: this.props.value})
|
||||
let modeName = mode != null ? mode.name : 'text'
|
||||
let modeLabel = mode != null ? mode.label : this.props.value
|
||||
|
||||
return (
|
||||
<div className={className + ' idle'} onClick={e => this.handleIdleSelectClick(e)}>
|
||||
<ModeIcon mode={modeName}/>{modeLabel}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
let search = _.escapeRegExp(this.state.search)
|
||||
let filteredOptions = modes
|
||||
.filter(mode => {
|
||||
let nameMatched = mode.name.match(search)
|
||||
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
|
||||
return nameMatched || aliasMatched
|
||||
})
|
||||
.map((mode, index) => {
|
||||
return (
|
||||
<div key={mode.name} className={index === this.state.focusIndex ? 'ModeSelect-options-item active' : 'ModeSelect-options-item'} onClick={e => this.handleModeOptionClick(mode.name)(e)}><ModeIcon mode={mode.name}/>{mode.label}</div>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={className + ' edit'}>
|
||||
<input onBlur={e => this.handleBlur(e)} onKeyDown={e => this.handleSearchKeyDown(e)} ref='search' onChange={e => this.handleSearchChange(e)} value={this.state.search} type='text'/>
|
||||
<div ref='options' className='ModeSelect-options hide'>
|
||||
{filteredOptions}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ModeSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onKeyDown: PropTypes.func
|
||||
}
|
||||
24
browser/components/ProfileImage.js
Normal file
24
browser/components/ProfileImage.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import md5 from 'md5'
|
||||
|
||||
export default class ProfileImage extends React.Component {
|
||||
render () {
|
||||
let className = this.props.className == null ? 'ProfileImage' : 'ProfileImage ' + this.props.className
|
||||
let email = this.props.email != null ? this.props.email : ''
|
||||
let src = 'http://www.gravatar.com/avatar/' + md5(email.trim().toLowerCase()) + '?s=' + this.props.size
|
||||
|
||||
return (
|
||||
<img
|
||||
className={className}
|
||||
width={this.props.size}
|
||||
height={this.props.size}
|
||||
src={src}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ProfileImage.propTypes = {
|
||||
email: PropTypes.string,
|
||||
size: PropTypes.string,
|
||||
className: PropTypes.string
|
||||
}
|
||||
168
browser/components/TagSelect.js
Normal file
168
browser/components/TagSelect.js
Normal file
@@ -0,0 +1,168 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import _ from 'lodash'
|
||||
import linkState from '../lib/linkState'
|
||||
|
||||
function isNotEmptyString (str) {
|
||||
return _.isString(str) && str.length > 0
|
||||
}
|
||||
|
||||
export default class TagSelect extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
input: '',
|
||||
isInputFocused: false
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.blurInputBlurHandler = e => {
|
||||
if (ReactDOM.findDOMNode(this.refs.tagInput) !== document.activeElement) {
|
||||
this.setState({isInputFocused: false})
|
||||
}
|
||||
}
|
||||
window.addEventListener('click', this.blurInputBlurHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount (e) {
|
||||
window.removeEventListener('click', this.blurInputBlurHandler)
|
||||
}
|
||||
|
||||
// Suggestは必ずInputの下に位置するようにする
|
||||
componentDidUpdate () {
|
||||
if (this.shouldShowSuggest()) {
|
||||
let inputRect = ReactDOM.findDOMNode(this.refs.tagInput).getBoundingClientRect()
|
||||
let suggestElement = ReactDOM.findDOMNode(this.refs.suggestTags)
|
||||
if (suggestElement != null) {
|
||||
suggestElement.style.top = inputRect.top + 20 + 'px'
|
||||
suggestElement.style.left = inputRect.left + 'px'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shouldShowSuggest () {
|
||||
return this.state.isInputFocused && isNotEmptyString(this.state.input)
|
||||
}
|
||||
|
||||
addTag (tag, clearInput = true) {
|
||||
let tags = this.props.tags.slice(0)
|
||||
let newTag = tag.trim()
|
||||
|
||||
if (newTag.length === 0 && clearInput) {
|
||||
this.setState({input: ''})
|
||||
return
|
||||
}
|
||||
|
||||
tags.push(newTag)
|
||||
tags = _.uniq(tags)
|
||||
|
||||
if (_.isFunction(this.props.onChange)) {
|
||||
this.props.onChange(newTag, tags)
|
||||
}
|
||||
if (clearInput) this.setState({input: ''})
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
case 8:
|
||||
{
|
||||
if (this.state.input.length > 0) break
|
||||
e.preventDefault()
|
||||
|
||||
let tags = this.props.tags.slice(0)
|
||||
tags.pop()
|
||||
|
||||
this.props.onChange(null, tags)
|
||||
}
|
||||
break
|
||||
case 13:
|
||||
{
|
||||
e.preventDefault()
|
||||
this.addTag(this.state.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleThisClick (e) {
|
||||
ReactDOM.findDOMNode(this.refs.tagInput).focus()
|
||||
}
|
||||
|
||||
handleInputFocus (e) {
|
||||
this.setState({isInputFocused: true})
|
||||
}
|
||||
|
||||
handleItemRemoveButton (tag) {
|
||||
return e => {
|
||||
e.stopPropagation()
|
||||
|
||||
let tags = this.props.tags.slice(0)
|
||||
tags.splice(tags.indexOf(tag), 1)
|
||||
|
||||
if (_.isFunction(this.props.onChange)) {
|
||||
this.props.onChange(null, tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleSuggestClick (tag) {
|
||||
return e => {
|
||||
this.addTag(tag)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { tags, suggestTags } = this.props
|
||||
|
||||
let tagElements = _.isArray(tags)
|
||||
? this.props.tags.map(tag => (
|
||||
<div key={tag} className='TagSelect-tags-item'>
|
||||
<button onClick={e => this.handleItemRemoveButton(tag)(e)} className='TagSelect-tags-item-remove'><i className='fa fa-fw fa-times'/></button>
|
||||
<div className='TagSelect-tags-item-label'>{tag}</div>
|
||||
</div>))
|
||||
: null
|
||||
|
||||
let suggestElements = this.shouldShowSuggest() ? suggestTags
|
||||
.filter(tag => {
|
||||
return tag.match(this.state.input)
|
||||
})
|
||||
.map(tag => {
|
||||
return <button onClick={e => this.handleSuggestClick(tag)(e)} key={tag}>{tag}</button>
|
||||
})
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className='TagSelect' onClick={e => this.handleThisClick(e)}>
|
||||
<div className='TagSelect-tags'>
|
||||
{tagElements}
|
||||
<input
|
||||
type='text'
|
||||
onKeyDown={e => this.handleKeyDown(e)}
|
||||
ref='tagInput'
|
||||
valueLink={this.linkState('input')}
|
||||
placeholder='Click here to add tags'
|
||||
className='TagSelect-input'
|
||||
onFocus={e => this.handleInputFocus(e)}
|
||||
/>
|
||||
</div>
|
||||
{suggestElements != null && suggestElements.length > 0
|
||||
? (
|
||||
<div ref='suggestTags' className='TagSelect-suggest'>
|
||||
{suggestElements}
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TagSelect.propTypes = {
|
||||
tags: PropTypes.arrayOf(PropTypes.string),
|
||||
onChange: PropTypes.func,
|
||||
suggestTags: PropTypes.array
|
||||
}
|
||||
|
||||
TagSelect.prototype.linkState = linkState
|
||||
Submodule browser/electron-stylus deleted from 6d86de7c66
@@ -1,42 +0,0 @@
|
||||
var React = require('react/addons')
|
||||
|
||||
var CodeViewer = require('../../main/Components/CodeViewer')
|
||||
|
||||
var MarkdownPreview = require('../../main/Components/MarkdownPreview')
|
||||
|
||||
module.exports = React.createClass({
|
||||
propTypes: {
|
||||
currentArticle: React.PropTypes.object
|
||||
},
|
||||
render: function () {
|
||||
var article = this.props.currentArticle
|
||||
|
||||
if (article != null) {
|
||||
if (article.type === 'code') {
|
||||
return (
|
||||
<div className='FinderDetail'>
|
||||
<div className='header'><i className='fa fa-code fa-fw'/> {article.description}</div>
|
||||
<div className='content'>
|
||||
<CodeViewer code={article.content} mode={article.mode}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} else if (article.type === 'note') {
|
||||
|
||||
return (
|
||||
<div className='FinderDetail'>
|
||||
<div className='header'><i className='fa fa-file-text-o fa-fw'/> {article.title}</div>
|
||||
<div className='content'>
|
||||
<MarkdownPreview className='marked' content={article.content}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className='FinderDetail'>
|
||||
<div className='nothing'>Nothing selected</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,15 +0,0 @@
|
||||
var React = require('react/addons')
|
||||
|
||||
module.exports = React.createClass({
|
||||
propTypes: {
|
||||
onChange: React.PropTypes.func,
|
||||
search: React.PropTypes.string
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<div className='FinderInput'>
|
||||
<input value={this.props.search} onChange={this.props.onChange} type='text'/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,79 +0,0 @@
|
||||
var React = require('react/addons')
|
||||
|
||||
module.exports = React.createClass({
|
||||
propTypes: {
|
||||
articles: React.PropTypes.arrayOf,
|
||||
currentArticle: React.PropTypes.shape({
|
||||
id: React.PropTypes.number,
|
||||
type: React.PropTypes.string
|
||||
}),
|
||||
selectArticle: React.PropTypes.func
|
||||
},
|
||||
componentDidUpdate: function () {
|
||||
var index = this.props.articles.indexOf(this.props.currentArticle)
|
||||
var el = React.findDOMNode(this)
|
||||
var li = el.querySelectorAll('li')[index]
|
||||
|
||||
if (li == null) {
|
||||
return
|
||||
}
|
||||
|
||||
var overflowBelow = el.clientHeight + el.scrollTop < li.offsetTop + li.clientHeight
|
||||
if (overflowBelow) {
|
||||
el.scrollTop = li.offsetTop + li.clientHeight - el.clientHeight
|
||||
}
|
||||
var overflowAbove = el.scrollTop > li.offsetTop
|
||||
if (overflowAbove) {
|
||||
el.scrollTop = li.offsetTop
|
||||
}
|
||||
},
|
||||
handleArticleClick: function (article) {
|
||||
return function () {
|
||||
this.props.selectArticle(article)
|
||||
}.bind(this)
|
||||
},
|
||||
render: function () {
|
||||
var list = this.props.articles.map(function (article) {
|
||||
if (article == null) {
|
||||
return (
|
||||
<li className={isActive ? 'active' : ''}>
|
||||
<div className='articleItem'>Undefined</div>
|
||||
<div className='divider'/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
var isActive = this.props.currentArticle != null && (article.type === this.props.currentArticle.type && article.id === this.props.currentArticle.id)
|
||||
if (article.type === 'code') {
|
||||
return (
|
||||
<li onClick={this.handleArticleClick(article)} className={isActive ? 'active' : ''}>
|
||||
<div className='articleItem'><i className='fa fa-code fa-fw'/> {article.description}</div>
|
||||
<div className='divider'/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
if (article.type === 'note') {
|
||||
return (
|
||||
<li onClick={this.handleArticleClick(article)} className={isActive ? 'active' : ''}>
|
||||
<div className='articleItem'><i className='fa fa-file-text-o fa-fw'/> {article.title}</div>
|
||||
<div className='divider'/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<li className={isActive ? 'active' : ''}>
|
||||
<div className='articleItem'>Undefined</div>
|
||||
<div className='divider'/>
|
||||
</li>
|
||||
)
|
||||
}.bind(this))
|
||||
|
||||
return (
|
||||
<div className='FinderList'>
|
||||
<ul>
|
||||
{list}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
44
browser/finder/FinderDetail.js
Normal file
44
browser/finder/FinderDetail.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import ModeIcon from 'browser/components/ModeIcon'
|
||||
|
||||
export default class FinderDetail extends React.Component {
|
||||
render () {
|
||||
let { activeArticle } = this.props
|
||||
|
||||
if (activeArticle != null) {
|
||||
return (
|
||||
<div className='FinderDetail'>
|
||||
<div className='header'>
|
||||
<div className='left'>
|
||||
<ModeIcon mode={activeArticle.mode}/> {activeArticle.title}
|
||||
</div>
|
||||
<div className='right'>
|
||||
<button onClick={this.props.saveToClipboard} className='clipboardBtn'>
|
||||
<i className='fa fa-clipboard fa-fw'/>
|
||||
<span className='tooltip'>Copy to clipboard (Enter)</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='content'>
|
||||
{activeArticle.mode === 'markdown'
|
||||
? <MarkdownPreview content={activeArticle.content}/>
|
||||
: <CodeEditor readOnly article={activeArticle}/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className='FinderDetail'>
|
||||
<div className='nothing'>Nothing selected</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FinderDetail.propTypes = {
|
||||
activeArticle: PropTypes.shape(),
|
||||
saveToClipboard: PropTypes.func
|
||||
}
|
||||
16
browser/finder/FinderInput.js
Normal file
16
browser/finder/FinderInput.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
|
||||
export default class FinderInput extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div className='FinderInput'>
|
||||
<input ref='input' value={this.props.value} onChange={this.props.handleSearchChange} type='text'/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FinderInput.propTypes = {
|
||||
handleSearchChange: PropTypes.func,
|
||||
value: PropTypes.string
|
||||
}
|
||||
71
browser/finder/FinderList.js
Normal file
71
browser/finder/FinderList.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ModeIcon from 'browser/components/ModeIcon'
|
||||
import { selectArticle } from './actions'
|
||||
|
||||
export default class FinderList extends React.Component {
|
||||
componentDidUpdate () {
|
||||
var index = this.props.articles.indexOf(this.props.activeArticle)
|
||||
var el = ReactDOM.findDOMNode(this)
|
||||
var li = el.querySelectorAll('li')[index]
|
||||
|
||||
if (li == null) {
|
||||
return
|
||||
}
|
||||
|
||||
var overflowBelow = el.clientHeight + el.scrollTop < li.offsetTop + li.clientHeight
|
||||
if (overflowBelow) {
|
||||
el.scrollTop = li.offsetTop + li.clientHeight - el.clientHeight
|
||||
}
|
||||
var overflowAbove = el.scrollTop > li.offsetTop
|
||||
if (overflowAbove) {
|
||||
el.scrollTop = li.offsetTop
|
||||
}
|
||||
}
|
||||
|
||||
handleArticleClick (article) {
|
||||
return (e) => {
|
||||
let { dispatch } = this.props
|
||||
dispatch(selectArticle(article.key))
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let articleElements = this.props.articles.map(function (article) {
|
||||
if (article == null) {
|
||||
return (
|
||||
<li className={isActive ? 'active' : ''}>
|
||||
<div className='articleItem'>Undefined</div>
|
||||
<div className='divider'/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
var isActive = this.props.activeArticle != null && (article.key === this.props.activeArticle.key)
|
||||
return (
|
||||
<li key={'article-' + article.key} onClick={this.handleArticleClick(article)} className={isActive ? 'active' : ''}>
|
||||
<div className='articleItem'>
|
||||
<ModeIcon mode={article.mode}/> {article.title}</div>
|
||||
<div className='divider'/>
|
||||
</li>
|
||||
)
|
||||
}.bind(this))
|
||||
|
||||
return (
|
||||
<div className='FinderList'>
|
||||
<ul>
|
||||
{articleElements}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FinderList.propTypes = {
|
||||
articles: PropTypes.array,
|
||||
activeArticle: PropTypes.shape({
|
||||
type: PropTypes.string,
|
||||
key: PropTypes.string
|
||||
}),
|
||||
dispatch: PropTypes.func
|
||||
}
|
||||
39
browser/finder/actions.js
Normal file
39
browser/finder/actions.js
Normal file
@@ -0,0 +1,39 @@
|
||||
export const SELECT_ARTICLE = 'SELECT_ARTICLE'
|
||||
export const SEARCH_ARTICLE = 'SEARCH_ARTICLE'
|
||||
export const REFRESH_DATA = 'REFRESH_DATA'
|
||||
|
||||
export function selectArticle (key) {
|
||||
return {
|
||||
type: SELECT_ARTICLE,
|
||||
data: { key }
|
||||
}
|
||||
}
|
||||
|
||||
export function searchArticle (input) {
|
||||
return {
|
||||
type: SEARCH_ARTICLE,
|
||||
data: { input }
|
||||
}
|
||||
}
|
||||
|
||||
export function refreshData (data) {
|
||||
console.log('refreshing data')
|
||||
let { folders, articles } = data
|
||||
|
||||
return {
|
||||
type: REFRESH_DATA,
|
||||
data: {
|
||||
articles,
|
||||
folders
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
SELECT_ARTICLE,
|
||||
SEARCH_ARTICLE,
|
||||
REFRESH_DATA,
|
||||
selectArticle,
|
||||
searchArticle,
|
||||
refreshData
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<title>CodeXen Popup</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
|
||||
|
||||
<link rel="stylesheet" href="../../node_modules/font-awesome/css/font-awesome.min.css" media="screen" title="no title" charset="utf-8">
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
src: url('../../Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
|
||||
url('../../Lato-Regular.woff') format('woff'), /* Modern Browsers */
|
||||
url('../../Lato-Regular.ttf') format('truetype');
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener('mousewheel', function(e) {
|
||||
if(e.deltaY % 1 !== 0) {
|
||||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
if (!Object.assign) {
|
||||
Object.defineProperty(Object, 'assign', {
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: function(target) {
|
||||
'use strict';
|
||||
if (target === undefined || target === null) {
|
||||
throw new TypeError('Cannot convert first argument to object');
|
||||
}
|
||||
|
||||
var to = Object(target);
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
var nextSource = arguments[i];
|
||||
if (nextSource === undefined || nextSource === null) {
|
||||
continue;
|
||||
}
|
||||
nextSource = Object(nextSource);
|
||||
|
||||
var keysArray = Object.keys(Object(nextSource));
|
||||
for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
|
||||
var nextKey = keysArray[nextIndex];
|
||||
var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
|
||||
if (desc !== undefined && desc.enumerable) {
|
||||
to[nextKey] = nextSource[nextKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
return to;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="content"></div>
|
||||
<script src="../ace/src-min/ace.js"></script>
|
||||
<script>
|
||||
require('../electron-stylus')(__dirname + '/../styles/finder/index.styl', 'finderCss')
|
||||
require('node-jsx').install({ harmony: true, extension: '.jsx' })
|
||||
require('./index.jsx')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
284
browser/finder/index.js
Normal file
284
browser/finder/index.js
Normal file
@@ -0,0 +1,284 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { connect, Provider } from 'react-redux'
|
||||
import reducer from './reducer'
|
||||
import { createStore } from 'redux'
|
||||
import FinderInput from './FinderInput'
|
||||
import FinderList from './FinderList'
|
||||
import FinderDetail from './FinderDetail'
|
||||
import actions, { selectArticle, searchArticle } from './actions'
|
||||
import _ from 'lodash'
|
||||
import dataStore from 'browser/lib/dataStore'
|
||||
import fetchConfig from '../lib/fetchConfig'
|
||||
const electron = require('electron')
|
||||
const { clipboard, ipcRenderer, remote } = electron
|
||||
const path = require('path')
|
||||
|
||||
let config = fetchConfig()
|
||||
applyConfig(config)
|
||||
|
||||
ipcRenderer.on('config-apply', function (e, newConfig) {
|
||||
config = newConfig
|
||||
applyConfig(config)
|
||||
})
|
||||
|
||||
function applyConfig () {
|
||||
let body = document.body
|
||||
body.setAttribute('data-theme', config['theme-ui'])
|
||||
|
||||
let hljsCss = document.getElementById('hljs-css')
|
||||
hljsCss.setAttribute('href', '../node_modules/highlight.js/styles/' + config['theme-code'] + '.css')
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
window.addEventListener('keydown', function (e) {
|
||||
if (e.keyCode === 73 && e.metaKey && e.altKey) {
|
||||
remote.getCurrentWindow().toggleDevTools()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function hideFinder () {
|
||||
ipcRenderer.send('hide-finder')
|
||||
}
|
||||
|
||||
function notify (title, options) {
|
||||
if (process.platform === 'win32') {
|
||||
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
||||
}
|
||||
return new window.Notification(title, options)
|
||||
}
|
||||
|
||||
require('../styles/finder/index.styl')
|
||||
|
||||
const FOLDER_FILTER = 'FOLDER_FILTER'
|
||||
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
|
||||
const TEXT_FILTER = 'TEXT_FILTER'
|
||||
const TAG_FILTER = 'TAG_FILTER'
|
||||
|
||||
class FinderMain extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.keyDownHandler = e => this.handleKeyDown(e)
|
||||
document.addEventListener('keydown', this.keyDownHandler)
|
||||
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
|
||||
this.focusHandler = e => {
|
||||
let { dispatch } = this.props
|
||||
|
||||
dispatch(searchArticle(''))
|
||||
dispatch(selectArticle(null))
|
||||
}
|
||||
window.addEventListener('focus', this.focusHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('keydown', this.keyDownHandler)
|
||||
window.removeEventListener('focus', this.focusHandler)
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
if (e.keyCode === 38) {
|
||||
this.selectPrevious()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
if (e.keyCode === 40) {
|
||||
this.selectNext()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
if (e.keyCode === 13) {
|
||||
this.saveToClipboard()
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.keyCode === 27) {
|
||||
hideFinder()
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.keyCode === 91 || e.metaKey) {
|
||||
return
|
||||
}
|
||||
|
||||
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
|
||||
}
|
||||
|
||||
saveToClipboard () {
|
||||
let { activeArticle } = this.props
|
||||
clipboard.writeText(activeArticle.content)
|
||||
|
||||
ipcRenderer.send('copy-finder')
|
||||
notify('Saved to Clipboard!', {
|
||||
body: 'Paste it wherever you want!',
|
||||
silent: true
|
||||
})
|
||||
hideFinder()
|
||||
}
|
||||
|
||||
handleSearchChange (e) {
|
||||
let { dispatch } = this.props
|
||||
|
||||
dispatch(searchArticle(e.target.value))
|
||||
}
|
||||
|
||||
selectArticle (article) {
|
||||
this.setState({currentArticle: article})
|
||||
}
|
||||
|
||||
selectPrevious () {
|
||||
let { activeArticle, dispatch } = this.props
|
||||
let index = this.refs.finderList.props.articles.indexOf(activeArticle)
|
||||
let previousArticle = this.refs.finderList.props.articles[index - 1]
|
||||
if (previousArticle != null) dispatch(selectArticle(previousArticle.key))
|
||||
}
|
||||
|
||||
selectNext () {
|
||||
let { activeArticle, dispatch } = this.props
|
||||
let index = this.refs.finderList.props.articles.indexOf(activeArticle)
|
||||
let previousArticle = this.refs.finderList.props.articles[index + 1]
|
||||
if (previousArticle != null) dispatch(selectArticle(previousArticle.key))
|
||||
}
|
||||
|
||||
render () {
|
||||
let { articles, activeArticle, status, dispatch } = this.props
|
||||
let saveToClipboard = () => this.saveToClipboard()
|
||||
return (
|
||||
<div className='Finder'>
|
||||
<FinderInput
|
||||
handleSearchChange={e => this.handleSearchChange(e)}
|
||||
ref='finderInput'
|
||||
onChange={this.handleChange}
|
||||
value={status.search}
|
||||
/>
|
||||
<FinderList
|
||||
ref='finderList'
|
||||
activeArticle={activeArticle}
|
||||
articles={articles}
|
||||
dispatch={dispatch}
|
||||
selectArticle={article => this.selectArticle(article)}
|
||||
/>
|
||||
<FinderDetail
|
||||
activeArticle={activeArticle}
|
||||
saveToClipboard={saveToClipboard}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FinderMain.propTypes = {
|
||||
articles: PropTypes.array,
|
||||
activeArticle: PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
tags: PropTypes.array,
|
||||
title: PropTypes.string,
|
||||
content: PropTypes.string
|
||||
}),
|
||||
status: PropTypes.shape(),
|
||||
dispatch: PropTypes.func
|
||||
}
|
||||
|
||||
// Ignore invalid key
|
||||
function ignoreInvalidKey (key) {
|
||||
return key.length > 0 && !key.match(/^\/\/$/) && !key.match(/^\/$/) && !key.match(/^#$/)
|
||||
}
|
||||
|
||||
// Build filter object by key
|
||||
function buildFilter (key) {
|
||||
if (key.match(/^\/\/.+/)) {
|
||||
return {type: FOLDER_EXACT_FILTER, value: key.match(/^\/\/(.+)$/)[1]}
|
||||
}
|
||||
if (key.match(/^\/.+/)) {
|
||||
return {type: FOLDER_FILTER, value: key.match(/^\/(.+)$/)[1]}
|
||||
}
|
||||
if (key.match(/^#(.+)/)) {
|
||||
return {type: TAG_FILTER, value: key.match(/^#(.+)$/)[1]}
|
||||
}
|
||||
return {type: TEXT_FILTER, value: key}
|
||||
}
|
||||
|
||||
function isContaining (target, needle) {
|
||||
return target.match(new RegExp(_.escapeRegExp(needle), 'i'))
|
||||
}
|
||||
|
||||
function startsWith (target, needle) {
|
||||
return target.match(new RegExp('^' + _.escapeRegExp(needle), 'i'))
|
||||
}
|
||||
|
||||
function remap (state) {
|
||||
let { articles, folders, status } = state
|
||||
|
||||
let filters = status.search.split(' ')
|
||||
.map(key => key.trim())
|
||||
.filter(ignoreInvalidKey)
|
||||
.map(buildFilter)
|
||||
|
||||
let folderExactFilters = filters.filter(filter => filter.type === FOLDER_EXACT_FILTER)
|
||||
let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER)
|
||||
let textFilters = filters.filter(filter => filter.type === TEXT_FILTER)
|
||||
let tagFilters = filters.filter(filter => filter.type === TAG_FILTER)
|
||||
|
||||
let targetFolders
|
||||
if (folders != null) {
|
||||
let exactTargetFolders = folders.filter(folder => {
|
||||
return _.find(folderExactFilters, filter => filter.value.toLowerCase() === folder.name.toLowerCase())
|
||||
})
|
||||
let fuzzyTargetFolders = folders.filter(folder => {
|
||||
return _.find(folderFilters, filter => startsWith(folder.name.replace(/_/g, ''), filter.value.replace(/_/g, '')))
|
||||
})
|
||||
targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders)
|
||||
|
||||
if (targetFolders.length > 0) {
|
||||
articles = articles.filter(article => {
|
||||
return _.findWhere(targetFolders, {key: article.FolderKey})
|
||||
})
|
||||
}
|
||||
|
||||
if (textFilters.length > 0) {
|
||||
articles = textFilters.reduce((articles, textFilter) => {
|
||||
return articles.filter(article => {
|
||||
return isContaining(article.title, textFilter.value) || isContaining(article.content, textFilter.value)
|
||||
})
|
||||
}, articles)
|
||||
}
|
||||
|
||||
if (tagFilters.length > 0) {
|
||||
articles = tagFilters.reduce((articles, tagFilter) => {
|
||||
return articles.filter(article => {
|
||||
return _.find(article.tags, tag => isContaining(tag, tagFilter.value))
|
||||
})
|
||||
}, articles)
|
||||
}
|
||||
}
|
||||
|
||||
let activeArticle = _.findWhere(articles, {key: status.articleKey})
|
||||
if (activeArticle == null) activeArticle = articles[0]
|
||||
|
||||
return {
|
||||
articles,
|
||||
activeArticle,
|
||||
status
|
||||
}
|
||||
}
|
||||
|
||||
var Finder = connect(remap)(FinderMain)
|
||||
var store = createStore(reducer)
|
||||
|
||||
function refreshData () {
|
||||
let data = dataStore.getData(true)
|
||||
store.dispatch(actions.refreshData(data))
|
||||
}
|
||||
|
||||
window.onfocus = e => {
|
||||
refreshData()
|
||||
}
|
||||
|
||||
ReactDOM.render((
|
||||
<Provider store={store}>
|
||||
<Finder/>
|
||||
</Provider>
|
||||
), document.getElementById('content'), function () {
|
||||
refreshData()
|
||||
})
|
||||
@@ -1,135 +0,0 @@
|
||||
/* global localStorage */
|
||||
var remote = require('remote')
|
||||
var hideFinder = remote.getGlobal('hideFinder')
|
||||
var clipboard = require('clipboard')
|
||||
|
||||
var React = require('react/addons')
|
||||
|
||||
var ArticleFilter = require('../main/Mixins/ArticleFilter')
|
||||
|
||||
var FinderInput = require('./Components/FinderInput')
|
||||
var FinderList = require('./Components/FinderList')
|
||||
var FinderDetail = require('./Components/FinderDetail')
|
||||
|
||||
// Filter end
|
||||
|
||||
function fetchArticles () {
|
||||
var user = JSON.parse(localStorage.getItem('currentUser'))
|
||||
if (user == null) {
|
||||
console.log('need to login')
|
||||
return []
|
||||
}
|
||||
|
||||
var articles = []
|
||||
user.Planets.forEach(function (planet) {
|
||||
var _planet = JSON.parse(localStorage.getItem('planet-' + planet.id))
|
||||
articles = articles.concat(_planet.Codes, _planet.Notes)
|
||||
})
|
||||
user.Teams.forEach(function (team) {
|
||||
team.Planets.forEach(function (planet) {
|
||||
var _planet = JSON.parse(localStorage.getItem('planet-' + planet.id))
|
||||
articles = articles.concat(_planet.Codes, _planet.Notes)
|
||||
})
|
||||
})
|
||||
|
||||
return articles
|
||||
}
|
||||
|
||||
var Finder = React.createClass({
|
||||
mixins: [ArticleFilter],
|
||||
getInitialState: function () {
|
||||
var articles = fetchArticles()
|
||||
return {
|
||||
articles: articles,
|
||||
currentArticle: articles[0],
|
||||
search: ''
|
||||
}
|
||||
},
|
||||
componentDidMount: function () {
|
||||
document.addEventListener('keydown', this.handleKeyDown)
|
||||
document.addEventListener('click', this.handleClick)
|
||||
window.addEventListener('focus', this.handleFinderFocus)
|
||||
this.handleFinderFocus()
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
document.removeEventListener('keydown', this.handleKeyDown)
|
||||
document.removeEventListener('click', this.handleClick)
|
||||
window.removeEventListener('focus', this.handleFinderFocus)
|
||||
},
|
||||
handleFinderFocus: function () {
|
||||
console.log('focusseeddddd')
|
||||
this.focusInput()
|
||||
var articles = fetchArticles()
|
||||
this.setState({
|
||||
articles: articles,
|
||||
search: ''
|
||||
}, function () {
|
||||
var firstArticle = this.refs.finderList.props.articles[0]
|
||||
if (firstArticle) {
|
||||
this.setState({
|
||||
currentArticle: firstArticle
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
handleKeyDown: function (e) {
|
||||
if (e.keyCode === 38) {
|
||||
this.selectPrevious()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
if (e.keyCode === 40) {
|
||||
this.selectNext()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
if (e.keyCode === 13) {
|
||||
var article = this.state.currentArticle
|
||||
clipboard.writeText(article.content)
|
||||
hideFinder()
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.keyCode === 27) {
|
||||
hideFinder()
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
focusInput: function () {
|
||||
React.findDOMNode(this.refs.finderInput).querySelector('input').focus()
|
||||
},
|
||||
handleClick: function () {
|
||||
this.focusInput()
|
||||
},
|
||||
selectPrevious: function () {
|
||||
var index = this.refs.finderList.props.articles.indexOf(this.state.currentArticle)
|
||||
if (index > 0) {
|
||||
this.setState({currentArticle: this.refs.finderList.props.articles[index - 1]})
|
||||
}
|
||||
},
|
||||
selectNext: function () {
|
||||
var index = this.refs.finderList.props.articles.indexOf(this.state.currentArticle)
|
||||
if (index > -1 && index < this.refs.finderList.props.articles.length - 1) {
|
||||
this.setState({currentArticle: this.refs.finderList.props.articles[index + 1]})
|
||||
}
|
||||
},
|
||||
selectArticle: function (article) {
|
||||
this.setState({currentArticle: article})
|
||||
},
|
||||
handleChange: function (e) {
|
||||
this.setState({search: e.target.value}, function () {
|
||||
this.setState({currentArticle: this.refs.finderList.props.articles[0]})
|
||||
})
|
||||
},
|
||||
render: function () {
|
||||
var articles = this.searchArticle(this.state.search, this.state.articles)
|
||||
return (
|
||||
<div className='Finder'>
|
||||
<FinderInput ref='finderInput' onChange={this.handleChange} search={this.state.search}/>
|
||||
<FinderList ref='finderList' currentArticle={this.state.currentArticle} articles={articles} selectArticle={this.selectArticle}/>
|
||||
<FinderDetail currentArticle={this.state.currentArticle}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
React.render(<Finder/>, document.getElementById('content'))
|
||||
47
browser/finder/reducer.js
Normal file
47
browser/finder/reducer.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { combineReducers } from 'redux'
|
||||
import { SELECT_ARTICLE, SEARCH_ARTICLE, REFRESH_DATA } from './actions'
|
||||
|
||||
let initialArticles = []
|
||||
let initialFolders = []
|
||||
let initialStatus = {
|
||||
articleKey: null,
|
||||
search: ''
|
||||
}
|
||||
|
||||
function status (state = initialStatus, action) {
|
||||
switch (action.type) {
|
||||
case SELECT_ARTICLE:
|
||||
state.articleKey = action.data.key
|
||||
return Object.assign({}, state)
|
||||
case SEARCH_ARTICLE:
|
||||
state.search = action.data.input
|
||||
return Object.assign({}, state)
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function articles (state = initialArticles, action) {
|
||||
switch (action.type) {
|
||||
case REFRESH_DATA:
|
||||
return action.data.articles
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function folders (state = initialFolders, action) {
|
||||
switch (action.type) {
|
||||
case REFRESH_DATA:
|
||||
console.log(action)
|
||||
return action.data.folders
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
status,
|
||||
folders,
|
||||
articles
|
||||
})
|
||||
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/main">Go Main</a>
|
||||
<a href="/main">Go Popup</a>
|
||||
</body>
|
||||
</html>
|
||||
138
browser/lib/activityRecord.js
Normal file
138
browser/lib/activityRecord.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
import dataStore from './dataStore'
|
||||
import { request, SERVER_URL } from './api'
|
||||
import clientKey from './clientKey'
|
||||
|
||||
const electron = require('electron')
|
||||
const version = electron.remote.app.getVersion()
|
||||
|
||||
function isSameDate (a, b) {
|
||||
a = moment(a).utcOffset(+540).format('YYYYMMDD')
|
||||
b = moment(b).utcOffset(+540).format('YYYYMMDD')
|
||||
|
||||
return a === b
|
||||
}
|
||||
|
||||
export function init () {
|
||||
let records = getAllRecords()
|
||||
if (records == null) {
|
||||
saveAllRecords([])
|
||||
}
|
||||
emit(null)
|
||||
|
||||
postRecords()
|
||||
if (window != null) {
|
||||
window.addEventListener('online', postRecords)
|
||||
window.setInterval(postRecords, 1000 * 60 * 60 * 24)
|
||||
}
|
||||
}
|
||||
|
||||
export function getAllRecords () {
|
||||
return JSON.parse(localStorage.getItem('activityRecords'))
|
||||
}
|
||||
|
||||
export function saveAllRecords (records) {
|
||||
localStorage.setItem('activityRecords', JSON.stringify(records))
|
||||
}
|
||||
|
||||
/*
|
||||
Post all records(except today)
|
||||
and remove all posted records
|
||||
*/
|
||||
export function postRecords (data) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log('post failed - NOT PRODUCTION ')
|
||||
return
|
||||
}
|
||||
|
||||
let records = getAllRecords()
|
||||
records = records.filter(record => {
|
||||
return !isSameDate(new Date(), record.date)
|
||||
})
|
||||
|
||||
if (records.length === 0) {
|
||||
console.log('No records to post')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('posting...', records)
|
||||
let input = {
|
||||
clientKey: clientKey.get(),
|
||||
records
|
||||
}
|
||||
return request.post(SERVER_URL + 'apis/activity')
|
||||
.send(input)
|
||||
.then(res => {
|
||||
let records = getAllRecords()
|
||||
let todayRecord = _.find(records, record => {
|
||||
return isSameDate(new Date(), record.date)
|
||||
})
|
||||
if (todayRecord != null) saveAllRecords([todayRecord])
|
||||
else saveAllRecords([])
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
export function emit (type, data = {}) {
|
||||
let records = getAllRecords()
|
||||
|
||||
let index = _.findIndex(records, record => {
|
||||
return isSameDate(new Date(), record.date)
|
||||
})
|
||||
|
||||
let todayRecord
|
||||
if (index < 0) {
|
||||
todayRecord = {date: new Date()}
|
||||
records.push(todayRecord)
|
||||
}
|
||||
else todayRecord = records[index]
|
||||
switch (type) {
|
||||
case 'ARTICLE_CREATE':
|
||||
case 'ARTICLE_UPDATE':
|
||||
case 'ARTICLE_DESTROY':
|
||||
case 'FOLDER_CREATE':
|
||||
case 'FOLDER_UPDATE':
|
||||
case 'FOLDER_DESTROY':
|
||||
case 'FINDER_OPEN':
|
||||
case 'FINDER_COPY':
|
||||
case 'MAIN_DETAIL_COPY':
|
||||
case 'ARTICLE_SHARE':
|
||||
todayRecord[type] = todayRecord[type] == null
|
||||
? 1
|
||||
: todayRecord[type] + 1
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
// Count ARTICLE_CREATE and ARTICLE_UPDATE again by syntax
|
||||
if (type === 'ARTICLE_UPDATE' && data.mode != null) {
|
||||
let recordKey = type + '_BY_SYNTAX'
|
||||
if (todayRecord[recordKey] == null) todayRecord[recordKey] = {}
|
||||
|
||||
todayRecord[recordKey][data.mode] = todayRecord[recordKey][data.mode] == null
|
||||
? 1
|
||||
: todayRecord[recordKey][data.mode] + 1
|
||||
}
|
||||
|
||||
let storeData = dataStore.getData()
|
||||
todayRecord.FOLDER_COUNT = storeData && _.isArray(storeData.folders) ? storeData.folders.length : 0
|
||||
todayRecord.ARTICLE_COUNT = storeData && _.isArray(storeData.articles) ? storeData.articles.length : 0
|
||||
todayRecord.CLIENT_VERSION = version
|
||||
|
||||
todayRecord.SYNTAX_COUNT = storeData && _.isArray(storeData.articles) ? storeData.articles.reduce((sum, article) => {
|
||||
if (sum[article.mode] == null) sum[article.mode] = 1
|
||||
else sum[article.mode]++
|
||||
return sum
|
||||
}, {}) : 0
|
||||
|
||||
saveAllRecords(records)
|
||||
}
|
||||
|
||||
export default {
|
||||
init,
|
||||
emit,
|
||||
postRecords
|
||||
}
|
||||
21
browser/lib/api.js
Normal file
21
browser/lib/api.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import superagent from 'superagent'
|
||||
import superagentPromise from 'superagent-promise'
|
||||
|
||||
export const SERVER_URL = 'https://b00st.io/'
|
||||
// export const SERVER_URL = 'http://localhost:3333/'
|
||||
|
||||
export const request = superagentPromise(superagent, Promise)
|
||||
|
||||
export function shareViaPublicURL (input) {
|
||||
return request
|
||||
.post(SERVER_URL + 'apis/share')
|
||||
// .set({
|
||||
// Authorization: 'Bearer ' + auth.token()
|
||||
// })
|
||||
.send(input)
|
||||
}
|
||||
|
||||
export default {
|
||||
SERVER_URL,
|
||||
shareViaPublicURL
|
||||
}
|
||||
23
browser/lib/clientKey.js
Normal file
23
browser/lib/clientKey.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import _ from 'lodash'
|
||||
import keygen from './keygen'
|
||||
|
||||
function getClientKey () {
|
||||
let clientKey = localStorage.getItem('clientKey')
|
||||
if (!_.isString(clientKey) || clientKey.length !== 40) {
|
||||
clientKey = keygen()
|
||||
setClientKey(clientKey)
|
||||
}
|
||||
|
||||
return clientKey
|
||||
}
|
||||
|
||||
function setClientKey (newKey) {
|
||||
localStorage.setItem('clientKey', newKey)
|
||||
}
|
||||
|
||||
getClientKey()
|
||||
|
||||
export default {
|
||||
get: getClientKey,
|
||||
set: setClientKey
|
||||
}
|
||||
156
browser/lib/dataStore.js
Normal file
156
browser/lib/dataStore.js
Normal file
@@ -0,0 +1,156 @@
|
||||
import keygen from './keygen'
|
||||
import _ from 'lodash'
|
||||
|
||||
const electron = require('electron')
|
||||
const remote = electron.remote
|
||||
const jetpack = require('fs-jetpack')
|
||||
const path = require('path')
|
||||
|
||||
let defaultContent = 'Boost is a brand new note App for programmers.\n\n> 下に日本語版があります。\n\n# \u25CEfeature\n\nBoost has some preponderant functions for efficient engineer\'s task.See some part of it.\n\n1. classify information by\u300CFolders\u300D\n2. deal with great variety of syntax\n3. Finder function\n\n\uFF0A\u3000\uFF0A\u3000\uFF0A\u3000\uFF0A\n\n# 1. classify information by \u300CFolders\u300D- access the information you needed easily.\n\n\u300CFolders\u300D which on the left side bar. Press plus button now. flexible way of classification.\n- Create Folder every language or flamework\n- Make Folder for your own casual memos\n\n# 2. Deal with a great variety of syntax \u2013 instead of your brain\nSave handy all information related with programming\n- Use markdown and gather api specification\n- Well using module and snippet\n\nSave them on Boost, you don\'t need to rewrite or re-search same code again.\n\n# 3. Load Finder function \u2013 now you don\'t need to spell command by hand typing.\n\n**Shift +ctrl+tab** press buttons at same time.\nThen, the window will show up for search Boost contents that instant.\n\nUsing cursor key to chose, press enter, cmd+v to paste and\u2026 please check it out by your own eye.\n\n- Such command spl or linux which programmers often use but troublesome to hand type\n\n- (Phrases commonly used for e-mail or customer support)\n\nWe support preponderant efficiency\n\n\uFF0A\u3000\uFF0A\u3000\uFF0A\u3000\uFF0A\n\n## \u25CEfor more information\nFrequently updated with this blog ( http:\/\/blog-jp.b00st.io )\n\nHave wonderful programmer life!\n\n## Hack your memory**\n\n\n\n# 日本語版\n\n**Boost**は全く新しいエンジニアライクのノートアプリです。\n\n# ◎特徴\nBoostはエンジニアの仕事を圧倒的に効率化するいくつかの機能を備えています。\nその一部をご紹介します。\n1. Folderで情報を分類\n2. 豊富なsyantaxに対応\n3. Finder機能\n\n\n* * * *\n\n# 1. Folderで情報を分類、欲しい情報にすぐアクセス。\n左側のバーに存在する「Folders」。\n今すぐプラスボタンを押しましょう。\n分類の仕方も自由自在です。\n- 言語やフレームワークごとにFolderを作成\n- 自分用のカジュアルなメモをまとめる場としてFolderを作成\n\n\n# 2. 豊富なsyntaxに対応、自分の脳の代わりに。\nプログラミングに関する情報を全て、手軽に保存しましょう。\n- mdで、apiの仕様をまとめる\n- よく使うモジュールやスニペット\n\nBoostに保存しておくことで、何度も同じコードを書いたり調べたりする必要がなくなります。\n\n# 3. Finder機能を搭載、もうコマンドを手打ちする必要はありません。\n**「shift+ctrl+tab」** を同時に押してみてください。\nここでは、一瞬でBoostの中身を検索するウィンドウを表示させることができます。\n\n矢印キーで選択、Enterを押し、cmd+vでペーストすると…続きはご自身の目でお確かめください。\n- sqlやlinux等の、よく使うが手打ちが面倒なコマンド\n- (メールやカスタマーサポート等でよく使うフレーズ)\n\n私たちは、圧倒的な効率性を支援します。\n\* * * *\n\n\n## ◎詳しくは\nこちらのブログ( http://blog-jp.b00st.io )にて随時更新しています。\n\nそれでは素晴らしいエンジニアライフを!\n\n## Hack your memory**'
|
||||
|
||||
let data = null
|
||||
|
||||
function getLocalPath () {
|
||||
return path.join(remote.app.getPath('userData'), 'local.json')
|
||||
}
|
||||
|
||||
function forgeInitialRepositories () {
|
||||
let defaultRepo = {
|
||||
key: keygen(),
|
||||
name: 'local',
|
||||
type: 'userData',
|
||||
user: {
|
||||
name: 'New user'
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
defaultRepo.user.name = remote.process.env.USER
|
||||
} else if (process.platform === 'win32') {
|
||||
defaultRepo.user.name = remote.process.env.USERNAME
|
||||
}
|
||||
|
||||
return [defaultRepo]
|
||||
}
|
||||
|
||||
function getRepositories () {
|
||||
let raw = localStorage.getItem('repositories')
|
||||
try {
|
||||
let parsed = JSON.parse(raw)
|
||||
if (!_.isArray(parsed)) {
|
||||
throw new Error('repositories data is corrupted. re-init data.')
|
||||
}
|
||||
return parsed
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
let newRepos = forgeInitialRepositories()
|
||||
saveRepositories(newRepos)
|
||||
return newRepos
|
||||
}
|
||||
}
|
||||
|
||||
function saveRepositories (repos) {
|
||||
localStorage.setItem('repositories', JSON.stringify(repos))
|
||||
}
|
||||
|
||||
export function getUser (repoName) {
|
||||
if (repoName == null) {
|
||||
return getRepositories()[0]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function saveUser (repoName, user) {
|
||||
let repos = getRepositories()
|
||||
if (repoName == null) {
|
||||
Object.assign(repos[0].user, user)
|
||||
}
|
||||
saveRepositories(repos)
|
||||
}
|
||||
|
||||
export function init () {
|
||||
// set repositories info
|
||||
getRepositories()
|
||||
data = jetpack.read(getLocalPath(), 'json')
|
||||
if (data == null) {
|
||||
let defaultFolder = {
|
||||
name: 'default',
|
||||
key: keygen()
|
||||
}
|
||||
let defaultArticle = {
|
||||
title: 'About Boost',
|
||||
tags: ['boost', 'intro'],
|
||||
content: defaultContent,
|
||||
mode: 'markdown',
|
||||
key: keygen(),
|
||||
FolderKey: defaultFolder.key,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
|
||||
data = {
|
||||
articles: [defaultArticle],
|
||||
folders: [defaultFolder],
|
||||
version: '0.4'
|
||||
}
|
||||
saveData()
|
||||
}
|
||||
}
|
||||
|
||||
export function getData (forceRead) {
|
||||
if (forceRead) {
|
||||
try {
|
||||
data = jetpack.read(getLocalPath(), 'json')
|
||||
} catch (e) {}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
let timer = null
|
||||
let isSaving = false
|
||||
let saveAgain = false
|
||||
function saveData () {
|
||||
timer = null
|
||||
isSaving = true
|
||||
jetpack.writeAsync(getLocalPath(), data)
|
||||
.then(function () {
|
||||
isSaving = false
|
||||
if (saveAgain) {
|
||||
saveAgain = false
|
||||
queueSave()
|
||||
}
|
||||
})
|
||||
}
|
||||
function queueSave () {
|
||||
if (!isSaving) {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
timer = setTimeout(saveData, 500)
|
||||
} else {
|
||||
saveAgain = true
|
||||
}
|
||||
}
|
||||
|
||||
export function setArticles (articles) {
|
||||
if (!_.isArray(articles)) throw new Error('Articles must be an array')
|
||||
let data = getData()
|
||||
data.articles = articles
|
||||
queueSave()
|
||||
}
|
||||
|
||||
export function setFolders (folders) {
|
||||
if (!_.isArray(folders)) throw new Error('Folders must be an array')
|
||||
let data = getData()
|
||||
data.folders = folders
|
||||
queueSave()
|
||||
}
|
||||
|
||||
export default {
|
||||
getUser,
|
||||
saveUser,
|
||||
init,
|
||||
getData,
|
||||
setArticles,
|
||||
setFolders
|
||||
}
|
||||
24
browser/lib/fetchConfig.js
Normal file
24
browser/lib/fetchConfig.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const electron = require('electron')
|
||||
const remote = electron.remote
|
||||
const jetpack = require('fs-jetpack')
|
||||
|
||||
const userDataPath = remote.app.getPath('userData')
|
||||
const configFile = 'config.json'
|
||||
|
||||
const defaultConfig = {
|
||||
'editor-font-size': '14',
|
||||
'editor-font-family': 'Monaco, Consolas',
|
||||
'editor-indent-type': 'space',
|
||||
'editor-indent-size': '4',
|
||||
'preview-font-size': '14',
|
||||
'preview-font-family': 'Lato',
|
||||
'switch-preview': 'blur',
|
||||
'disable-direct-write': false,
|
||||
'theme-ui': 'light',
|
||||
'theme-code': 'xcode',
|
||||
'theme-syntax': 'xcode'
|
||||
}
|
||||
|
||||
export default function fetchConfig () {
|
||||
return Object.assign({}, defaultConfig, JSON.parse(jetpack.cwd(userDataPath).read(configFile, 'utf-8')))
|
||||
}
|
||||
78
browser/lib/hljsThemes.js
Normal file
78
browser/lib/hljsThemes.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const hljsThemeList = [
|
||||
{caption: 'Default', name: 'default'},
|
||||
{caption: 'Agate', name: 'agate'},
|
||||
{caption: 'Androidstudio', name: 'androidstudio'},
|
||||
{caption: 'Arduino Light', name: 'arduino-light'},
|
||||
{caption: 'Arta', name: 'arta'},
|
||||
{caption: 'Ascetic', name: 'ascetic'},
|
||||
{caption: 'Atelier Cave Dark', name: 'atelier-cave-dark'},
|
||||
{caption: 'Atelier Cave Light', name: 'atelier-cave-light'},
|
||||
{caption: 'Atelier Dune Dark', name: 'atelier-dune-dark'},
|
||||
{caption: 'Atelier Dune Light', name: 'atelier-dune-light'},
|
||||
{caption: 'Atelier Estuary Dark', name: 'atelier-estuary-dark'},
|
||||
{caption: 'Atelier Estuary Light', name: 'atelier-estuary-light'},
|
||||
{caption: 'Atelier Forest Dark', name: 'atelier-forest-dark'},
|
||||
{caption: 'Atelier Forest Light', name: 'atelier-forest-light'},
|
||||
{caption: 'Atelier Heath Dark', name: 'atelier-heath-dark'},
|
||||
{caption: 'Atelier Heath Light', name: 'atelier-heath-light'},
|
||||
{caption: 'Atelier Lakeside Dark', name: 'atelier-lakeside-dark'},
|
||||
{caption: 'Atelier Lakeside Light', name: 'atelier-lakeside-light'},
|
||||
{caption: 'Atelier Plateau Dark', name: 'atelier-plateau-dark'},
|
||||
{caption: 'Atelier Plateau Light', name: 'atelier-plateau-light'},
|
||||
{caption: 'Atelier Savanna Dark', name: 'atelier-savanna-dark'},
|
||||
{caption: 'Atelier Savanna Light', name: 'atelier-savanna-light'},
|
||||
{caption: 'Atelier Seaside Dark', name: 'atelier-seaside-dark'},
|
||||
{caption: 'Atelier Seaside Light', name: 'atelier-seaside-light'},
|
||||
{caption: 'Atelier Sulphurpool Dark', name: 'atelier-sulphurpool-dark'},
|
||||
{caption: 'Atelier Sulphurpool Light', name: 'atelier-sulphurpool-light'},
|
||||
{caption: 'Brown Paper', name: 'brown-paper'},
|
||||
{caption: 'Codepen Embed', name: 'codepen-embed'},
|
||||
{caption: 'Color Brewer', name: 'color-brewer'},
|
||||
{caption: 'Dark', name: 'dark'},
|
||||
{caption: 'Darkula', name: 'darkula'},
|
||||
{caption: 'Docco', name: 'docco'},
|
||||
{caption: 'Dracula', name: 'dracula'},
|
||||
{caption: 'Far', name: 'far'},
|
||||
{caption: 'Foundation', name: 'foundation'},
|
||||
{caption: 'Github Gist', name: 'github-gist'},
|
||||
{caption: 'Github', name: 'github'},
|
||||
{caption: 'Googlecode', name: 'googlecode'},
|
||||
{caption: 'Grayscale', name: 'grayscale'},
|
||||
{caption: 'Gruvbox Dark', name: 'gruvbox.dark'},
|
||||
{caption: 'Gruvbox Light', name: 'gruvbox.light'},
|
||||
{caption: 'Hopscotch', name: 'hopscotch'},
|
||||
{caption: 'Hybrid', name: 'hybrid'},
|
||||
{caption: 'Idea', name: 'idea'},
|
||||
{caption: 'Ir Black', name: 'ir-black'},
|
||||
{caption: 'Kimbie Dark', name: 'kimbie.dark'},
|
||||
{caption: 'Kimbie Light', name: 'kimbie.light'},
|
||||
{caption: 'Magula', name: 'magula'},
|
||||
{caption: 'Mono Blue', name: 'mono-blue'},
|
||||
{caption: 'Monokai Sublime', name: 'monokai-sublime'},
|
||||
{caption: 'Monokai', name: 'monokai'},
|
||||
{caption: 'Obsidian', name: 'obsidian'},
|
||||
{caption: 'Paraiso Dark', name: 'paraiso-dark'},
|
||||
{caption: 'Paraiso Light', name: 'paraiso-light'},
|
||||
{caption: 'Pojoaque', name: 'pojoaque'},
|
||||
{caption: 'Qtcreator Dark', name: 'qtcreator_dark'},
|
||||
{caption: 'Qtcreator Light', name: 'qtcreator_light'},
|
||||
{caption: 'Railscasts', name: 'railscasts'},
|
||||
{caption: 'Rainbow', name: 'rainbow'},
|
||||
{caption: 'School Book', name: 'school-book'},
|
||||
{caption: 'Solarized Dark', name: 'solarized-dark'},
|
||||
{caption: 'Solarized Light', name: 'solarized-light'},
|
||||
{caption: 'Sunburst', name: 'sunburst'},
|
||||
{caption: 'Tomorrow Night Blue', name: 'tomorrow-night-blue'},
|
||||
{caption: 'Tomorrow Night Bright', name: 'tomorrow-night-bright'},
|
||||
{caption: 'Tomorrow Night Eighties', name: 'tomorrow-night-eighties'},
|
||||
{caption: 'Tomorrow Night', name: 'tomorrow-night'},
|
||||
{caption: 'Tomorrow', name: 'tomorrow'},
|
||||
{caption: 'Vs', name: 'vs'},
|
||||
{caption: 'Xcode', name: 'xcode'},
|
||||
{caption: 'Xt 256', name: 'xt256'},
|
||||
{caption: 'Zenburn', name: 'zenburn'}
|
||||
]
|
||||
|
||||
export default function hljsTheme () {
|
||||
return hljsThemeList
|
||||
}
|
||||
7
browser/lib/keygen.js
Normal file
7
browser/lib/keygen.js
Normal file
@@ -0,0 +1,7 @@
|
||||
var crypto = require('crypto')
|
||||
|
||||
module.exports = function () {
|
||||
var shasum = crypto.createHash('sha1')
|
||||
shasum.update(((new Date()).getTime() + Math.round(Math.random()*1000)).toString())
|
||||
return shasum.digest('hex')
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
function getIn (object, path) {
|
||||
var stack = path.split('.')
|
||||
let stack = path.split('.')
|
||||
while (stack.length > 1) {
|
||||
object = object[stack.shift()]
|
||||
}
|
||||
@@ -7,8 +7,8 @@ function getIn (object, path) {
|
||||
}
|
||||
|
||||
function updateIn (object, path, value) {
|
||||
var current = object
|
||||
var stack = path.split('.')
|
||||
let current = object
|
||||
let stack = path.split('.')
|
||||
while (stack.length > 1) {
|
||||
current = current[stack.shift()]
|
||||
}
|
||||
@@ -21,11 +21,16 @@ function setPartialState (component, path, value) {
|
||||
updateIn(component.state, path, value))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
linkState: function (path) {
|
||||
return {
|
||||
value: getIn(this.state, path),
|
||||
requestChange: setPartialState.bind(null, this, path)
|
||||
}
|
||||
export default function linkState (path) {
|
||||
return {
|
||||
value: getIn(this.state, path),
|
||||
requestChange: setPartialState.bind(null, this, path)
|
||||
}
|
||||
}
|
||||
|
||||
export function linkState2 (el, path) {
|
||||
return {
|
||||
value: getIn(el.state, path),
|
||||
requestChange: setPartialState.bind(null, el, path)
|
||||
}
|
||||
}
|
||||
52
browser/lib/markdown.js
Normal file
52
browser/lib/markdown.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import markdownit from 'markdown-it'
|
||||
import emoji from 'markdown-it-emoji'
|
||||
import math from '@rokt33r/markdown-it-math'
|
||||
import hljs from 'highlight.js'
|
||||
|
||||
var md = markdownit({
|
||||
typographer: true,
|
||||
linkify: true,
|
||||
html: true,
|
||||
xhtmlOut: true,
|
||||
highlight: function (str, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return '<pre class="hljs"><code>' +
|
||||
hljs.highlight(lang, str).value +
|
||||
'</code></pre>'
|
||||
} catch (e) {}
|
||||
}
|
||||
return '<pre class="hljs"><code>' +
|
||||
str.replace(/\&/g, '&').replace(/\</g, '<').replace(/\>/g, '>').replace(/\"/g, '"') +
|
||||
'</code></pre>'
|
||||
}
|
||||
})
|
||||
md.use(emoji, {
|
||||
shortcuts: {}
|
||||
})
|
||||
md.use(math, {
|
||||
inlineRenderer: function (str) {
|
||||
return `<span class='math'>${str}</span>`
|
||||
},
|
||||
blockRenderer: function (str) {
|
||||
return `<div class='math'>${str}</div>`
|
||||
}
|
||||
})
|
||||
md.use(require('markdown-it-checkbox'))
|
||||
|
||||
let originalRenderToken = md.renderer.renderToken
|
||||
md.renderer.renderToken = function renderToken (tokens, idx, options) {
|
||||
let token = tokens[idx]
|
||||
|
||||
let result = originalRenderToken.call(md.renderer, tokens, idx, options)
|
||||
if (token.map != null) {
|
||||
return result + '<a class=\'lineAnchor\' data-key=\'' + token.map[0] + '\'></a>'
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export default function markdown (content) {
|
||||
if (content == null) content = ''
|
||||
|
||||
return md.render(content.toString())
|
||||
}
|
||||
56
browser/lib/modal.js
Normal file
56
browser/lib/modal.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
const remote = require('electron').remote
|
||||
|
||||
class ModalBase extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
component: null,
|
||||
componentProps: {},
|
||||
isHidden: true
|
||||
}
|
||||
}
|
||||
|
||||
close () {
|
||||
if (modalBase != null) modalBase.setState({component: null, componentProps: null, isHidden: true})
|
||||
|
||||
remote.getCurrentWebContents().send('list-focus')
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className={'ModalBase' + (this.state.isHidden ? ' hide' : '')}>
|
||||
<div onClick={e => this.close(e)} className='modalBack'/>
|
||||
{this.state.component == null ? null : (
|
||||
<this.state.component {...this.state.componentProps} close={this.close}/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let el = document.createElement('div')
|
||||
document.body.appendChild(el)
|
||||
let modalBase = ReactDOM.render(<ModalBase/>, el)
|
||||
|
||||
export function openModal (component, props) {
|
||||
if (modalBase == null) { return }
|
||||
modalBase.setState({component: component, componentProps: props, isHidden: false})
|
||||
}
|
||||
|
||||
export function closeModal () {
|
||||
if (modalBase == null) { return }
|
||||
modalBase.close()
|
||||
}
|
||||
|
||||
export function isModalOpen () {
|
||||
return !modalBase.state.isHidden
|
||||
}
|
||||
|
||||
export default {
|
||||
open: openModal,
|
||||
close: closeModal,
|
||||
isOpen: isModalOpen
|
||||
}
|
||||
756
browser/lib/modes.js
Normal file
756
browser/lib/modes.js
Normal file
@@ -0,0 +1,756 @@
|
||||
const modes = [
|
||||
// Major
|
||||
{
|
||||
name: 'text',
|
||||
label: 'Plain text',
|
||||
mode: 'text'
|
||||
},
|
||||
{
|
||||
name: 'markdown',
|
||||
label: 'Markdown',
|
||||
alias: ['md'],
|
||||
mode: 'markdown'
|
||||
},
|
||||
{
|
||||
name: 'javascript',
|
||||
label: 'JavaScript',
|
||||
alias: ['js', 'jscript', 'babel', 'es'],
|
||||
mode: 'javascript'
|
||||
},
|
||||
{
|
||||
name: 'html',
|
||||
label: 'HTML',
|
||||
alias: [],
|
||||
mode: 'html'
|
||||
},
|
||||
{
|
||||
name: 'css',
|
||||
label: 'CSS',
|
||||
alias: ['cascade', 'stylesheet'],
|
||||
mode: 'css'
|
||||
},
|
||||
{
|
||||
name: 'php',
|
||||
label: 'PHP',
|
||||
alias: [],
|
||||
mode: 'php'
|
||||
},
|
||||
{
|
||||
name: 'python',
|
||||
label: 'Python',
|
||||
alias: ['py'],
|
||||
mode: 'python'
|
||||
},
|
||||
{
|
||||
name: 'ruby',
|
||||
label: 'Ruby',
|
||||
alias: ['rb'],
|
||||
mode: 'ruby'
|
||||
},
|
||||
{
|
||||
name: 'java',
|
||||
label: 'Java',
|
||||
alias: [],
|
||||
mode: 'java'
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
label: 'C',
|
||||
alias: ['c', 'h', 'clang', 'clang'],
|
||||
mode: 'c_cpp'
|
||||
},
|
||||
{
|
||||
name: 'cpp',
|
||||
label: 'C++',
|
||||
alias: ['cc', 'cpp', 'cxx', 'hh', 'c++', 'cplusplus'],
|
||||
mode: 'c_cpp'
|
||||
},
|
||||
{
|
||||
name: 'csharp',
|
||||
label: 'C#',
|
||||
alias: ['cs', 'c#'],
|
||||
mode: 'csharp'
|
||||
},
|
||||
{
|
||||
name: 'swift',
|
||||
label: 'Swift',
|
||||
alias: [],
|
||||
mode: 'swift'
|
||||
},
|
||||
{
|
||||
name: 'golang',
|
||||
label: 'Go',
|
||||
alias: ['go'],
|
||||
mode: 'golang'
|
||||
},
|
||||
|
||||
// Minor
|
||||
{
|
||||
name: 'abap',
|
||||
label: 'ABAP',
|
||||
alias: [],
|
||||
mode: 'abap'
|
||||
},
|
||||
{
|
||||
name: 'abc',
|
||||
label: 'ABC',
|
||||
alias: [],
|
||||
mode: 'abc'
|
||||
},
|
||||
{
|
||||
name: 'actionscript',
|
||||
label: 'ActionScript',
|
||||
alias: ['as'],
|
||||
mode: 'actionscript'
|
||||
},
|
||||
{
|
||||
name: 'ada',
|
||||
label: 'Ada',
|
||||
alias: [],
|
||||
mode: 'ada'
|
||||
},
|
||||
{
|
||||
name: 'apache_conf',
|
||||
label: 'Apache config',
|
||||
alias: ['apache', 'conf'],
|
||||
mode: 'apache_conf'
|
||||
},
|
||||
{
|
||||
name: 'applescript',
|
||||
label: 'AppleScript',
|
||||
alias: ['scpt'],
|
||||
mode: 'applescript'
|
||||
},
|
||||
{
|
||||
name: 'asciidoc',
|
||||
label: 'AsciiDoc',
|
||||
alias: ['ascii', 'doc', 'txt'],
|
||||
mode: 'asciidoc'
|
||||
},
|
||||
{
|
||||
name: 'assembly_x86',
|
||||
label: 'Assembly x86',
|
||||
alias: ['assembly', 'x86', 'asm'],
|
||||
mode: 'assembly_x86'
|
||||
},
|
||||
{
|
||||
name: 'autohotkey',
|
||||
label: 'AutoHotkey',
|
||||
alias: ['ahk'],
|
||||
mode: 'autohotkey'
|
||||
},
|
||||
{
|
||||
name: 'batchfile',
|
||||
label: 'Batch file',
|
||||
alias: ['dos', 'windows', 'bat', 'cmd', 'btm'],
|
||||
mode: 'batchfile'
|
||||
},
|
||||
{
|
||||
name: 'cirru',
|
||||
label: 'Cirru',
|
||||
alias: [],
|
||||
mode: 'cirru'
|
||||
},
|
||||
{
|
||||
name: 'clojure',
|
||||
label: 'Clojure',
|
||||
alias: ['clj', 'cljs', 'cljc', 'edn'],
|
||||
mode: 'clojure'
|
||||
},
|
||||
{
|
||||
name: 'cobol',
|
||||
label: 'COBOL',
|
||||
alias: ['cbl', 'cob', 'cpy'],
|
||||
mode: 'cobol'
|
||||
},
|
||||
{
|
||||
name: 'coffee',
|
||||
label: 'CoffeeScript',
|
||||
alias: ['coffee'],
|
||||
mode: 'coffee'
|
||||
},
|
||||
{
|
||||
name: 'coldfusion',
|
||||
label: 'ColdFusion',
|
||||
alias: ['cfm', 'cfc'],
|
||||
mode: 'coldfusion'
|
||||
},
|
||||
{
|
||||
name: 'curly',
|
||||
label: 'Curly',
|
||||
alias: [],
|
||||
mode: 'curly'
|
||||
},
|
||||
{
|
||||
name: 'd',
|
||||
label: 'D',
|
||||
alias: ['dlang'],
|
||||
mode: 'd'
|
||||
},
|
||||
{
|
||||
name: 'dockerfile',
|
||||
label: 'DockerFile',
|
||||
alias: ['docker'],
|
||||
mode: 'docker'
|
||||
},
|
||||
{
|
||||
name: 'dart',
|
||||
label: 'Dart',
|
||||
alias: [],
|
||||
mode: 'dart'
|
||||
},
|
||||
{
|
||||
name: 'diff',
|
||||
label: 'Diff',
|
||||
alias: [],
|
||||
mode: 'diff'
|
||||
},
|
||||
{
|
||||
name: 'django',
|
||||
label: 'Django',
|
||||
alias: [],
|
||||
mode: 'djt'
|
||||
},
|
||||
{
|
||||
name: 'dot',
|
||||
label: 'DOT',
|
||||
alias: ['gv'],
|
||||
mode: 'dot'
|
||||
},
|
||||
{
|
||||
name: 'eiffel',
|
||||
label: 'Eiffel',
|
||||
alias: [],
|
||||
mode: 'eiffel'
|
||||
},
|
||||
{
|
||||
name: 'ejs',
|
||||
label: 'EJS',
|
||||
alias: [],
|
||||
mode: 'ejs'
|
||||
},
|
||||
{
|
||||
name: 'elixir',
|
||||
label: 'Elixir',
|
||||
alias: ['ex', 'exs'],
|
||||
mode: 'elixir'
|
||||
},
|
||||
{
|
||||
name: 'elm',
|
||||
label: 'Elm',
|
||||
alias: [],
|
||||
mode: 'elm'
|
||||
},
|
||||
{
|
||||
name: 'erlang',
|
||||
label: 'Erlang',
|
||||
alias: ['erl', 'hrl'],
|
||||
mode: 'erlang'
|
||||
},
|
||||
{
|
||||
name: 'forth',
|
||||
label: 'Forth',
|
||||
alias: ['fs', 'fth'],
|
||||
mode: 'forth'
|
||||
},
|
||||
{
|
||||
name: 'freemaker',
|
||||
label: 'Freemaker',
|
||||
alias: ['ftl'],
|
||||
mode: 'ftl'
|
||||
},
|
||||
{
|
||||
name: 'gcode',
|
||||
label: 'G-code',
|
||||
alias: ['mpt', 'mpf', 'nc'],
|
||||
mode: 'gcode'
|
||||
},
|
||||
{
|
||||
name: 'gherkin',
|
||||
label: 'Gherkin',
|
||||
alias: ['cucumber'],
|
||||
mode: 'gherkin'
|
||||
},
|
||||
{
|
||||
name: 'gitignore',
|
||||
label: 'Gitignore',
|
||||
alias: ['git'],
|
||||
mode: 'gitignore'
|
||||
},
|
||||
{
|
||||
name: 'glsl',
|
||||
label: 'GLSL',
|
||||
alias: ['opengl', 'shading'],
|
||||
mode: 'glsl'
|
||||
},
|
||||
{
|
||||
name: 'groovy',
|
||||
label: 'Groovy',
|
||||
alias: [],
|
||||
mode: 'grooby'
|
||||
},
|
||||
{
|
||||
name: 'haml',
|
||||
label: 'Haml',
|
||||
alias: [],
|
||||
mode: 'haml'
|
||||
},
|
||||
{
|
||||
name: 'handlebars',
|
||||
label: 'Handlebars',
|
||||
alias: ['hbs'],
|
||||
mode: 'handlebars'
|
||||
},
|
||||
{
|
||||
name: 'haskell',
|
||||
label: 'Haskell',
|
||||
alias: ['hs', 'lhs'],
|
||||
mode: 'haskell'
|
||||
},
|
||||
{
|
||||
name: 'haxe',
|
||||
label: 'Haxe',
|
||||
alias: ['hx', 'hxml'],
|
||||
mode: 'haxe'
|
||||
},
|
||||
{
|
||||
name: 'html_ruby',
|
||||
label: 'HTML (Ruby)',
|
||||
alias: ['erb', 'rhtml'],
|
||||
mode: 'html_ruby'
|
||||
},
|
||||
{
|
||||
name: 'jsx',
|
||||
label: 'JSX',
|
||||
alias: ['es', 'babel', 'js', 'jsx', 'react'],
|
||||
mode: 'jsx'
|
||||
},
|
||||
{
|
||||
name: 'typescript',
|
||||
label: 'TypeScript',
|
||||
alias: ['ts'],
|
||||
mode: 'typescript'
|
||||
},
|
||||
{
|
||||
name: 'ini',
|
||||
label: 'INI file',
|
||||
alias: [],
|
||||
mode: 'ini'
|
||||
},
|
||||
{
|
||||
name: 'io',
|
||||
label: 'Io',
|
||||
alias: [],
|
||||
mode: 'io'
|
||||
},
|
||||
{
|
||||
name: 'jack',
|
||||
label: 'Jack',
|
||||
alias: [],
|
||||
mode: 'jack'
|
||||
},
|
||||
{
|
||||
name: 'jade',
|
||||
label: 'Jade',
|
||||
alias: [],
|
||||
mode: 'jade'
|
||||
},
|
||||
{
|
||||
name: 'json',
|
||||
label: 'JSON',
|
||||
alias: [],
|
||||
mode: 'json'
|
||||
},
|
||||
{
|
||||
name: 'jsoniq',
|
||||
label: 'JSONiq',
|
||||
alias: ['query'],
|
||||
mode: 'jsoniq'
|
||||
},
|
||||
{
|
||||
name: 'jsp',
|
||||
label: 'JSP',
|
||||
alias: [],
|
||||
mode: 'jsp'
|
||||
},
|
||||
{
|
||||
name: 'julia',
|
||||
label: 'Julia',
|
||||
alias: [],
|
||||
mode: 'julia'
|
||||
},
|
||||
{
|
||||
name: 'latex',
|
||||
label: 'Latex',
|
||||
alias: ['tex'],
|
||||
mode: 'latex'
|
||||
},
|
||||
{
|
||||
name: 'lean',
|
||||
label: 'Lean',
|
||||
alias: [],
|
||||
mode: 'lean'
|
||||
},
|
||||
{
|
||||
name: 'less',
|
||||
label: 'Less',
|
||||
alias: [],
|
||||
mode: 'less'
|
||||
},
|
||||
{
|
||||
name: 'liquid',
|
||||
label: 'Liquid',
|
||||
alias: [],
|
||||
mode: 'liquid'
|
||||
},
|
||||
{
|
||||
name: 'lisp',
|
||||
label: 'Lisp',
|
||||
alias: ['lsp'],
|
||||
mode: 'lisp'
|
||||
},
|
||||
{
|
||||
name: 'livescript',
|
||||
label: 'LiveScript',
|
||||
alias: ['ls'],
|
||||
mode: 'livescript'
|
||||
},
|
||||
{
|
||||
name: 'logiql',
|
||||
label: 'LogiQL',
|
||||
alias: [],
|
||||
mode: 'logiql'
|
||||
},
|
||||
{
|
||||
name: 'lsl',
|
||||
label: 'LSL',
|
||||
alias: [],
|
||||
mode: 'lsl'
|
||||
},
|
||||
{
|
||||
name: 'lua',
|
||||
label: 'Lua',
|
||||
alias: [],
|
||||
mode: 'lua'
|
||||
},
|
||||
{
|
||||
name: 'luapage',
|
||||
label: 'Luapage',
|
||||
alias: [],
|
||||
mode: 'luapage'
|
||||
},
|
||||
{
|
||||
name: 'lucene',
|
||||
label: 'Lucene',
|
||||
alias: [],
|
||||
mode: 'lucene'
|
||||
},
|
||||
{
|
||||
name: 'makefile',
|
||||
label: 'Makefile',
|
||||
alias: [],
|
||||
mode: 'makefile'
|
||||
},
|
||||
{
|
||||
name: 'mask',
|
||||
label: 'Mask',
|
||||
alias: [],
|
||||
mode: 'mask'
|
||||
},
|
||||
{
|
||||
name: 'matlab',
|
||||
label: 'MATLAB',
|
||||
alias: [],
|
||||
mode: 'matlab'
|
||||
},
|
||||
{
|
||||
name: 'maze',
|
||||
label: 'Maze',
|
||||
alias: [],
|
||||
mode: 'maze'
|
||||
},
|
||||
{
|
||||
name: 'mel',
|
||||
label: 'MEL',
|
||||
alias: [],
|
||||
mode: 'mel'
|
||||
},
|
||||
{
|
||||
name: 'mipsassembler',
|
||||
label: 'MIPS assembly',
|
||||
alias: [],
|
||||
mode: 'mipsassembler'
|
||||
},
|
||||
{
|
||||
name: 'mushcode',
|
||||
label: 'MUSHCode',
|
||||
alias: [],
|
||||
mode: 'mushcode'
|
||||
},
|
||||
{
|
||||
name: 'mysql',
|
||||
label: 'MySQL',
|
||||
alias: [],
|
||||
mode: 'mysql'
|
||||
},
|
||||
{
|
||||
name: 'nix',
|
||||
label: 'Nix',
|
||||
alias: [],
|
||||
mode: 'nix'
|
||||
},
|
||||
{
|
||||
name: 'objectivec',
|
||||
label: 'Objective C',
|
||||
alias: ['objc'],
|
||||
mode: 'objectivec'
|
||||
},
|
||||
{
|
||||
name: 'ocaml',
|
||||
label: 'OCaml',
|
||||
alias: [],
|
||||
mode: 'ocaml'
|
||||
},
|
||||
{
|
||||
name: 'pascal',
|
||||
label: 'Pascal',
|
||||
alias: [],
|
||||
mode: 'pascal'
|
||||
},
|
||||
{
|
||||
name: 'perl',
|
||||
label: 'Perl',
|
||||
alias: [],
|
||||
mode: 'perl'
|
||||
},
|
||||
{
|
||||
name: 'pgsql',
|
||||
label: 'Postgres SQL',
|
||||
alias: ['postgres'],
|
||||
mode: 'pgsql'
|
||||
},
|
||||
{
|
||||
name: 'powershell',
|
||||
label: 'PowerShell',
|
||||
alias: ['ps1'],
|
||||
mode: 'powershell'
|
||||
},
|
||||
{
|
||||
name: 'praat',
|
||||
label: 'Praat',
|
||||
alias: [],
|
||||
mode: 'praat'
|
||||
},
|
||||
{
|
||||
name: 'prolog',
|
||||
label: 'Prolog',
|
||||
alias: ['pl', 'pro'],
|
||||
mode: 'prolog'
|
||||
},
|
||||
{
|
||||
name: 'properties',
|
||||
label: 'Properties',
|
||||
alias: [],
|
||||
mode: 'properties'
|
||||
},
|
||||
{
|
||||
name: 'protobuf',
|
||||
label: 'Protocol Buffers',
|
||||
alias: ['protocol', 'buffers'],
|
||||
mode: 'protobuf'
|
||||
},
|
||||
{
|
||||
name: 'r',
|
||||
label: 'R',
|
||||
alias: ['rlang'],
|
||||
mode: 'r'
|
||||
},
|
||||
{
|
||||
name: 'rdoc',
|
||||
label: 'RDoc',
|
||||
alias: [],
|
||||
mode: 'rdoc'
|
||||
},
|
||||
{
|
||||
name: 'rust',
|
||||
label: 'Rust',
|
||||
alias: [],
|
||||
mode: 'rust'
|
||||
},
|
||||
{
|
||||
name: 'sass',
|
||||
label: 'Sass',
|
||||
alias: [],
|
||||
mode: 'sass'
|
||||
},
|
||||
{
|
||||
name: 'scad',
|
||||
label: 'SCAD',
|
||||
alias: [],
|
||||
mode: 'scad'
|
||||
},
|
||||
{
|
||||
name: 'scala',
|
||||
label: 'Scala',
|
||||
alias: [],
|
||||
mode: 'scala'
|
||||
},
|
||||
{
|
||||
name: 'scheme',
|
||||
label: 'Scheme',
|
||||
alias: ['scm', 'ss'],
|
||||
mode: 'scheme'
|
||||
},
|
||||
{
|
||||
name: 'scss',
|
||||
label: 'Scss',
|
||||
alias: [],
|
||||
mode: 'scss'
|
||||
},
|
||||
{
|
||||
name: 'sh',
|
||||
label: 'Shell',
|
||||
alias: ['shell'],
|
||||
mode: 'sh'
|
||||
},
|
||||
{
|
||||
name: 'sjs',
|
||||
label: 'StratifiedJS',
|
||||
alias: ['stratified'],
|
||||
mode: 'sjs'
|
||||
},
|
||||
{
|
||||
name: 'smarty',
|
||||
label: 'Smarty',
|
||||
alias: [],
|
||||
mode: 'smarty'
|
||||
},
|
||||
{
|
||||
name: 'snippets',
|
||||
label: 'Snippets',
|
||||
alias: [],
|
||||
mode: 'snippets'
|
||||
},
|
||||
{
|
||||
name: 'soy_template',
|
||||
label: 'Soy Template',
|
||||
alias: ['soy'],
|
||||
mode: 'soy_template'
|
||||
},
|
||||
{
|
||||
name: 'space',
|
||||
label: 'Space',
|
||||
alias: [],
|
||||
mode: 'space'
|
||||
},
|
||||
{
|
||||
name: 'sql',
|
||||
label: 'SQL',
|
||||
alias: [],
|
||||
mode: 'sql'
|
||||
},
|
||||
{
|
||||
name: 'sqlserver',
|
||||
label: 'SQL Server',
|
||||
alias: [],
|
||||
mode: 'sqlserver'
|
||||
},
|
||||
{
|
||||
name: 'stylus',
|
||||
label: 'Stylus',
|
||||
alias: [],
|
||||
mode: 'stylus'
|
||||
},
|
||||
{
|
||||
name: 'svg',
|
||||
label: 'SVG',
|
||||
alias: [],
|
||||
mode: 'svg'
|
||||
},
|
||||
{
|
||||
name: 'swig',
|
||||
label: 'SWIG',
|
||||
alias: [],
|
||||
mode: 'swig'
|
||||
},
|
||||
{
|
||||
name: 'tcl',
|
||||
label: 'Tcl',
|
||||
alias: [],
|
||||
mode: 'tcl'
|
||||
},
|
||||
{
|
||||
name: 'tex',
|
||||
label: 'TeX',
|
||||
alias: [],
|
||||
mode: 'tex'
|
||||
},
|
||||
{
|
||||
name: 'textile',
|
||||
label: 'Textile',
|
||||
alias: [],
|
||||
mode: 'textile'
|
||||
},
|
||||
{
|
||||
name: 'toml',
|
||||
label: 'TOML',
|
||||
alias: [],
|
||||
mode: 'toml'
|
||||
},
|
||||
{
|
||||
name: 'twig',
|
||||
label: 'Twig',
|
||||
alias: [],
|
||||
mode: 'twig'
|
||||
},
|
||||
{
|
||||
name: 'vala',
|
||||
label: 'Vala',
|
||||
alias: [],
|
||||
mode: 'vala'
|
||||
},
|
||||
{
|
||||
name: 'vbscript',
|
||||
label: 'VBScript',
|
||||
alias: ['vbs', 'vbe'],
|
||||
mode: 'vbscript'
|
||||
},
|
||||
{
|
||||
name: 'velocity',
|
||||
label: 'Velocity',
|
||||
alias: [],
|
||||
mode: 'velocity'
|
||||
},
|
||||
{
|
||||
name: 'verilog',
|
||||
label: 'Verilog',
|
||||
alias: [],
|
||||
mode: 'verilog'
|
||||
},
|
||||
{
|
||||
name: 'vhdl',
|
||||
label: 'VHDL',
|
||||
alias: [],
|
||||
mode: 'vhdl'
|
||||
},
|
||||
{
|
||||
name: 'xml',
|
||||
label: 'XML',
|
||||
alias: [],
|
||||
mode: 'xml'
|
||||
},
|
||||
{
|
||||
name: 'xquery',
|
||||
label: 'XQuery',
|
||||
alias: [],
|
||||
mode: 'xquery'
|
||||
},
|
||||
{
|
||||
name: 'yaml',
|
||||
label: 'YAML',
|
||||
alias: [],
|
||||
mode: 'yaml'
|
||||
}
|
||||
]
|
||||
|
||||
export default modes
|
||||
7
browser/lib/openExternal.js
Normal file
7
browser/lib/openExternal.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const electron = require('electron')
|
||||
const shell = electron.shell
|
||||
|
||||
export default function (e) {
|
||||
shell.openExternal(e.currentTarget.href)
|
||||
e.preventDefault()
|
||||
}
|
||||
40
browser/lib/search.js
Normal file
40
browser/lib/search.js
Normal file
@@ -0,0 +1,40 @@
|
||||
'use strict'
|
||||
|
||||
var _ = require('lodash')
|
||||
|
||||
const TEXT_FILTER = 'TEXT_FILTER'
|
||||
const FOLDER_FILTER = 'FOLDER_FILTER'
|
||||
const TAG_FILTER = 'TAG_FILTER'
|
||||
|
||||
export default function search (articles, search) {
|
||||
let filters = search.split(' ').map(key => key.trim()).filter(key => key.length > 0 && !key.match(/^#$/)).map(key => {
|
||||
if (key.match(/^in:.+$/)) {
|
||||
return {type: FOLDER_FILTER, value: key.match(/^in:(.+)$/)[1]}
|
||||
}
|
||||
if (key.match(/^#(.+)/)) {
|
||||
return {type: TAG_FILTER, value: key.match(/^#(.+)$/)[1]}
|
||||
}
|
||||
return {type: TEXT_FILTER, value: key}
|
||||
})
|
||||
// let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER)
|
||||
let textFilters = filters.filter(filter => filter.type === TEXT_FILTER)
|
||||
let tagFilters = filters.filter(filter => filter.type === TAG_FILTER)
|
||||
|
||||
if (textFilters.length > 0) {
|
||||
articles = textFilters.reduce((articles, textFilter) => {
|
||||
return articles.filter(article => {
|
||||
return article.title.match(new RegExp(textFilter.value, 'i')) || article.content.match(new RegExp(textFilter.value, 'i'))
|
||||
})
|
||||
}, articles)
|
||||
}
|
||||
|
||||
if (tagFilters.length > 0) {
|
||||
articles = tagFilters.reduce((articles, tagFilter) => {
|
||||
return articles.filter(article => {
|
||||
return _.find(article.Tags, tag => tag.name.match(new RegExp(tagFilter.value, 'i')))
|
||||
})
|
||||
}, articles)
|
||||
}
|
||||
|
||||
return articles
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
var React = require('react/addons')
|
||||
|
||||
var ExternalLink = require('../Mixins/ExternalLink')
|
||||
var KeyCaster = require('../Mixins/KeyCaster')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [ExternalLink, KeyCaster('aboutModal')],
|
||||
propTypes: {
|
||||
close: React.PropTypes.func
|
||||
},
|
||||
onKeyCast: function (e) {
|
||||
switch (e.status) {
|
||||
case 'closeModal':
|
||||
this.props.close()
|
||||
break
|
||||
}
|
||||
},
|
||||
render: function () {
|
||||
var version = global.version
|
||||
return (
|
||||
<div className='PreferencesModal sideNavModal modal'>
|
||||
<div className='about1'>
|
||||
<img className='logo' src='resources/favicon-230x230.png'/>
|
||||
<div className='appInfo'>Boost {version == null || version.length === 0 ? 'DEV version' : 'v' + version}</div>
|
||||
</div>
|
||||
|
||||
<div className='about2'>
|
||||
<div className='externalLabel'>External links</div>
|
||||
<ul className='externalList'>
|
||||
<li><a onClick={this.openExternal} href='http://b00st.io'>Boost Homepage <i className='fa fa-external-link'/></a></li>
|
||||
<li><a>Regulation <i className='fa fa-external-link'/></a></li>
|
||||
<li><a>Private policy <i className='fa fa-external-link'/></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,102 +0,0 @@
|
||||
var React = require('react/addons')
|
||||
var Select = require('react-select')
|
||||
|
||||
var LinkedState = require('../Mixins/LinkedState')
|
||||
|
||||
var Hq = require('../Services/Hq')
|
||||
|
||||
var KeyCaster = require('../Mixins/KeyCaster')
|
||||
|
||||
var UserStore = require('../Stores/UserStore')
|
||||
|
||||
var getOptions = function (input, callback) {
|
||||
Hq.searchUser(input)
|
||||
.then(function (res) {
|
||||
callback(null, {
|
||||
options: res.body.map(function (user) {
|
||||
return {
|
||||
label: user.name,
|
||||
value: user.name
|
||||
}
|
||||
}),
|
||||
complete: false
|
||||
})
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [LinkedState, KeyCaster('addMemberModal')],
|
||||
propTypes: {
|
||||
team: React.PropTypes.object,
|
||||
close: React.PropTypes.func
|
||||
},
|
||||
getInitialState: function () {
|
||||
return {
|
||||
userName: '',
|
||||
role: 'member'
|
||||
}
|
||||
},
|
||||
onKeyCast: function (e) {
|
||||
switch (e.status) {
|
||||
case 'closeModal':
|
||||
this.props.close()
|
||||
break
|
||||
case 'submitAddMemberModal':
|
||||
this.handleSubmit()
|
||||
break
|
||||
}
|
||||
},
|
||||
handleSubmit: function () {
|
||||
this.setState({errorMessage: null}, function () {
|
||||
Hq
|
||||
.addMember(this.props.team.name, {
|
||||
userName: this.state.userName,
|
||||
role: this.state.role
|
||||
})
|
||||
.then(function (res) {
|
||||
console.log(res.body)
|
||||
UserStore.Actions.addMember(res.body)
|
||||
this.props.close()
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
if (err.status === 403) {
|
||||
this.setState({errorMessage: err.response.body.message})
|
||||
}
|
||||
}.bind(this))
|
||||
})
|
||||
},
|
||||
handleChange: function (value) {
|
||||
this.setState({userName: value})
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<div className='AddMemberModal modal'>
|
||||
<Select
|
||||
name='userName'
|
||||
value={this.state.userName}
|
||||
placeholder='Username to add'
|
||||
asyncOptions={getOptions}
|
||||
onChange={this.handleChange}
|
||||
className='userNameSelect'
|
||||
/>
|
||||
|
||||
<div className='formField'>
|
||||
Add member as
|
||||
<select valueLink={this.linkState('role')}>
|
||||
<option value={'member'}>Member</option>
|
||||
<option value={'owner'}>Owner</option>
|
||||
</select>
|
||||
role
|
||||
</div>
|
||||
|
||||
{this.state.errorMessage != null ? (<p className='errorAlert'>{this.state.errorMessage}</p>) : null}
|
||||
|
||||
<button onClick={this.handleSubmit} className='submitButton'><i className='fa fa-check'/></button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,55 +0,0 @@
|
||||
var React = require('react')
|
||||
|
||||
var Hq = require('../Services/Hq')
|
||||
|
||||
var KeyCaster = require('../Mixins/KeyCaster')
|
||||
|
||||
var PlanetStore = require('../Stores/PlanetStore')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [KeyCaster('codeDeleteModal')],
|
||||
propTypes: {
|
||||
planet: React.PropTypes.object,
|
||||
code: React.PropTypes.object,
|
||||
close: React.PropTypes.func
|
||||
},
|
||||
onKeyCast: function (e) {
|
||||
switch (e.status) {
|
||||
case 'submitCodeDeleteModal':
|
||||
this.submit()
|
||||
break
|
||||
case 'closeModal':
|
||||
this.props.close()
|
||||
break
|
||||
}
|
||||
},
|
||||
submit: function () {
|
||||
var planet = this.props.planet
|
||||
Hq.destroyCode(planet.userName, planet.name, this.props.code.localId)
|
||||
.then(function (res) {
|
||||
PlanetStore.Actions.destroyCode(res.body)
|
||||
this.props.close()
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
})
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<div className='CodeDeleteModal modal'>
|
||||
<div className='modal-header'>
|
||||
<h1>Delete Code</h1>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
<p>Are you sure to delete it?</p>
|
||||
</div>
|
||||
<div className='modal-footer'>
|
||||
<div className='modal-control'>
|
||||
<button onClick={this.props.close} className='btn-default'>Cancel</button>
|
||||
<button ref='submit' onClick={this.submit} className='btn-primary'>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,26 +0,0 @@
|
||||
var React = require('react')
|
||||
var CodeForm = require('./CodeForm')
|
||||
|
||||
module.exports = React.createClass({
|
||||
propTypes: {
|
||||
close: React.PropTypes.func,
|
||||
code: React.PropTypes.object,
|
||||
planet: React.PropTypes.object
|
||||
},
|
||||
componentDidMount: function () {
|
||||
// TODO: Hacked!! should fix later
|
||||
setTimeout(function () {
|
||||
React.findDOMNode(this.refs.form.refs.description).focus()
|
||||
}.bind(this), 1)
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<div className='CodeEditModal modal'>
|
||||
<div className='modal-header'>
|
||||
<h1>Edit Code</h1>
|
||||
</div>
|
||||
<CodeForm ref='form' code={this.props.code} planet={this.props.planet} close={this.props.close}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,59 +0,0 @@
|
||||
var React = require('react/addons')
|
||||
|
||||
var ace = window.ace
|
||||
|
||||
module.exports = React.createClass({
|
||||
propTypes: {
|
||||
code: React.PropTypes.string,
|
||||
mode: React.PropTypes.string,
|
||||
className: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func
|
||||
},
|
||||
componentDidMount: function () {
|
||||
var el = React.findDOMNode(this.refs.target)
|
||||
var editor = ace.edit(el)
|
||||
editor.$blockScrolling = Infinity
|
||||
editor.setValue(this.props.code)
|
||||
editor.renderer.setShowGutter(true)
|
||||
editor.setTheme('ace/theme/xcode')
|
||||
editor.clearSelection()
|
||||
|
||||
var session = editor.getSession()
|
||||
if (this.props.mode != null && this.props.mode.length > 0) {
|
||||
session.setMode('ace/mode/' + this.props.mode)
|
||||
} else {
|
||||
session.setMode('ace/mode/text')
|
||||
}
|
||||
session.setUseSoftTabs(true)
|
||||
session.setOption('useWorker', false)
|
||||
session.setUseWrapMode(true)
|
||||
|
||||
session.on('change', function (e) {
|
||||
if (this.props.onChange != null) {
|
||||
var value = editor.getValue()
|
||||
this.props.onChange(e, value)
|
||||
}
|
||||
}.bind(this))
|
||||
|
||||
this.setState({editor: editor})
|
||||
},
|
||||
componentDidUpdate: function (prevProps) {
|
||||
if (this.state.editor.getValue() !== this.props.code) {
|
||||
this.state.editor.setValue(this.props.code)
|
||||
this.state.editor.clearSelection()
|
||||
}
|
||||
if (prevProps.mode !== this.props.mode) {
|
||||
var session = this.state.editor.getSession()
|
||||
if (this.props.mode != null && this.props.mode.length > 0) {
|
||||
session.setMode('ace/mode/' + this.props.mode)
|
||||
} else {
|
||||
session.setMode('ace/mode/text')
|
||||
}
|
||||
}
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<div ref='target' className={this.props.className}></div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,162 +0,0 @@
|
||||
var React = require('react/addons')
|
||||
var CodeEditor = require('./CodeEditor')
|
||||
var Select = require('react-select')
|
||||
|
||||
var Hq = require('../Services/Hq')
|
||||
|
||||
var LinkedState = require('../Mixins/LinkedState')
|
||||
var KeyCaster = require('../Mixins/KeyCaster')
|
||||
|
||||
var PlanetStore = require('../Stores/PlanetStore')
|
||||
|
||||
var aceModes = require('../../../modules/ace-modes')
|
||||
|
||||
var getOptions = function (input, callback) {
|
||||
Hq.searchTag(input)
|
||||
.then(function (res) {
|
||||
callback(null, {
|
||||
options: res.body.map(function (tag) {
|
||||
return {
|
||||
label: tag.name,
|
||||
value: tag.name
|
||||
}
|
||||
}),
|
||||
complete: false
|
||||
})
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [LinkedState, KeyCaster('codeForm')],
|
||||
propTypes: {
|
||||
planet: React.PropTypes.object,
|
||||
close: React.PropTypes.func,
|
||||
transitionTo: React.PropTypes.func,
|
||||
code: React.PropTypes.object
|
||||
},
|
||||
getInitialState: function () {
|
||||
var code = Object.assign({
|
||||
description: '',
|
||||
mode: '',
|
||||
content: '',
|
||||
Tags: []
|
||||
}, this.props.code)
|
||||
|
||||
code.Tags = code.Tags.map(function (tag) {
|
||||
return {
|
||||
label: tag.name,
|
||||
value: tag.name
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
code: code
|
||||
}
|
||||
},
|
||||
onKeyCast: function (e) {
|
||||
switch (e.status) {
|
||||
case 'submitCodeForm':
|
||||
this.submit()
|
||||
break
|
||||
case 'closeModal':
|
||||
this.props.close()
|
||||
break
|
||||
}
|
||||
},
|
||||
handleModeChange: function (selected) {
|
||||
var code = this.state.code
|
||||
code.mode = selected
|
||||
this.setState({code: code})
|
||||
},
|
||||
handleTagsChange: function (selected, all) {
|
||||
var code = this.state.code
|
||||
code.Tags = all
|
||||
this.setState({code: code})
|
||||
},
|
||||
handleContentChange: function (e, value) {
|
||||
var code = this.state.code
|
||||
code.content = value
|
||||
this.setState({code: code})
|
||||
},
|
||||
submit: function () {
|
||||
var planet = this.props.planet
|
||||
var code = this.state.code
|
||||
code.Tags = code.Tags.map(function (tag) {
|
||||
return tag.value
|
||||
})
|
||||
if (this.props.code == null) {
|
||||
Hq.createCode(planet.userName, planet.name, this.state.code)
|
||||
.then(function (res) {
|
||||
var code = res.body
|
||||
PlanetStore.Actions.updateCode(code)
|
||||
this.props.close()
|
||||
this.props.transitionTo('codes', {userName: planet.userName, planetName: planet.name, localId: code.localId})
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
})
|
||||
} else {
|
||||
Hq.updateCode(planet.userName, planet.name, this.props.code.localId, this.state.code)
|
||||
.then(function (res) {
|
||||
var code = res.body
|
||||
PlanetStore.Actions.updateCode(code)
|
||||
this.props.close()
|
||||
}.bind(this))
|
||||
}
|
||||
},
|
||||
handleKeyDown: function (e) {
|
||||
if (e.keyCode === 13 && e.metaKey) {
|
||||
this.submit()
|
||||
e.stopPropagation()
|
||||
}
|
||||
},
|
||||
render: function () {
|
||||
var modeOptions = aceModes.map(function (mode) {
|
||||
return {
|
||||
label: mode,
|
||||
value: mode
|
||||
}
|
||||
})
|
||||
return (
|
||||
<div className='CodeForm'>
|
||||
<div className='modal-body'>
|
||||
<div className='form-group'>
|
||||
<textarea ref='description' className='codeDescription block-input' valueLink={this.linkState('code.description')} placeholder='Description'/>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<Select
|
||||
name='mode'
|
||||
className='modeSelect'
|
||||
value={this.state.code.mode}
|
||||
placeholder='Select Language'
|
||||
options={modeOptions}
|
||||
onChange={this.handleModeChange}/>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<CodeEditor onChange={this.handleContentChange} code={this.state.code.content} mode={this.state.code.mode}/>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<Select
|
||||
name='Tags'
|
||||
multi={true}
|
||||
allowCreate={true}
|
||||
value={this.state.code.Tags}
|
||||
placeholder='Tags...'
|
||||
asyncOptions={getOptions}
|
||||
onChange={this.handleTagsChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='modal-footer'>
|
||||
<div className='modal-control'>
|
||||
<button onClick={this.props.close} className='btn-default'>Cancel</button>
|
||||
<button onClick={this.submit} className='btn-primary'>{this.props.code == null ? 'Launch' : 'Relaunch'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,53 +0,0 @@
|
||||
var React = require('react/addons')
|
||||
|
||||
var ace = window.ace
|
||||
|
||||
module.exports = React.createClass({
|
||||
propTypes: {
|
||||
code: React.PropTypes.string,
|
||||
mode: React.PropTypes.string,
|
||||
className: React.PropTypes.string
|
||||
},
|
||||
componentDidMount: function () {
|
||||
var el = React.findDOMNode(this.refs.target)
|
||||
var editor = ace.edit(el)
|
||||
editor.$blockScrolling = Infinity
|
||||
editor.setValue(this.props.code)
|
||||
editor.renderer.setShowGutter(false)
|
||||
editor.setReadOnly(true)
|
||||
editor.setTheme('ace/theme/xcode')
|
||||
editor.setHighlightActiveLine(false)
|
||||
editor.clearSelection()
|
||||
|
||||
var session = editor.getSession()
|
||||
if (this.props.mode != null && this.props.mode.length > 0) {
|
||||
session.setMode('ace/mode/' + this.props.mode)
|
||||
} else {
|
||||
session.setMode('ace/mode/text')
|
||||
}
|
||||
session.setUseSoftTabs(true)
|
||||
session.setOption('useWorker', false)
|
||||
session.setUseWrapMode(true)
|
||||
|
||||
this.setState({editor: editor})
|
||||
},
|
||||
componentDidUpdate: function (prevProps) {
|
||||
if (this.state.editor.getValue() !== this.props.code) {
|
||||
this.state.editor.setValue(this.props.code)
|
||||
this.state.editor.clearSelection()
|
||||
}
|
||||
if (prevProps.mode !== this.props.mode) {
|
||||
var session = this.state.editor.getSession()
|
||||
if (this.props.mode != null && this.props.mode.length > 0) {
|
||||
session.setMode('ace/mode/' + this.props.mode)
|
||||
} else {
|
||||
session.setMode('ace/mode/text')
|
||||
}
|
||||
}
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<div ref='target' className={this.props.className}></div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,80 +0,0 @@
|
||||
var React = require('react')
|
||||
|
||||
var LinkedState = require('../Mixins/LinkedState')
|
||||
var KeyCaster = require('../Mixins/KeyCaster')
|
||||
|
||||
var Hq = require('../Services/Hq')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [LinkedState, KeyCaster('contactModal')],
|
||||
propTypes: {
|
||||
close: React.PropTypes.func
|
||||
},
|
||||
getInitialState: function () {
|
||||
return {
|
||||
isSent: false,
|
||||
mail: {
|
||||
title: '',
|
||||
content: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
onKeyCast: function (e) {
|
||||
switch (e.status) {
|
||||
case 'closeModal':
|
||||
this.props.close()
|
||||
break
|
||||
case 'submitContactModal':
|
||||
if (this.state.isSent) {
|
||||
this.props.close()
|
||||
return
|
||||
}
|
||||
this.sendEmail()
|
||||
break
|
||||
}
|
||||
},
|
||||
componentDidMount: function () {
|
||||
React.findDOMNode(this.refs.title).focus()
|
||||
},
|
||||
sendEmail: function () {
|
||||
Hq.sendEmail(this.state.mail)
|
||||
.then(function (res) {
|
||||
this.setState({isSent: !this.state.isSent})
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
})
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<div className='ContactModal modal'>
|
||||
<div className='modal-header'><h1>Contact form</h1></div>
|
||||
|
||||
{!this.state.isSent ? (
|
||||
<div className='contactForm'>
|
||||
<div className='modal-body'>
|
||||
<div className='formField'>
|
||||
<input ref='title' valueLink={this.linkState('mail.title')} placeholder='Title'/>
|
||||
</div>
|
||||
<div className='formField'>
|
||||
<textarea valueLink={this.linkState('mail.content')} placeholder='Content'/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='modal-footer'>
|
||||
<div className='formControl'>
|
||||
<button onClick={this.sendEmail} className='sendButton'>Send</button>
|
||||
<button onClick={this.props.close}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='confirmation'>
|
||||
<div className='confirmationMessage'>Thanks for sharing your opinion!</div>
|
||||
<button className='doneButton' onClick={this.props.close}>Done</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,170 +0,0 @@
|
||||
/* global localStorage */
|
||||
|
||||
var React = require('react/addons')
|
||||
|
||||
var Hq = require('../Services/Hq')
|
||||
|
||||
var LinkedState = require('../Mixins/LinkedState')
|
||||
var KeyCaster = require('../Mixins/KeyCaster')
|
||||
|
||||
var UserStore = require('../Stores/UserStore')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [LinkedState, KeyCaster('editProfileModal')],
|
||||
propTypes: {
|
||||
user: React.PropTypes.shape({
|
||||
name: React.PropTypes.string,
|
||||
profileName: React.PropTypes.string,
|
||||
email: React.PropTypes.string
|
||||
}),
|
||||
close: React.PropTypes.func
|
||||
},
|
||||
getInitialState: function () {
|
||||
var user = this.props.user
|
||||
return {
|
||||
currentTab: 'userInfo',
|
||||
user: {
|
||||
profileName: user.profileName,
|
||||
email: user.email
|
||||
},
|
||||
userSubmitStatus: null,
|
||||
password: {
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
passwordConfirmation: ''
|
||||
},
|
||||
passwordSubmitStatus: null
|
||||
}
|
||||
},
|
||||
onKeyCast: function (e) {
|
||||
switch (e.status) {
|
||||
case 'closeModal':
|
||||
this.props.close()
|
||||
break
|
||||
}
|
||||
},
|
||||
selectTab: function (tabName) {
|
||||
return function () {
|
||||
this.setState({currentTab: tabName})
|
||||
}.bind(this)
|
||||
},
|
||||
saveUserInfo: function () {
|
||||
this.setState({
|
||||
userSubmitStatus: 'sending'
|
||||
}, function () {
|
||||
Hq.updateUser(this.props.user.name, this.state.user)
|
||||
.then(function (res) {
|
||||
this.setState({userSubmitStatus: 'done'}, function () {
|
||||
localStorage.setItem('currentUser', JSON.stringify(res.body))
|
||||
UserStore.Actions.update(res.body)
|
||||
})
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
this.setState({userSubmitStatus: 'error'})
|
||||
}.bind(this))
|
||||
})
|
||||
},
|
||||
savePassword: function () {
|
||||
this.setState({
|
||||
passwordSubmitStatus: 'sending'
|
||||
}, function () {
|
||||
Hq.changePassword(this.state.password)
|
||||
.then(function (res) {
|
||||
this.setState({
|
||||
passwordSubmitStatus: 'done',
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
passwordConfirmation: ''
|
||||
})
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
this.setState({
|
||||
passwordSubmitStatus: 'error',
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
passwordConfirmation: ''
|
||||
})
|
||||
}.bind(this))
|
||||
})
|
||||
},
|
||||
render: function () {
|
||||
var content
|
||||
|
||||
switch (this.state.currentTab) {
|
||||
case 'userInfo':
|
||||
content = this.renderUserInfoTab()
|
||||
break
|
||||
case 'password':
|
||||
content = this.renderPasswordTab()
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='EditProfileModal sideNavModal modal'>
|
||||
<div className='leftPane'>
|
||||
<div className='modalLabel'>Edit profile</div>
|
||||
<div className='tabList'>
|
||||
<button className={this.state.currentTab === 'userInfo' ? 'active' : ''} onClick={this.selectTab('userInfo')}><i className='fa fa-user fa-fw'/> User Info</button>
|
||||
<button className={this.state.currentTab === 'password' ? 'active' : ''} onClick={this.selectTab('password')}><i className='fa fa-lock fa-fw'/> Password</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='rightPane'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
renderUserInfoTab: function () {
|
||||
return (
|
||||
<div className='userInfoTab tab'>
|
||||
<div className='formField'>
|
||||
<label>Profile Name</label>
|
||||
<input valueLink={this.linkState('user.profileName')}/>
|
||||
</div>
|
||||
<div className='formField'>
|
||||
<label>E-mail</label>
|
||||
<input valueLink={this.linkState('user.email')}/>
|
||||
</div>
|
||||
<div className='formConfirm'>
|
||||
<button disabled={this.state.userSubmitStatus === 'sending'} onClick={this.saveUserInfo}>Save</button>
|
||||
|
||||
<div className={'alertInfo' + (this.state.userSubmitStatus === 'sending' ? '' : ' hide')}>on Sending...</div>
|
||||
|
||||
<div className={'alertError' + (this.state.userSubmitStatus === 'error' ? '' : ' hide')}>Connection failed.. Try again.</div>
|
||||
|
||||
<div className={'alertSuccess' + (this.state.userSubmitStatus === 'done' ? '' : ' hide')}>Successfully done!!</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
renderPasswordTab: function () {
|
||||
return (
|
||||
<div className='passwordTab tab'>
|
||||
<div className='formField'>
|
||||
<label>Current password</label>
|
||||
<input type='password' valueLink={this.linkState('password.currentPassword')}/>
|
||||
</div>
|
||||
<div className='formField'>
|
||||
<label>New password</label>
|
||||
<input type='password' valueLink={this.linkState('password.newPassword')}/>
|
||||
</div>
|
||||
<div className='formField'>
|
||||
<label>Confirmation</label>
|
||||
<input type='password' valueLink={this.linkState('password.passwordConfirmation')}/>
|
||||
</div>
|
||||
|
||||
<div className='formConfirm'>
|
||||
<button disabled={this.state.password.newPassword.length === 0 || this.state.password.newPassword !== this.state.password.passwordConfirmation || this.state.passwordSubmitStatus === 'sending'} onClick={this.savePassword}>Save</button>
|
||||
|
||||
<div className={'alertInfo' + (this.state.passwordSubmitStatus === 'sending' ? '' : ' hide')}>on Sending...</div>
|
||||
|
||||
<div className={'alertError' + (this.state.passwordSubmitStatus === 'error' ? '' : ' hide')}>Connection failed.. Try again.</div>
|
||||
|
||||
<div className={'alertSuccess' + (this.state.passwordSubmitStatus === 'done' ? '' : ' hide')}>Successfully done!!</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,180 +0,0 @@
|
||||
/* global localStorage */
|
||||
|
||||
var React = require('react/addons')
|
||||
var ReactRouter = require('react-router')
|
||||
var Navigation = ReactRouter.Navigation
|
||||
var State = ReactRouter.State
|
||||
var Link = ReactRouter.Link
|
||||
var Reflux = require('reflux')
|
||||
|
||||
var Modal = require('../Mixins/Modal')
|
||||
|
||||
var UserStore = require('../Stores/UserStore')
|
||||
|
||||
var PreferencesModal = require('./PreferencesModal')
|
||||
var PlanetCreateModal = require('./PlanetCreateModal')
|
||||
var TeamCreateModal = require('./TeamCreateModal')
|
||||
var LogoutModal = require('./LogoutModal')
|
||||
var ProfileImage = require('./ProfileImage')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [Navigation, State, Reflux.listenTo(UserStore, 'onUserChange'), Modal],
|
||||
getInitialState: function () {
|
||||
return {
|
||||
isPlanetCreateModalOpen: false,
|
||||
currentUser: JSON.parse(localStorage.getItem('currentUser'))
|
||||
}
|
||||
},
|
||||
onUserChange: function (res) {
|
||||
switch (res.status) {
|
||||
case 'userUpdated':
|
||||
var user = res.data
|
||||
var currentUser = this.state.currentUser
|
||||
if (currentUser.id === user.id) {
|
||||
this.setState({currentUser: user})
|
||||
return
|
||||
}
|
||||
|
||||
if (user.userType === 'team') {
|
||||
var isMyTeam = user.Members.some(function (member) {
|
||||
if (currentUser.id === member.id) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if (isMyTeam) {
|
||||
var isNew = !currentUser.Teams.some(function (team, index) {
|
||||
if (user.id === team.id) {
|
||||
currentUser.Teams.splice(index, 1, user)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if (isNew) {
|
||||
currentUser.Teams.push(user)
|
||||
}
|
||||
|
||||
this.setState({currentUser: currentUser})
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
openTeamCreateModal: function () {
|
||||
this.openModal(TeamCreateModal, {user: this.state.currentUser, transitionTo: this.transitionTo})
|
||||
},
|
||||
openPreferencesModal: function () {
|
||||
this.openModal(PreferencesModal)
|
||||
},
|
||||
openPlanetCreateModal: function () {
|
||||
this.openModal(PlanetCreateModal, {transitionTo: this.transitionTo})
|
||||
},
|
||||
toggleProfilePopup: function () {
|
||||
this.openProfilePopup()
|
||||
},
|
||||
openProfilePopup: function () {
|
||||
this.setState({isProfilePopupOpen: true}, function () {
|
||||
document.addEventListener('click', this.closeProfilePopup)
|
||||
})
|
||||
},
|
||||
closeProfilePopup: function () {
|
||||
document.removeEventListener('click', this.closeProfilePopup)
|
||||
this.setState({isProfilePopupOpen: false})
|
||||
},
|
||||
handleLogoutClick: function () {
|
||||
this.openModal(LogoutModal, {transitionTo: this.transitionTo})
|
||||
},
|
||||
switchPlanetByIndex: function (index) {
|
||||
var planetProps = this.refs.planets.props.children[index - 1].props
|
||||
this.transitionTo('planet', {userName: planetProps.userName, planetName: planetProps.planetName})
|
||||
},
|
||||
render: function () {
|
||||
var params = this.getParams()
|
||||
|
||||
if (this.state.currentUser == null) {
|
||||
return (
|
||||
<div className='HomeNavigator'>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
var planets = (this.state.currentUser.Planets.concat(this.state.currentUser.Teams.reduce(function (planets, team) {
|
||||
return team.Planets == null ? planets : planets.concat(team.Planets)
|
||||
}, []))).map(function (planet, index) {
|
||||
return (
|
||||
<li userName={planet.userName} planetName={planet.name} key={planet.id} className={params.userName === planet.userName && params.planetName === planet.name ? 'active' : ''}>
|
||||
<Link to='planet' params={{userName: planet.userName, planetName: planet.name}}>
|
||||
{planet.name[0]}
|
||||
<div className='planetTooltip'>{planet.userName}/{planet.name}</div>
|
||||
</Link>
|
||||
{index < 9 ? (<div className='shortCut'>⌘{index + 1}</div>) : null}
|
||||
</li>
|
||||
)
|
||||
})
|
||||
|
||||
var popup = this.renderPopup()
|
||||
|
||||
return (
|
||||
<div className='HomeNavigator'>
|
||||
<button onClick={this.toggleProfilePopup} className='profileButton'>
|
||||
<ProfileImage size='55' email={this.state.currentUser.email}/>
|
||||
</button>
|
||||
{popup}
|
||||
<ul ref='planets' className='planetList'>
|
||||
{planets}
|
||||
</ul>
|
||||
<button onClick={this.openPlanetCreateModal} className='newPlanet'>
|
||||
<i className='fa fa-plus'/>
|
||||
<div className='tooltip'>Create new planet</div>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
renderPopup: function () {
|
||||
var teams = this.state.currentUser.Teams == null ? [] : this.state.currentUser.Teams.map(function (team) {
|
||||
return (
|
||||
<li key={'user-' + team.id}>
|
||||
<Link to='userHome' params={{userName: team.name}} className='userName'>{team.profileName} ({team.name})</Link>
|
||||
</li>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div ref='profilePopup' className={'profilePopup' + (this.state.isProfilePopupOpen ? '' : ' close')}>
|
||||
<div className='profileGroup'>
|
||||
<div className='profileGroupLabel'>
|
||||
<span>You</span>
|
||||
</div>
|
||||
<ul className='profileGroupList'>
|
||||
<li>
|
||||
<Link to='userHome' params={{userName: this.state.currentUser.name}} className='userName'>Profile ({this.state.currentUser.name})</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className='profileGroup'>
|
||||
<div className='profileGroupLabel'>
|
||||
<span>Team</span>
|
||||
</div>
|
||||
<ul className='profileGroupList'>
|
||||
{teams}
|
||||
<li>
|
||||
<button onClick={this.openTeamCreateModal} className='createNewTeam'><i className='fa fa-plus-square-o'/> create new team</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul className='controlGroup'>
|
||||
<li>
|
||||
<button onClick={this.openPreferencesModal}><i className='fa fa-gears fa-fw'/> Preferences</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={this.handleLogoutClick}><i className='fa fa-sign-out fa-fw'/> Log out</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,79 +0,0 @@
|
||||
var React = require('react/addons')
|
||||
|
||||
var CodeForm = require('./CodeForm')
|
||||
var NoteForm = require('./NoteForm')
|
||||
|
||||
module.exports = React.createClass({
|
||||
propTypes: {
|
||||
planet: React.PropTypes.object,
|
||||
transitionTo: React.PropTypes.func,
|
||||
close: React.PropTypes.func
|
||||
},
|
||||
getInitialState: function () {
|
||||
return {
|
||||
currentTab: 'code'
|
||||
}
|
||||
},
|
||||
componentDidMount: function () {
|
||||
var codeButton = React.findDOMNode(this.refs.codeButton)
|
||||
codeButton.addEventListener('keydown', this.handleKeyDown)
|
||||
React.findDOMNode(this.refs.noteButton).addEventListener('keydown', this.handleKeyDown)
|
||||
codeButton.focus()
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
React.findDOMNode(this.refs.codeButton).removeEventListener('keydown', this.handleKeyDown)
|
||||
React.findDOMNode(this.refs.noteButton).removeEventListener('keydown', this.handleKeyDown)
|
||||
},
|
||||
handleKeyDown: function (e) {
|
||||
if (e.keyCode === 37 && e.metaKey) {
|
||||
this.selectCodeTab()
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
if (e.keyCode === 39 && e.metaKey) {
|
||||
this.selectNoteTab()
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
if (e.keyCode === 9) {
|
||||
if (this.state.currentTab === 'code') React.findDOMNode(this.refs.form.refs.description).focus()
|
||||
else React.findDOMNode(this.refs.form.refs.title).focus()
|
||||
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
selectCodeTab: function () {
|
||||
this.setState({currentTab: 'code'}, function () {
|
||||
React.findDOMNode(this.refs.codeButton).focus()
|
||||
})
|
||||
},
|
||||
selectNoteTab: function () {
|
||||
this.setState({currentTab: 'note'}, function () {
|
||||
React.findDOMNode(this.refs.noteButton).focus()
|
||||
})
|
||||
},
|
||||
render: function () {
|
||||
var modalBody
|
||||
if (this.state.currentTab === 'code') {
|
||||
modalBody = (
|
||||
<CodeForm ref='form' planet={this.props.planet} transitionTo={this.props.transitionTo} close={this.props.close}/>
|
||||
)
|
||||
} else {
|
||||
modalBody = (
|
||||
<NoteForm ref='form' planet={this.props.planet} transitionTo={this.props.transitionTo} close={this.props.close}/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='LaunchModal modal'>
|
||||
<div className='modal-header'>
|
||||
<div className='modal-tab'>
|
||||
<button ref='codeButton' className={this.state.currentTab === 'code' ? 'btn-primary active' : 'btn-default'} onClick={this.selectCodeTab}>Code</button>
|
||||
<button ref='noteButton' className={this.state.currentTab === 'note' ? 'btn-primary active' : 'btn-default'} onClick={this.selectNoteTab}>Note</button>
|
||||
</div>
|
||||
</div>
|
||||
{modalBody}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,40 +0,0 @@
|
||||
/* global localStorage */
|
||||
|
||||
var React = require('react')
|
||||
|
||||
var KeyCaster = require('../Mixins/KeyCaster')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [KeyCaster('logoutModal')],
|
||||
propTypes: {
|
||||
transitionTo: React.PropTypes.func,
|
||||
close: React.PropTypes.func
|
||||
},
|
||||
onKeyCast: function (e) {
|
||||
switch (e.status) {
|
||||
case 'closeModal':
|
||||
this.props.close()
|
||||
break
|
||||
case 'submitLogoutModal':
|
||||
this.logout()
|
||||
break
|
||||
}
|
||||
},
|
||||
logout: function () {
|
||||
localStorage.removeItem('currentUser')
|
||||
localStorage.removeItem('token')
|
||||
this.props.transitionTo('login')
|
||||
this.props.close()
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<div className='LogoutModal modal'>
|
||||
<div className='messageLabel'>Are you sure to log out?</div>
|
||||
<div className='formControl'>
|
||||
<button onClick={this.props.close}>Cancel</button>
|
||||
<button className='logoutButton' onClick={this.logout}>Log out</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,43 +0,0 @@
|
||||
var React = require('react')
|
||||
|
||||
var Markdown = require('../Mixins/Markdown')
|
||||
var ExternalLink = require('../Mixins/ExternalLink')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [Markdown, ExternalLink],
|
||||
propTypes: {
|
||||
className: React.PropTypes.string,
|
||||
content: React.PropTypes.string
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this.addListener()
|
||||
},
|
||||
componentDidUpdate: function () {
|
||||
this.addListener()
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
this.removeListener()
|
||||
},
|
||||
componentWillUpdate: function () {
|
||||
this.removeListener()
|
||||
},
|
||||
addListener: function () {
|
||||
var anchors = React.findDOMNode(this).querySelectorAll('a')
|
||||
|
||||
for (var i = 0; i < anchors.length; i++) {
|
||||
anchors[i].addEventListener('click', this.openExternal)
|
||||
}
|
||||
},
|
||||
removeListener: function () {
|
||||
var anchors = React.findDOMNode(this).querySelectorAll('a')
|
||||
|
||||
for (var i = 0; i < anchors.length; i++) {
|
||||
anchors[i].removeEventListener('click', this.openExternal)
|
||||
}
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<div className={'MarkdownPreview' + (this.props.className != null ? ' ' + this.props.className : '')} dangerouslySetInnerHTML={{__html: ' ' + this.markdown(this.props.content)}}/>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,55 +0,0 @@
|
||||
var React = require('react')
|
||||
|
||||
var Hq = require('../Services/Hq')
|
||||
|
||||
var KeyCaster = require('../Mixins/KeyCaster')
|
||||
|
||||
var PlanetStore = require('../Stores/PlanetStore')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [KeyCaster('noteDeleteModal')],
|
||||
propTypes: {
|
||||
planet: React.PropTypes.object,
|
||||
note: React.PropTypes.object,
|
||||
close: React.PropTypes.func
|
||||
},
|
||||
onKeyCast: function (e) {
|
||||
switch (e.status) {
|
||||
case 'submitNoteDeleteModal':
|
||||
this.submit()
|
||||
break
|
||||
case 'closeModal':
|
||||
this.props.close()
|
||||
break
|
||||
}
|
||||
},
|
||||
submit: function () {
|
||||
var planet = this.props.planet
|
||||
Hq.destroyNote(planet.userName, planet.name, this.props.note.localId)
|
||||
.then(function (res) {
|
||||
PlanetStore.Actions.destroyNote(res.body)
|
||||
this.props.close()
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
})
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<div className='NoteDeleteModal modal'>
|
||||
<div className='modal-header'>
|
||||
<h1>Delete Note</h1>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
<p>Are you sure to delete it?</p>
|
||||
</div>
|
||||
<div className='modal-footer'>
|
||||
<div className='modal-control'>
|
||||
<button onClick={this.props.close} className='btn-default'>Cancel</button>
|
||||
<button ref='submit' onClick={this.submit} className='btn-primary'>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,27 +0,0 @@
|
||||
var React = require('react')
|
||||
|
||||
var NoteForm = require('./NoteForm')
|
||||
|
||||
module.exports = React.createClass({
|
||||
propTypes: {
|
||||
close: React.PropTypes.func,
|
||||
note: React.PropTypes.object,
|
||||
planet: React.PropTypes.object
|
||||
},
|
||||
componentDidMount: function () {
|
||||
// TODO: Hacked!! should fix later
|
||||
setTimeout(function () {
|
||||
React.findDOMNode(this.refs.form.refs.title).focus()
|
||||
}.bind(this), 1)
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<div className='NoteEditModal modal'>
|
||||
<div className='modal-header'>
|
||||
<h1>Edit Note</h1>
|
||||
</div>
|
||||
<NoteForm ref='form' note={this.props.note} planet={this.props.planet} close={this.props.close}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,153 +0,0 @@
|
||||
var React = require('react/addons')
|
||||
var Select = require('react-select')
|
||||
|
||||
var Hq = require('../Services/Hq')
|
||||
|
||||
var LinkedState = require('../Mixins/LinkedState')
|
||||
var Markdown = require('../Mixins/Markdown')
|
||||
var KeyCaster = require('../Mixins/KeyCaster')
|
||||
|
||||
var PlanetStore = require('../Stores/PlanetStore')
|
||||
|
||||
var CodeEditor = require('./CodeEditor')
|
||||
var MarkdownPreview = require('./MarkdownPreview')
|
||||
|
||||
var getOptions = function (input, callback) {
|
||||
Hq.searchTag(input)
|
||||
.then(function (res) {
|
||||
callback(null, {
|
||||
options: res.body.map(function (tag) {
|
||||
return {
|
||||
label: tag.name,
|
||||
value: tag.name
|
||||
}
|
||||
}),
|
||||
complete: false
|
||||
})
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
var EDIT_MODE = 0
|
||||
var PREVIEW_MODE = 1
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [LinkedState, Markdown, KeyCaster('noteForm')],
|
||||
propTypes: {
|
||||
planet: React.PropTypes.object,
|
||||
close: React.PropTypes.func,
|
||||
transitionTo: React.PropTypes.func,
|
||||
note: React.PropTypes.object
|
||||
},
|
||||
getInitialState: function () {
|
||||
var note = Object.assign({
|
||||
title: '',
|
||||
content: '',
|
||||
Tags: []
|
||||
}, this.props.note)
|
||||
note.Tags = note.Tags.map(function (tag) {
|
||||
return {
|
||||
label: tag.name,
|
||||
value: tag.name
|
||||
}
|
||||
})
|
||||
return {
|
||||
note: note,
|
||||
mode: EDIT_MODE
|
||||
}
|
||||
},
|
||||
onKeyCast: function (e) {
|
||||
switch (e.status) {
|
||||
case 'submitNoteForm':
|
||||
this.submit()
|
||||
break
|
||||
case 'closeModal':
|
||||
this.props.close()
|
||||
break
|
||||
}
|
||||
},
|
||||
handleTagsChange: function (selected, all) {
|
||||
var note = this.state.note
|
||||
note.Tags = all
|
||||
this.setState({note: note})
|
||||
},
|
||||
handleContentChange: function (e, value) {
|
||||
var note = this.state.note
|
||||
note.content = value
|
||||
this.setState({note: note})
|
||||
},
|
||||
togglePreview: function () {
|
||||
this.setState({mode: this.state.mode === EDIT_MODE ? PREVIEW_MODE : EDIT_MODE})
|
||||
},
|
||||
submit: function () {
|
||||
var planet = this.props.planet
|
||||
var note = this.state.note
|
||||
note.Tags = note.Tags.map(function (tag) {
|
||||
return tag.value
|
||||
})
|
||||
|
||||
if (this.props.note == null) {
|
||||
Hq.createNote(planet.userName, planet.name, this.state.note)
|
||||
.then(function (res) {
|
||||
var note = res.body
|
||||
PlanetStore.Actions.updateNote(note)
|
||||
this.props.close()
|
||||
this.props.transitionTo('notes', {userName: planet.userName, planetName: planet.name, localId: note.localId})
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
})
|
||||
} else {
|
||||
Hq.updateNote(planet.userName, planet.name, this.props.note.localId, this.state.note)
|
||||
.then(function (res) {
|
||||
var note = res.body
|
||||
PlanetStore.Actions.updateNote(note)
|
||||
this.props.close()
|
||||
}.bind(this))
|
||||
}
|
||||
},
|
||||
render: function () {
|
||||
var content = this.state.mode === EDIT_MODE ? (
|
||||
<div className='form-group'>
|
||||
<CodeEditor onChange={this.handleContentChange} code={this.state.note.content} mode={'markdown'}/>
|
||||
</div>
|
||||
) : (
|
||||
<div className='form-group relative'>
|
||||
<div className='previewMode'>Preview mode</div>
|
||||
<MarkdownPreview className='marked' content={this.state.note.content}/>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='NoteForm'>
|
||||
<div className='modal-body'>
|
||||
<div className='form-group'>
|
||||
<input ref='title' className='block-input' valueLink={this.linkState('note.title')} placeholder='Title'/>
|
||||
</div>
|
||||
{content}
|
||||
<div className='form-group'>
|
||||
<Select
|
||||
name='Tags'
|
||||
multi={true}
|
||||
allowCreate={true}
|
||||
value={this.state.note.Tags}
|
||||
placeholder='Tags...'
|
||||
asyncOptions={getOptions}
|
||||
onChange={this.handleTagsChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='modal-footer'>
|
||||
<button onClick={this.togglePreview} className={'btn-default' + (this.state.mode === PREVIEW_MODE ? ' active' : '')}>Preview mode</button>
|
||||
<div className='modal-control'>
|
||||
<button onClick={this.props.close} className='btn-default'>Cancel</button>
|
||||
<button onClick={this.submit} className='btn-primary'>Launch</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,126 +0,0 @@
|
||||
var React = require('react/addons')
|
||||
var moment = require('moment')
|
||||
|
||||
var CodeViewer = require('./CodeViewer')
|
||||
var CodeEditModal = require('./CodeEditModal')
|
||||
var CodeDeleteModal = require('./CodeDeleteModal')
|
||||
var NoteEditModal = require('./NoteEditModal')
|
||||
var NoteDeleteModal = require('./NoteDeleteModal')
|
||||
var MarkdownPreview = require('./MarkdownPreview')
|
||||
var ProfileImage = require('./ProfileImage')
|
||||
|
||||
var Modal = require('../Mixins/Modal')
|
||||
var ForceUpdate = require('../Mixins/ForceUpdate')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [ForceUpdate(60000), Modal],
|
||||
propTypes: {
|
||||
article: React.PropTypes.object,
|
||||
showOnlyWithTag: React.PropTypes.func,
|
||||
planet: React.PropTypes.object
|
||||
},
|
||||
getInitialState: function () {
|
||||
return {
|
||||
isEditModalOpen: false
|
||||
}
|
||||
},
|
||||
openEditModal: function () {
|
||||
if (this.props.article == null) return
|
||||
switch (this.props.article.type) {
|
||||
case 'code' :
|
||||
this.openModal(CodeEditModal, {code: this.props.article, planet: this.props.planet})
|
||||
break
|
||||
case 'note' :
|
||||
this.openModal(NoteEditModal, {note: this.props.article, planet: this.props.planet})
|
||||
}
|
||||
},
|
||||
openDeleteModal: function () {
|
||||
if (this.props.article == null) return
|
||||
switch (this.props.article.type) {
|
||||
case 'code' :
|
||||
this.openModal(CodeDeleteModal, {code: this.props.article, planet: this.props.planet})
|
||||
break
|
||||
case 'note' :
|
||||
this.openModal(NoteDeleteModal, {note: this.props.article, planet: this.props.planet})
|
||||
}
|
||||
},
|
||||
render: function () {
|
||||
var article = this.props.article
|
||||
if (article == null) {
|
||||
return (
|
||||
<div className='PlanetArticleDetail'>
|
||||
Nothing selected
|
||||
</div>
|
||||
)
|
||||
}
|
||||
var tags = article.Tags.length > 0 ? article.Tags.map(function (tag) {
|
||||
return (
|
||||
<a onClick={this.props.showOnlyWithTag(tag.name)} key={tag.id}>#{tag.name}</a>
|
||||
)
|
||||
}.bind(this)) : (
|
||||
<a className='noTag'>Not tagged yet</a>
|
||||
)
|
||||
if (article.type === 'code') {
|
||||
return (
|
||||
<div className='PlanetArticleDetail codeDetail'>
|
||||
<div className='detailHeader'>
|
||||
<div className='itemLeft'>
|
||||
<ProfileImage className='profileImage' size='25' email={article.User.email}/>
|
||||
<i className='fa fa-code fa-fw'></i>
|
||||
</div>
|
||||
|
||||
<div className='itemRight'>
|
||||
<div className='itemInfo'>{moment(article.updatedAt).fromNow()} by <span className='userProfileName'>{article.User.profileName}</span></div>
|
||||
<div className='description'>{article.description}</div>
|
||||
<div className='tags'><i className='fa fa-tags'/>{tags}</div>
|
||||
</div>
|
||||
|
||||
<span className='itemControl'>
|
||||
<button id='articleEditButton' onClick={this.openEditModal} className='editButton'>
|
||||
<i className='fa fa-edit fa-fw'></i>
|
||||
<div className='tooltip'>Edit</div>
|
||||
</button>
|
||||
<button onClick={this.openDeleteModal} className='deleteButton'>
|
||||
<i className='fa fa-trash fa-fw'></i>
|
||||
<div className='tooltip'>Delete</div>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div className='detailBody'>
|
||||
<CodeViewer className='content' code={article.content} mode={article.mode}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className='PlanetArticleDetail noteDetail'>
|
||||
<div className='detailHeader'>
|
||||
<div className='itemLeft'>
|
||||
<ProfileImage className='profileImage' size='25' email={article.User.email}/>
|
||||
<i className='fa fa-file-text-o fa-fw'></i>
|
||||
</div>
|
||||
|
||||
<div className='itemRight'>
|
||||
<div className='itemInfo'>{moment(article.updatedAt).fromNow()} by <span className='userProfileName'>{article.User.profileName}</span></div>
|
||||
<div className='description'>{article.title}</div>
|
||||
<div className='tags'><i className='fa fa-tags'/>{tags}</div>
|
||||
</div>
|
||||
|
||||
<span className='itemControl'>
|
||||
<button id='articleEditButton' onClick={this.openEditModal} className='editButton'>
|
||||
<i className='fa fa-edit fa-fw'></i>
|
||||
<div className='tooltip'>Edit</div>
|
||||
</button>
|
||||
<button onClick={this.openDeleteModal} className='deleteButton'>
|
||||
<i className='fa fa-trash fa-fw'></i>
|
||||
<div className='tooltip'>Delete</div>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div className='detailBody'>
|
||||
<MarkdownPreview className='content' content={article.content}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,102 +0,0 @@
|
||||
var React = require('react/addons')
|
||||
var ReactRouter = require('react-router')
|
||||
var moment = require('moment')
|
||||
|
||||
var ForceUpdate = require('../Mixins/ForceUpdate')
|
||||
var Markdown = require('../Mixins/Markdown')
|
||||
|
||||
var ProfileImage = require('../Components/ProfileImage')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [ReactRouter.Navigation, ReactRouter.State, ForceUpdate(60000), Markdown],
|
||||
propTypes: {
|
||||
articles: React.PropTypes.array,
|
||||
showOnlyWithTag: React.PropTypes.func
|
||||
},
|
||||
handleArticleClikck: function (article) {
|
||||
if (article.type === 'code') {
|
||||
return function (e) {
|
||||
var params = this.getParams()
|
||||
|
||||
document.getElementById('articleEditButton').focus()
|
||||
this.transitionTo('codes', {
|
||||
userName: params.userName,
|
||||
planetName: params.planetName,
|
||||
localId: article.localId
|
||||
})
|
||||
}.bind(this)
|
||||
}
|
||||
|
||||
if (article.type === 'note') {
|
||||
return function (e) {
|
||||
var params = this.getParams()
|
||||
|
||||
document.getElementById('articleEditButton').focus()
|
||||
this.transitionTo('notes', {
|
||||
userName: params.userName,
|
||||
planetName: params.planetName,
|
||||
localId: article.localId
|
||||
})
|
||||
}.bind(this)
|
||||
}
|
||||
},
|
||||
render: function () {
|
||||
var articles = this.props.articles.map(function (article) {
|
||||
var tags = article.Tags.length > 0 ? article.Tags.map(function (tag) {
|
||||
return (
|
||||
<a onClick={this.props.showOnlyWithTag(tag.name)} key={tag.id}>#{tag.name}</a>
|
||||
)
|
||||
}.bind(this)) : (
|
||||
<a className='noTag'>Not tagged yet</a>
|
||||
)
|
||||
var params = this.getParams()
|
||||
var isActive = article.type === 'code' ? this.isActive('codes') && parseInt(params.localId, 10) === article.localId : this.isActive('notes') && parseInt(params.localId, 10) === article.localId
|
||||
|
||||
if (article.type === 'code') {
|
||||
return (
|
||||
<li onClick={this.handleArticleClikck(article)} key={'code-' + article.id}>
|
||||
<div className={'articleItem' + (isActive ? ' active' : '')}>
|
||||
<div className='itemLeft'>
|
||||
<ProfileImage className='profileImage' size='25' email={article.User.email}/>
|
||||
<i className='fa fa-code fa-fw'></i>
|
||||
</div>
|
||||
<div className='itemRight'>
|
||||
<div className='itemInfo'>{moment(article.updatedAt).fromNow()} by <span className='userProfileName'>{article.User.profileName}</span></div>
|
||||
<div className='description'>{article.description}</div>
|
||||
<div className='tags'><i className='fa fa-tags'/>{tags}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='divider'></div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<li onClick={this.handleArticleClikck(article)} key={'note-' + article.id}>
|
||||
<div className={'articleItem blueprintItem' + (isActive ? ' active' : '')}>
|
||||
<div className='itemLeft'>
|
||||
<ProfileImage className='profileImage' size='25' email={article.User.email}/>
|
||||
<i className='fa fa-file-text-o fa-fw'></i>
|
||||
</div>
|
||||
|
||||
<div className='itemRight'>
|
||||
<div className='itemInfo'>{moment(article.updatedAt).fromNow()} by <span className='userProfileName'>{article.User.profileName}</span></div>
|
||||
<div className='description'>{article.title}</div>
|
||||
<div className='tags'><i className='fa fa-tags'/>{tags}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='divider'></div>
|
||||
</li>
|
||||
)
|
||||
|
||||
}.bind(this))
|
||||
|
||||
return (
|
||||
<div className='PlanetArticleList'>
|
||||
<ul ref='articles'>
|
||||
{articles}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,111 +0,0 @@
|
||||
/* global localStorage */
|
||||
|
||||
var React = require('react/addons')
|
||||
|
||||
var Hq = require('../Services/Hq')
|
||||
|
||||
var LinkedState = require('../Mixins/LinkedState')
|
||||
var KeyCaster = require('../Mixins/KeyCaster')
|
||||
|
||||
var PlanetStore = require('../Stores/PlanetStore')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [LinkedState, KeyCaster('planetCreateModal')],
|
||||
propTypes: {
|
||||
ownerName: React.PropTypes.string,
|
||||
transitionTo: React.PropTypes.func,
|
||||
close: React.PropTypes.func
|
||||
},
|
||||
getInitialState: function () {
|
||||
var currentUser = JSON.parse(localStorage.getItem('currentUser'))
|
||||
var ownerName = this.props.ownerName != null ? this.props.ownerName : currentUser.name
|
||||
return {
|
||||
user: currentUser,
|
||||
planet: {
|
||||
name: '',
|
||||
public: true
|
||||
},
|
||||
ownerName: ownerName,
|
||||
error: null
|
||||
}
|
||||
},
|
||||
componentDidMount: function () {
|
||||
React.findDOMNode(this.refs.name).focus()
|
||||
},
|
||||
onKeyCast: function (e) {
|
||||
switch (e.status) {
|
||||
case 'closeModal':
|
||||
this.props.close()
|
||||
break
|
||||
case 'submitPlanetCreateModal':
|
||||
this.handleSubmit()
|
||||
break
|
||||
}
|
||||
},
|
||||
handleSubmit: function () {
|
||||
this.setState({error: null}, function () {
|
||||
Hq.createPlanet(this.state.ownerName, this.state.planet)
|
||||
.then(function (res) {
|
||||
var planet = res.body
|
||||
|
||||
PlanetStore.Actions.update(planet)
|
||||
|
||||
if (this.props.transitionTo != null) {
|
||||
this.props.transitionTo('planetHome', {userName: planet.userName, planetName: planet.name})
|
||||
}
|
||||
|
||||
this.props.close()
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
|
||||
if (err.status == null) return this.setState({error: {message: 'Check your network connection'}})
|
||||
|
||||
switch (err.status) {
|
||||
case 403:
|
||||
this.setState({error: err.response.body})
|
||||
break
|
||||
case 422:
|
||||
this.setState({error: {message: 'Planet name should be Alphanumeric with _, -'}})
|
||||
break
|
||||
case 409:
|
||||
this.setState({error: {message: 'The entered name already in use'}})
|
||||
break
|
||||
default:
|
||||
this.setState({error: {message: 'Unexpected error occured! please try again'}})
|
||||
}
|
||||
}.bind(this))
|
||||
})
|
||||
},
|
||||
render: function () {
|
||||
var teamOptions = this.state.user.Teams.map(function (team) {
|
||||
return (
|
||||
<option key={'user-' + team.id} value={team.name}>{team.profileName} ({team.name})</option>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<div className='PlanetCreateModal modal'>
|
||||
<input ref='name' valueLink={this.linkState('planet.name')} className='nameInput stripInput' placeholder='Crate new Planet'/>
|
||||
|
||||
<div className='formField'>
|
||||
of
|
||||
<select valueLink={this.linkState('ownerName')}>
|
||||
<option value={this.state.user.name}>Me({this.state.user.name})</option>
|
||||
{teamOptions}
|
||||
</select>
|
||||
as
|
||||
<select valueLink={this.linkState('planet.public')}>
|
||||
<option value={true}>Public</option>
|
||||
<option value={false}>Private</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{this.state.error != null ? (<p className='errorAlert'>{this.state.error.message != null ? this.state.error.message : 'Error message undefined'}</p>) : null}
|
||||
|
||||
<button onClick={this.handleSubmit} className='submitButton'>
|
||||
<i className='fa fa-check'/>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,86 +0,0 @@
|
||||
var React = require('react/addons')
|
||||
var ReactRouter = require('react-router')
|
||||
var Link = ReactRouter.Link
|
||||
|
||||
var Modal = require('../Mixins/Modal')
|
||||
var ExternalLink = require('../Mixins/ExternalLink')
|
||||
|
||||
var PlanetSettingModal = require('./PlanetSettingModal')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [ReactRouter.State, Modal, ExternalLink],
|
||||
propTypes: {
|
||||
search: React.PropTypes.string,
|
||||
fetchPlanet: React.PropTypes.func,
|
||||
onSearchChange: React.PropTypes.func,
|
||||
currentPlanet: React.PropTypes.object
|
||||
},
|
||||
getInitialState: function () {
|
||||
return {
|
||||
search: ''
|
||||
}
|
||||
},
|
||||
componentDidMount: function () {
|
||||
var search = React.findDOMNode(this.refs.search)
|
||||
search.addEventListener('keydown', this.handleSearchKeyDown)
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
var search = React.findDOMNode(this.refs.search)
|
||||
search.removeEventListener('keydown', this.handleSearchKeyDown)
|
||||
},
|
||||
handleSearchKeyDown: function (e) {
|
||||
if (e.keyCode === 38 || e.keyCode === 40) {
|
||||
var search = React.findDOMNode(this.refs.search)
|
||||
search.blur()
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.keyCode !== 27 && (e.keyCode !== 13 || !e.metaKey)) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
},
|
||||
openPlanetSettingModal: function () {
|
||||
this.openModal(PlanetSettingModal, {planet: this.props.currentPlanet})
|
||||
},
|
||||
refresh: function () {
|
||||
this.props.fetchPlanet()
|
||||
},
|
||||
render: function () {
|
||||
var currentPlanetName = this.props.currentPlanet.name
|
||||
var currentUserName = this.props.currentPlanet.userName
|
||||
|
||||
return (
|
||||
<div className='PlanetHeader'>
|
||||
<div className='headerLabel'>
|
||||
<Link to='userHome' params={{userName: currentUserName}} className='userName'>{currentUserName}</Link>
|
||||
<span className='planetName'>{currentPlanetName}</span>
|
||||
|
||||
{this.props.currentPlanet.public ? null : (
|
||||
<div className='private'>
|
||||
<i className='fa fa-lock'/>
|
||||
<div className='tooltip'>Private planet</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button onClick={this.openPlanetSettingModal} className='planetSettingButton'>
|
||||
<i className='fa fa-chevron-down'></i>
|
||||
<div className='tooltip'>Planet setting</div>
|
||||
</button>
|
||||
</div>
|
||||
<div className='headerControl'>
|
||||
<div className='searchInput'>
|
||||
<i className='fa fa-search'/>
|
||||
<input onChange={this.props.onSearchChange} value={this.props.search} ref='search' type='text' className='inline-input circleInput' placeholder='Search...'/>
|
||||
</div>
|
||||
<button onClick={this.refresh} className='refreshButton'>
|
||||
<i className='fa fa-refresh'/>
|
||||
<div className='tooltip'>Refresh planet</div>
|
||||
</button>
|
||||
<a onClick={this.openExternal} href='http://b00st.io' className='logo'>
|
||||
<img width='44' height='44' src='resources/favicon-230x230.png'/>
|
||||
<div className='tooltip'>Boost official page</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,76 +0,0 @@
|
||||
var React = require('react/addons')
|
||||
var ReactRouter = require('react-router')
|
||||
var Navigation = ReactRouter.Navigation
|
||||
|
||||
var Modal = require('../Mixins/Modal')
|
||||
|
||||
var LaunchModal = require('../Components/LaunchModal')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [Modal, Navigation],
|
||||
propTypes: {
|
||||
planet: React.PropTypes.shape({
|
||||
name: React.PropTypes.string,
|
||||
Owner: React.PropTypes.shape({
|
||||
id: React.PropTypes.number,
|
||||
userType: React.PropTypes.string
|
||||
})
|
||||
}),
|
||||
search: React.PropTypes.string,
|
||||
toggleCodeFilter: React.PropTypes.func,
|
||||
toggleNoteFilter: React.PropTypes.func,
|
||||
currentUser: React.PropTypes.shape({
|
||||
id: React.PropTypes.number,
|
||||
userType: React.PropTypes.string,
|
||||
Teams: React.PropTypes.array
|
||||
})
|
||||
},
|
||||
getInitialState: function () {
|
||||
return {
|
||||
isLaunchModalOpen: false
|
||||
}
|
||||
},
|
||||
openLaunchModal: function () {
|
||||
this.openModal(LaunchModal, {planet: this.props.planet, transitionTo: this.transitionTo})
|
||||
},
|
||||
isMyPlanet: function () {
|
||||
if (this.props.currentUser == null) return false
|
||||
if (this.props.planet.Owner.userType === 'person' && this.props.planet.Owner.id !== this.props.currentUser.id) return false
|
||||
if (this.props.planet.Owner.userType === 'team' && !this.props.currentUser.Teams.some(function (team) {
|
||||
if (team.id === this.props.planet.Owner.id) return true
|
||||
return false
|
||||
}.bind(this))) return false
|
||||
|
||||
return true
|
||||
|
||||
},
|
||||
render: function () {
|
||||
var keywords = this.props.search.split(' ')
|
||||
var usingCodeFilter = keywords.some(function (keyword) {
|
||||
if (keyword === '$c') return true
|
||||
return false
|
||||
})
|
||||
var usingNoteFilter = keywords.some(function (keyword) {
|
||||
if (keyword === '$n') return true
|
||||
return false
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='PlanetNavigator'>
|
||||
{this.isMyPlanet() ? (
|
||||
<button onClick={this.openLaunchModal} className='launchButton btn-primary btn-block'>
|
||||
<i className='fa fa-rocket fa-fw'/> Launch
|
||||
</button>
|
||||
) : null}
|
||||
<nav className='articleFilters'>
|
||||
<a className={usingCodeFilter && !usingNoteFilter ? 'active' : ''} onClick={this.props.toggleCodeFilter}>
|
||||
<i className='fa fa-code fa-fw'/> Codes
|
||||
</a>
|
||||
<a className={!usingCodeFilter && usingNoteFilter ? 'active' : ''} onClick={this.props.toggleNoteFilter}>
|
||||
<i className='fa fa-file-text-o fa-fw'/> Notes
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,188 +0,0 @@
|
||||
var React = require('react/addons')
|
||||
|
||||
var Hq = require('../Services/Hq')
|
||||
|
||||
var LinkedState = require('../Mixins/LinkedState')
|
||||
var KeyCaster = require('../Mixins/KeyCaster')
|
||||
|
||||
var PlanetStore = require('../Stores/PlanetStore')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [LinkedState, KeyCaster('planetSettingModal')],
|
||||
propTypes: {
|
||||
close: React.PropTypes.func,
|
||||
planet: React.PropTypes.shape({
|
||||
name: React.PropTypes.string,
|
||||
public: React.PropTypes.bool,
|
||||
userName: React.PropTypes.string
|
||||
})
|
||||
},
|
||||
getInitialState: function () {
|
||||
var deleteTextCandidates = [
|
||||
'Confirm',
|
||||
'Exterminatus',
|
||||
'Avada Kedavra'
|
||||
]
|
||||
var random = Math.round(Math.random() * 10) % 10
|
||||
var randomDeleteText = random > 1 ? deleteTextCandidates[0] : random === 1 ? deleteTextCandidates[1] : deleteTextCandidates[2]
|
||||
|
||||
return {
|
||||
currentTab: 'profile',
|
||||
planet: {
|
||||
name: this.props.planet.name,
|
||||
public: this.props.planet.public
|
||||
},
|
||||
randomDeleteText: randomDeleteText,
|
||||
deleteConfirmation: ''
|
||||
}
|
||||
},
|
||||
onKeyCast: function (e) {
|
||||
switch (e.status) {
|
||||
case 'closeModal':
|
||||
this.props.close()
|
||||
break
|
||||
}
|
||||
},
|
||||
activePlanetProfile: function () {
|
||||
this.setState({currentTab: 'profile'})
|
||||
},
|
||||
activePlanetDelete: function () {
|
||||
this.setState({currentTab: 'delete'})
|
||||
},
|
||||
handlePublicChange: function (value) {
|
||||
return function () {
|
||||
this.state.planet.public = value
|
||||
this.setState({planet: this.state.planet})
|
||||
}.bind(this)
|
||||
},
|
||||
handleSavePlanetProfile: function (e) {
|
||||
var planet = this.props.planet
|
||||
|
||||
this.setState({profileFormStatus: 'sending', profileFormError: null}, function () {
|
||||
Hq.updatePlanet(planet.userName, planet.name, this.state.planet)
|
||||
.then(function (res) {
|
||||
var planet = res.body
|
||||
console.log(planet)
|
||||
this.setState({profileFormStatus: 'done'})
|
||||
|
||||
PlanetStore.Actions.update(planet)
|
||||
this.props.close()
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
var newState = {
|
||||
profileFormStatus: 'error'
|
||||
}
|
||||
|
||||
if (err.status == null) {
|
||||
newState.profileFormError = {message: 'Check your network connection'}
|
||||
return this.setState(newState)
|
||||
}
|
||||
|
||||
switch (err.status) {
|
||||
case 403:
|
||||
newState.profileFormError = err.response.body
|
||||
this.setState(newState)
|
||||
break
|
||||
case 422:
|
||||
newState.profileFormError = {message: 'Planet name should be Alphanumeric with _, -'}
|
||||
this.setState(newState)
|
||||
break
|
||||
case 409:
|
||||
newState.profileFormError = {message: 'The entered name already in use'}
|
||||
this.setState(newState)
|
||||
break
|
||||
default:
|
||||
newState.profileFormError = {message: 'Undefined error please try again'}
|
||||
this.setState(newState)
|
||||
}
|
||||
}.bind(this))
|
||||
})
|
||||
},
|
||||
handleDeletePlanetClick: function () {
|
||||
var planet = this.props.planet
|
||||
|
||||
this.setState({deleteSubmitStatus: 'sending'}, function () {
|
||||
Hq.destroyPlanet(planet.userName, planet.name)
|
||||
.then(function (res) {
|
||||
var planet = res.body
|
||||
|
||||
PlanetStore.Actions.destroy(planet)
|
||||
this.setState({deleteSubmitStatus: 'done'}, function () {
|
||||
this.props.close()
|
||||
})
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
this.setState({deleteSubmitStatus: 'error'})
|
||||
console.error(err)
|
||||
}.bind(this))
|
||||
})
|
||||
|
||||
},
|
||||
render: function () {
|
||||
var content
|
||||
|
||||
content = this.state.currentTab === 'profile' ? this.renderPlanetProfileTab() : this.renderPlanetDeleteTab()
|
||||
|
||||
return (
|
||||
<div className='PlanetSettingModal sideNavModal modal'>
|
||||
<div className='leftPane'>
|
||||
<h1 className='modalLabel'>Planet setting</h1>
|
||||
<nav className='tabList'>
|
||||
<button onClick={this.activePlanetProfile} className={this.state.currentTab === 'profile' ? 'active' : ''}><i className='fa fa-globe fa-fw'/> Planet profile</button>
|
||||
<button onClick={this.activePlanetDelete} className={this.state.currentTab === 'delete' ? 'active' : ''}><i className='fa fa-trash fa-fw'/> Delete Planet</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className='rightPane'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
renderPlanetProfileTab: function () {
|
||||
return (
|
||||
<div className='planetProfileTab tab'>
|
||||
<div className='formField'>
|
||||
<label>Planet name </label>
|
||||
<input valueLink={this.linkState('planet.name')}/>
|
||||
</div>
|
||||
|
||||
<div className='formRadioField'>
|
||||
<input id='publicOption' checked={this.state.planet.public} onChange={this.handlePublicChange(true)} name='public' type='radio'/> <label htmlFor='publicOption'>Public</label>
|
||||
|
||||
<input id='privateOption' checked={!this.state.planet.public} onChange={this.handlePublicChange(false)} name='public' type='radio'/> <label htmlFor='privateOption'>Private</label>
|
||||
</div>
|
||||
<div className='formConfirm'>
|
||||
<button onClick={this.handleSavePlanetProfile} className='saveButton btn-primary'>Save</button>
|
||||
|
||||
<div className={'alertInfo' + (this.state.profileFormStatus === 'sending' ? '' : ' hide')}>on Sending...</div>
|
||||
|
||||
<div className={'alertError' + (this.state.profileFormStatus === 'error' ? '' : ' hide')}>{this.state.profileFormError != null ? this.state.profileFormError.message : 'Unexpected error occured! please try again'}</div>
|
||||
|
||||
<div className={'alertSuccess' + (this.state.profileFormStatus === 'done' ? '' : ' hide')}>Successfully done!!</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
renderPlanetDeleteTab: function () {
|
||||
var disabled = !this.state.deleteConfirmation.match(new RegExp('^' + this.props.planet.userName + '/' + this.props.planet.name + '$'))
|
||||
|
||||
return (
|
||||
<div className='planetDeleteTab tab'>
|
||||
<p>Are you sure to destroy <strong>'{this.props.planet.userName + '/' + this.props.planet.name}'</strong>?</p>
|
||||
<p>If you are sure, write <strong>'{this.props.planet.userName + '/' + this.props.planet.name}'</strong> to input below and click <strong>'{this.state.randomDeleteText}'</strong> button.</p>
|
||||
<input valueLink={this.linkState('deleteConfirmation')} placeholder='userName/planetName'/>
|
||||
<div className='formConfirm'>
|
||||
<button disabled={disabled} onClick={this.handleDeletePlanetClick}>{this.state.randomDeleteText}</button>
|
||||
|
||||
<div className={'alertInfo' + (this.state.deleteSubmitStatus === 'sending' ? '' : ' hide')}>on Sending...</div>
|
||||
|
||||
<div className={'alertError' + (this.state.deleteSubmitStatus === 'error' ? '' : ' hide')}>Connection failed.. Try again.</div>
|
||||
|
||||
<div className={'alertSuccess' + (this.state.deleteSubmitStatus === 'done' ? '' : ' hide')}>Successfully done!!</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,112 +0,0 @@
|
||||
var ipc = require('ipc')
|
||||
var remote = require('remote')
|
||||
|
||||
var React = require('react/addons')
|
||||
|
||||
var LinkedState = require('../Mixins/LinkedState')
|
||||
var ExternalLink = require('../Mixins/ExternalLink')
|
||||
var KeyCaster = require('../Mixins/KeyCaster')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [LinkedState, ExternalLink, KeyCaster('aboutModal')],
|
||||
propTypes: {
|
||||
close: React.PropTypes.func
|
||||
},
|
||||
getInitialState: function () {
|
||||
var keymap = remote.getGlobal('keymap')
|
||||
console.log(keymap)
|
||||
return {
|
||||
currentTab: 'settings',
|
||||
keymap: keymap
|
||||
}
|
||||
},
|
||||
onKeyCast: function (e) {
|
||||
switch (e.status) {
|
||||
case 'closeModal':
|
||||
this.props.close()
|
||||
break
|
||||
}
|
||||
},
|
||||
activeSettings: function () {
|
||||
this.setState({currentTab: 'settings'})
|
||||
},
|
||||
activeAbout: function () {
|
||||
this.setState({currentTab: 'about'})
|
||||
},
|
||||
saveKeymap: function () {
|
||||
ipc.send('hotkeyUpdated', JSON.stringify(this.state.keymap))
|
||||
},
|
||||
render: function () {
|
||||
var content = this.state.currentTab === 'settings' ? this.renderSettingsTab() : this.renderAboutTab()
|
||||
|
||||
return (
|
||||
<div className='PreferencesModal sideNavModal modal'>
|
||||
<div className='leftPane'>
|
||||
<h1 className='modalLabel'>Preferences</h1>
|
||||
<nav className='tabList'>
|
||||
<button onClick={this.activeSettings} className={this.state.currentTab === 'settings' ? 'active' : ''}><i className='fa fa-gear fa-fw'/> Settings</button>
|
||||
<button onClick={this.activeAbout} className={this.state.currentTab === 'about' ? 'active' : ''}><i className='fa fa-info-circle fa-fw'/> About this app</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div className='rightPane'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
renderSettingsTab: function () {
|
||||
return (
|
||||
<div className='settingsTab tab'>
|
||||
<div className='categoryLabel'>Hotkey</div>
|
||||
<div className='formField'>
|
||||
<label>Toggle finder</label>
|
||||
<input valueLink={this.linkState('keymap.toggleFinder')}/>
|
||||
</div>
|
||||
<div className='formConfirm'>
|
||||
<button onClick={this.saveKeymap}>Save</button>
|
||||
</div>
|
||||
<div className='example'>
|
||||
<h3>Example</h3>
|
||||
<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>
|
||||
)
|
||||
},
|
||||
renderAboutTab: function () {
|
||||
var version = global.version
|
||||
return (
|
||||
<div className='aboutTab tab'>
|
||||
<div className='about1'>
|
||||
<img className='logo' src='resources/favicon-230x230.png'/>
|
||||
<div className='appInfo'>Boost {version == null || version.length === 0 ? 'DEV version' : 'v' + version}</div>
|
||||
</div>
|
||||
|
||||
<div className='about2'>
|
||||
<div className='externalLabel'>External links</div>
|
||||
<ul className='externalList'>
|
||||
<li><a onClick={this.openExternal} href='http://b00st.io'>Boost Homepage <i className='fa fa-external-link'/></a></li>
|
||||
<li><a onClick={this.openExternal} href='http://boostio.github.io/regulations.html'>Regulation <i className='fa fa-external-link'/></a></li>
|
||||
<li><a onClick={this.openExternal} href='http://boostio.github.io/privacypolicies.html'>Private policy <i className='fa fa-external-link'/></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,15 +0,0 @@
|
||||
var React = require('react/addons')
|
||||
var md5 = require('md5')
|
||||
|
||||
module.exports = React.createClass({
|
||||
propTypes: {
|
||||
email: React.PropTypes.string,
|
||||
size: React.PropTypes.string,
|
||||
className: React.PropTypes.string
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<img className={this.props.className} width={this.props.size} height={this.props.size} src={'http://www.gravatar.com/avatar/' + md5(this.props.email.trim().toLowerCase()) + '?s=' + this.props.size}/>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,87 +0,0 @@
|
||||
/* global localStorage */
|
||||
|
||||
var React = require('react/addons')
|
||||
|
||||
var Hq = require('../Services/Hq')
|
||||
|
||||
var LinkedState = require('../Mixins/LinkedState')
|
||||
var KeyCaster = require('../Mixins/KeyCaster')
|
||||
|
||||
var UserStore = require('../Stores/UserStore')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [LinkedState, KeyCaster('teamCreateModal')],
|
||||
propTypes: {
|
||||
user: React.PropTypes.shape({
|
||||
name: React.PropTypes.string
|
||||
}),
|
||||
transitionTo: React.PropTypes.func,
|
||||
close: React.PropTypes.func
|
||||
},
|
||||
getInitialState: function () {
|
||||
return {
|
||||
team: {
|
||||
name: ''
|
||||
},
|
||||
error: null
|
||||
}
|
||||
},
|
||||
componentDidMount: function () {
|
||||
React.findDOMNode(this.refs.teamName).focus()
|
||||
},
|
||||
onKeyCast: function (e) {
|
||||
switch (e.status) {
|
||||
case 'closeModal':
|
||||
this.props.close()
|
||||
break
|
||||
case 'submitTeamCreateModal':
|
||||
this.handleSubmit()
|
||||
break
|
||||
}
|
||||
},
|
||||
handleSubmit: function () {
|
||||
this.setState({error: null}, function () {
|
||||
Hq.createTeam(this.props.user.name, this.state.team)
|
||||
.then(function (res) {
|
||||
var currentUser = JSON.parse(localStorage.getItem('currentUser'))
|
||||
var team = res.body
|
||||
|
||||
currentUser.Teams.push(team)
|
||||
localStorage.setItem('currentUser', JSON.stringify(currentUser))
|
||||
UserStore.Actions.update(currentUser)
|
||||
|
||||
if (this.props.transitionTo != null) {
|
||||
this.props.transitionTo('userHome', {userName: team.name})
|
||||
}
|
||||
this.props.close()
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
|
||||
if (err.status == null) return this.setState({error: {message: 'Check your network connection'}})
|
||||
|
||||
switch (err.status) {
|
||||
case 422:
|
||||
this.setState({error: {message: 'Team name should be Alphanumeric with _, -'}})
|
||||
break
|
||||
case 409:
|
||||
this.setState({error: {message: 'The entered name already in use'}})
|
||||
break
|
||||
default:
|
||||
this.setState({error: {message: 'Error message undefined'}})
|
||||
}
|
||||
}.bind(this))
|
||||
})
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<div className='TeamCreateModal modal'>
|
||||
<input ref='teamName' valueLink={this.linkState('team.name')} className='nameInput stripInput' placeholder='Create new team'/>
|
||||
{this.state.error != null ? (<p className='errorAlert'>{this.state.error.message != null ? this.state.error.message : 'Unintended error occured'}</p>) : null}
|
||||
<button onClick={this.handleSubmit} className='submitButton'>
|
||||
<i className='fa fa-check'/>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,282 +0,0 @@
|
||||
/* global localStorage */
|
||||
|
||||
var React = require('react/addons')
|
||||
var Reflux = require('reflux')
|
||||
var Select = require('react-select')
|
||||
|
||||
var Hq = require('../Services/Hq')
|
||||
|
||||
var LinkedState = require('../Mixins/LinkedState')
|
||||
var Helper = require('../Mixins/Helper')
|
||||
var KeyCaster = require('../Mixins/KeyCaster')
|
||||
|
||||
var UserStore = require('../Stores/UserStore')
|
||||
|
||||
var getOptions = function (input, callback) {
|
||||
Hq.searchUser(input)
|
||||
.then(function (res) {
|
||||
callback(null, {
|
||||
options: res.body.map(function (user) {
|
||||
return {
|
||||
label: user.name,
|
||||
value: user.name
|
||||
}
|
||||
}),
|
||||
complete: false
|
||||
})
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [LinkedState, Reflux.listenTo(UserStore, 'onUserChange'), Helper, KeyCaster('teamSettingsModal')],
|
||||
propTypes: {
|
||||
team: React.PropTypes.shape({
|
||||
id: React.PropTypes.number,
|
||||
name: React.PropTypes.string,
|
||||
profileName: React.PropTypes.string,
|
||||
email: React.PropTypes.string,
|
||||
Members: React.PropTypes.array
|
||||
}),
|
||||
close: React.PropTypes.func
|
||||
},
|
||||
getInitialState: function () {
|
||||
var team = this.props.team
|
||||
return {
|
||||
currentTab: 'teamInfo',
|
||||
team: {
|
||||
profileName: team.profileName
|
||||
},
|
||||
userSubmitStatus: null,
|
||||
member: {
|
||||
name: '',
|
||||
role: 'member'
|
||||
},
|
||||
updatingMember: false
|
||||
}
|
||||
},
|
||||
onKeyCast: function (e) {
|
||||
switch (e.status) {
|
||||
case 'closeModal':
|
||||
this.props.close()
|
||||
break
|
||||
}
|
||||
},
|
||||
onUserChange: function (res) {
|
||||
var member
|
||||
switch (res.status) {
|
||||
case 'memberAdded':
|
||||
member = res.data
|
||||
if (member.TeamMember.TeamId === this.props.team.id) {
|
||||
this.forceUpdate()
|
||||
}
|
||||
break
|
||||
case 'memberRemoved':
|
||||
member = res.data
|
||||
if (member.TeamMember.TeamId === this.props.team.id) {
|
||||
this.forceUpdate()
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
selectTab: function (tabName) {
|
||||
return function () {
|
||||
this.setState({currentTab: tabName})
|
||||
}.bind(this)
|
||||
},
|
||||
saveUserInfo: function () {
|
||||
this.setState({
|
||||
userSubmitStatus: 'sending'
|
||||
}, function () {
|
||||
Hq.updateUser(this.props.team.name, this.state.team)
|
||||
.then(function (res) {
|
||||
this.setState({userSubmitStatus: 'done'}, function () {
|
||||
UserStore.Actions.update(res.body)
|
||||
this.forceUpdate()
|
||||
})
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
this.setState({userSubmitStatus: 'error'})
|
||||
}.bind(this))
|
||||
})
|
||||
},
|
||||
handleMemberNameChange: function (value) {
|
||||
var member = this.state.member
|
||||
member.name = value
|
||||
this.setState({member: member})
|
||||
},
|
||||
addMember: function () {
|
||||
this.setState({updatingMember: true}, function () {
|
||||
Hq
|
||||
.addMember(this.props.team.name, {
|
||||
userName: this.state.member.name,
|
||||
role: this.state.member.role
|
||||
})
|
||||
.then(function (res) {
|
||||
UserStore.Actions.addMember(res.body)
|
||||
this.setState({updatingMember: false})
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
this.setState({updatingMember: false})
|
||||
}.bind(this))
|
||||
})
|
||||
},
|
||||
roleChange: function (memberName) {
|
||||
return function (e) {
|
||||
var role = e.target.value
|
||||
this.setState({updatingMember: true}, function () {
|
||||
Hq
|
||||
.addMember(this.props.team.name, {
|
||||
userName: memberName,
|
||||
role: role
|
||||
})
|
||||
.then(function (res) {
|
||||
UserStore.Actions.addMember(res.body)
|
||||
this.setState({updatingMember: false})
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
this.setState({updatingMember: false})
|
||||
}.bind(this))
|
||||
})
|
||||
}.bind(this)
|
||||
},
|
||||
removeMember: function (memberName) {
|
||||
return function () {
|
||||
this.setState({updatingMember: true}, function () {
|
||||
Hq
|
||||
.removeMember(this.props.team.name, {
|
||||
userName: memberName
|
||||
})
|
||||
.then(function (res) {
|
||||
UserStore.Actions.removeMember(res.body)
|
||||
this.setState({updatingMember: false})
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
this.setState({updatingMember: false})
|
||||
}.bind(this))
|
||||
})
|
||||
}.bind(this)
|
||||
},
|
||||
render: function () {
|
||||
var content
|
||||
|
||||
switch (this.state.currentTab) {
|
||||
case 'teamInfo':
|
||||
content = this.renderTeamInfoTab()
|
||||
break
|
||||
case 'members':
|
||||
content = this.renderMembersTab()
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='TeamSettingsModal sideNavModal modal'>
|
||||
<div className='leftPane'>
|
||||
<div className='modalLabel'>Team settings</div>
|
||||
<div className='tabList'>
|
||||
<button className={this.state.currentTab === 'teamInfo' ? 'active' : ''} onClick={this.selectTab('teamInfo')}><i className='fa fa-info-circle fa-fw'/> Team Info</button>
|
||||
<button className={this.state.currentTab === 'members' ? 'active' : ''} onClick={this.selectTab('members')}><i className='fa fa-users fa-fw'/> Members</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='rightPane'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
renderTeamInfoTab: function () {
|
||||
return (
|
||||
<div className='userInfoTab tab'>
|
||||
<div className='formField'>
|
||||
<label>Profile Name</label>
|
||||
<input valueLink={this.linkState('team.profileName')}/>
|
||||
</div>
|
||||
<div className='formConfirm'>
|
||||
<button disabled={this.state.userSubmitStatus === 'sending'} onClick={this.saveUserInfo}>Save</button>
|
||||
|
||||
<div className={'alertInfo' + (this.state.userSubmitStatus === 'sending' ? '' : ' hide')}>on Sending...</div>
|
||||
|
||||
<div className={'alertError' + (this.state.userSubmitStatus === 'error' ? '' : ' hide')}>Connection failed.. Try again.</div>
|
||||
|
||||
<div className={'alertSuccess' + (this.state.userSubmitStatus === 'done' ? '' : ' hide')}>Successfully done!!</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
renderMembersTab: function () {
|
||||
var currentUser = JSON.parse(localStorage.getItem('currentUser'))
|
||||
|
||||
var members = this.props.team.Members.map(function (member) {
|
||||
var isCurrentUser = currentUser.id === member.id
|
||||
return (
|
||||
<tr>
|
||||
<td>{member.profileName}({member.name})</td>
|
||||
<td>
|
||||
{isCurrentUser ? (
|
||||
'Owner'
|
||||
) : (
|
||||
<select disabled={this.state.updatingMember} onChange={this.roleChange(member.name)} className='roleSelect' value={member.TeamMember.role}>
|
||||
<option value='owner'>Owner</option>
|
||||
<option value='member'>Member</option>
|
||||
</select>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{isCurrentUser ? '-' : (
|
||||
<button disabled={this.state.updatingMember} onClick={this.removeMember(member.name)}><i className='fa fa-close fa-fw'/></button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}.bind(this))
|
||||
|
||||
var belowLimit = members.length < 5
|
||||
|
||||
return (
|
||||
<div className='membersTab tab'>
|
||||
<table className='memberTable'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Role</th>
|
||||
<th>Control</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{members}
|
||||
</tbody>
|
||||
</table>
|
||||
{belowLimit ? (
|
||||
<div className='addMemberForm'>
|
||||
<div className='formLabel'>Add Member</div>
|
||||
<div className='formGroup'>
|
||||
<Select
|
||||
name='userName'
|
||||
value={this.state.member.name}
|
||||
placeholder='Username to add'
|
||||
asyncOptions={getOptions}
|
||||
onChange={this.handleMemberNameChange}
|
||||
className='userNameSelect'
|
||||
/>
|
||||
<select valueLink={this.linkState('member.role')} className='roleSelect'>
|
||||
<option value={'member'}>Member</option>
|
||||
<option value={'owner'}>Owner</option>
|
||||
</select>
|
||||
<button disabled={this.state.updatingMember} onClick={this.addMember} className='confirmButton'>Add Member</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
Maximum number of members is 5 on Beta version. Please contact us if you want futher use.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,41 +0,0 @@
|
||||
/* global localStorage */
|
||||
|
||||
var React = require('react/addons')
|
||||
var ReactRouter = require('react-router')
|
||||
var RouteHandler = ReactRouter.RouteHandler
|
||||
var State = ReactRouter.State
|
||||
var Navigation = ReactRouter.Navigation
|
||||
|
||||
var AuthFilter = require('../Mixins/AuthFilter')
|
||||
var KeyCaster = require('../Mixins/KeyCaster')
|
||||
|
||||
var HomeNavigator = require('../Components/HomeNavigator')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [AuthFilter.OnlyUser, State, Navigation, KeyCaster('homeContainer')],
|
||||
componentDidMount: function () {
|
||||
if (this.isActive('homeEmpty')) {
|
||||
var user = JSON.parse(localStorage.getItem('currentUser'))
|
||||
if (user.Planets != null && user.Planets.length > 0) {
|
||||
this.transitionTo('planet', {userName: user.name, planetName: user.Planets[0].name})
|
||||
return
|
||||
}
|
||||
this.transitionTo('userHome', {userName: user.name})
|
||||
}
|
||||
},
|
||||
onKeyCast: function (e) {
|
||||
switch (e.status) {
|
||||
case 'switchPlanet':
|
||||
this.refs.navigator.switchPlanetByIndex(e.data)
|
||||
break
|
||||
}
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<div className='HomeContainer'>
|
||||
<HomeNavigator ref='navigator'/>
|
||||
<RouteHandler/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,106 +0,0 @@
|
||||
/* global localStorage */
|
||||
var React = require('react/addons')
|
||||
var ReactRouter = require('react-router')
|
||||
var Link = ReactRouter.Link
|
||||
|
||||
var AuthFilter = require('../Mixins/AuthFilter')
|
||||
var LinkedState = require('../Mixins/LinkedState')
|
||||
var Hq = require('../Services/Hq')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [LinkedState, ReactRouter.Navigation, AuthFilter.OnlyGuest],
|
||||
getInitialState: function () {
|
||||
return {
|
||||
user: {},
|
||||
authenticationFailed: false,
|
||||
connectionFailed: false,
|
||||
isSending: false
|
||||
}
|
||||
},
|
||||
onListen: function (res) {
|
||||
if (res.status === 'failedToLogIn') {
|
||||
if (res.data.status === 401) {
|
||||
// Wrong E-mail or Password
|
||||
this.setState({
|
||||
authenticationFailed: true,
|
||||
connectionFailed: false,
|
||||
isSending: false
|
||||
})
|
||||
return
|
||||
}
|
||||
// Connection Failed or Whatever
|
||||
this.setState({
|
||||
authenticationFailed: false,
|
||||
connectionFailed: true,
|
||||
isSending: false
|
||||
})
|
||||
return
|
||||
}
|
||||
},
|
||||
handleSubmit: function (e) {
|
||||
this.setState({
|
||||
authenticationFailed: false,
|
||||
connectionFailed: false,
|
||||
isSending: true
|
||||
}, function () {
|
||||
Hq.login(this.state.user)
|
||||
.then(function (res) {
|
||||
localStorage.setItem('token', res.body.token)
|
||||
localStorage.setItem('currentUser', JSON.stringify(res.body.user))
|
||||
|
||||
this.transitionTo('userHome', {userName: res.body.user.name})
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
if (err.status === 401) {
|
||||
this.setState({
|
||||
authenticationFailed: true,
|
||||
connectionFailed: false,
|
||||
isSending: false
|
||||
})
|
||||
return
|
||||
}
|
||||
this.setState({
|
||||
authenticationFailed: false,
|
||||
connectionFailed: true,
|
||||
isSending: false
|
||||
})
|
||||
}.bind(this))
|
||||
})
|
||||
|
||||
e.preventDefault()
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<div className='LoginContainer'>
|
||||
<img className='logo' src='resources/favicon-230x230.png'/>
|
||||
|
||||
<nav className='authNavigator text-center'><Link to='login'>Log In</Link> / <Link to='signup'>Sign Up</Link></nav>
|
||||
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className='form-group'>
|
||||
<input className='stripInput' valueLink={this.linkState('user.email')} type='text' placeholder='E-mail'/>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<input className='stripInput' 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.connectionFailed ? (
|
||||
<p className='alertError'>Please try again.</p>
|
||||
) : null}
|
||||
|
||||
{this.state.authenticationFailed ? (
|
||||
<p className='alertError'>Wrong E-mail or Password.</p>
|
||||
) : null}
|
||||
|
||||
<div className='form-group'>
|
||||
<button className='logInButton' type='submit'>Log In</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,106 +0,0 @@
|
||||
/* global localStorage */
|
||||
|
||||
var ipc = require('ipc')
|
||||
|
||||
var React = require('react/addons')
|
||||
var ReactRouter = require('react-router')
|
||||
var RouteHandler = ReactRouter.RouteHandler
|
||||
var Navigation = ReactRouter.Navigation
|
||||
var State = ReactRouter.State
|
||||
|
||||
var Hq = require('../Services/Hq')
|
||||
|
||||
var Modal = require('../Mixins/Modal')
|
||||
|
||||
var UserStore = require('../Stores/UserStore')
|
||||
|
||||
var ContactModal = require('../Components/ContactModal')
|
||||
|
||||
function fetchPlanet (userName, planetName) {
|
||||
Hq.fetchPlanet(userName, planetName)
|
||||
.then(function (res) {
|
||||
var planet = res.body
|
||||
|
||||
planet.Codes.forEach(function (code) {
|
||||
code.type = 'code'
|
||||
})
|
||||
|
||||
planet.Notes.forEach(function (note) {
|
||||
note.type = 'note'
|
||||
})
|
||||
|
||||
console.log('planet-' + planet.id + ' fetched!')
|
||||
localStorage.setItem('planet-' + planet.id, JSON.stringify(planet))
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [State, Navigation, Modal],
|
||||
getInitialState: function () {
|
||||
return {
|
||||
updateAvailable: false
|
||||
}
|
||||
},
|
||||
componentDidMount: function () {
|
||||
ipc.on('update-available', function (message) {
|
||||
this.setState({updateAvailable: true})
|
||||
}.bind(this))
|
||||
|
||||
if (this.isActive('root')) {
|
||||
if (localStorage.getItem('currentUser') == null) {
|
||||
this.transitionTo('login')
|
||||
return
|
||||
} else {
|
||||
this.transitionTo('home')
|
||||
}
|
||||
}
|
||||
|
||||
Hq.getUser()
|
||||
.then(function (res) {
|
||||
var user = res.body
|
||||
localStorage.setItem('currentUser', JSON.stringify(user))
|
||||
UserStore.Actions.update(user)
|
||||
|
||||
user.Planets.forEach(function (planet) {
|
||||
fetchPlanet(planet.userName, planet.name)
|
||||
})
|
||||
user.Teams.forEach(function (team) {
|
||||
team.Planets.forEach(function (planet) {
|
||||
fetchPlanet(planet.userName, planet.name)
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (err.status === 401) {
|
||||
console.log('Not logged in yet')
|
||||
localStorage.removeItem('currentUser')
|
||||
this.transitionTo('login')
|
||||
return
|
||||
}
|
||||
console.error(err)
|
||||
}.bind(this))
|
||||
},
|
||||
updateApp: function () {
|
||||
ipc.send('update-app', 'Deal with it.')
|
||||
},
|
||||
openContactModal: function () {
|
||||
this.openModal(ContactModal)
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<div className='Main'>
|
||||
{this.state.updateAvailable ? (
|
||||
<button onClick={this.updateApp} className='appUpdateButton'><i className='fa fa-cloud-download'/> Update available!</button>
|
||||
) : null}
|
||||
<button onClick={this.openContactModal} className='contactButton'>
|
||||
<i className='fa fa-paper-plane-o'/>
|
||||
<div className='tooltip'>Contact us</div>
|
||||
</button>
|
||||
<RouteHandler/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,415 +0,0 @@
|
||||
/* global localStorage*/
|
||||
'strict'
|
||||
var React = require('react/addons')
|
||||
var ReactRouter = require('react-router')
|
||||
var Reflux = require('reflux')
|
||||
|
||||
var PlanetHeader = require('../Components/PlanetHeader')
|
||||
var PlanetNavigator = require('../Components/PlanetNavigator')
|
||||
var PlanetArticleList = require('../Components/PlanetArticleList')
|
||||
var PlanetArticleDetail = require('../Components/PlanetArticleDetail')
|
||||
|
||||
var Hq = require('../Services/Hq')
|
||||
|
||||
var Modal = require('../Mixins/Modal')
|
||||
var ArticleFilter = require('../Mixins/ArticleFilter')
|
||||
var Helper = require('../Mixins/Helper')
|
||||
var KeyCaster = require('../Mixins/KeyCaster')
|
||||
|
||||
var UserStore = require('../Stores/UserStore')
|
||||
var PlanetStore = require('../Stores/PlanetStore')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [ReactRouter.Navigation, ReactRouter.State, Modal, Reflux.listenTo(UserStore, 'onUserChange'), Reflux.listenTo(PlanetStore, 'onPlanetChange'), ArticleFilter, Helper, KeyCaster('planetContainer')],
|
||||
propTypes: {
|
||||
params: React.PropTypes.object,
|
||||
planetName: React.PropTypes.string
|
||||
},
|
||||
getInitialState: function () {
|
||||
return {
|
||||
currentUser: JSON.parse(localStorage.getItem('currentUser')),
|
||||
planet: null,
|
||||
search: ''
|
||||
}
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this.fetchPlanet(this.props.params.userName, this.props.params.planetName)
|
||||
},
|
||||
componentDidUpdate: function () {
|
||||
if (this.isActive('planetHome') && this.refs.list != null && this.refs.list.props.articles.length > 0) {
|
||||
var article = this.refs.list.props.articles[0]
|
||||
var planet = this.state.planet
|
||||
switch (article.type) {
|
||||
case 'code':
|
||||
this.transitionTo('codes', {userName: planet.userName, planetName: planet.name, localId: article.localId})
|
||||
break
|
||||
case 'note':
|
||||
this.transitionTo('notes', {userName: planet.userName, planetName: planet.name, localId: article.localId})
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
componentWillReceiveProps: function (nextProps) {
|
||||
if (this.state.planet == null) {
|
||||
this.fetchPlanet(nextProps.params.userName, nextProps.params.planetName)
|
||||
return
|
||||
}
|
||||
|
||||
if (nextProps.params.userName !== this.state.planet.userName || nextProps.params.planetName !== this.state.planet.name) {
|
||||
this.setState({
|
||||
planet: null
|
||||
}, function () {
|
||||
this.fetchPlanet(nextProps.params.userName, nextProps.params.planetName)
|
||||
})
|
||||
}
|
||||
},
|
||||
onKeyCast: function (e) {
|
||||
switch (e.status) {
|
||||
case 'openLaunchModal':
|
||||
this.refs.navigator.openLaunchModal()
|
||||
break
|
||||
case 'selectNextArticle':
|
||||
this.selectNextArticle()
|
||||
break
|
||||
case 'selectPriorArticle':
|
||||
this.selectPriorArticle()
|
||||
break
|
||||
case 'toggleFocusSearchInput':
|
||||
this.toggleFocusSearchInput()
|
||||
break
|
||||
case 'openEditModal':
|
||||
this.refs.detail.openEditModal()
|
||||
break
|
||||
case 'openDeleteModal':
|
||||
this.refs.detail.openDeleteModal()
|
||||
break
|
||||
}
|
||||
},
|
||||
onPlanetChange: function (res) {
|
||||
if (this.state.planet == null) return
|
||||
|
||||
var planet, code, note, articleIndex, articlesCount
|
||||
switch (res.status) {
|
||||
case 'updated':
|
||||
planet = res.data
|
||||
if (this.state.planet.id === planet.id) {
|
||||
if (this.state.planet.name === planet.name) {
|
||||
this.setState({planet: planet})
|
||||
} else {
|
||||
this.transitionTo('planetHome', {userName: planet.userName, planetName: planet.name})
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'destroyed':
|
||||
planet = res.data
|
||||
if (this.state.planet.id === planet.id) {
|
||||
this.transitionTo('userHome', {userName: this.state.planet.userName})
|
||||
}
|
||||
break
|
||||
case 'codeUpdated':
|
||||
code = res.data
|
||||
if (code.PlanetId === this.state.planet.id) {
|
||||
this.state.planet.Codes = this.updateItemToTargetArray(code, this.state.planet.Codes)
|
||||
|
||||
this.setState({planet: this.state.planet})
|
||||
}
|
||||
break
|
||||
case 'noteUpdated':
|
||||
note = res.data
|
||||
if (note.PlanetId === this.state.planet.id) {
|
||||
this.state.planet.Notes = this.updateItemToTargetArray(note, this.state.planet.Notes)
|
||||
|
||||
this.setState({planet: this.state.planet})
|
||||
}
|
||||
break
|
||||
case 'codeDestroyed':
|
||||
code = res.data
|
||||
if (code.PlanetId === this.state.planet.id) {
|
||||
this.state.planet.Codes = this.deleteItemFromTargetArray(code, this.state.planet.Codes)
|
||||
|
||||
if (this.refs.detail.props.article != null && this.refs.detail.props.article.type === code.type && this.refs.detail.props.article.localId === code.localId) {
|
||||
articleIndex = this.getFilteredIndexOfCurrentArticle()
|
||||
articlesCount = this.refs.list.props.articles.length
|
||||
|
||||
this.setState({planet: this.state.planet}, function () {
|
||||
if (articlesCount > 1) {
|
||||
if (articleIndex > 0) {
|
||||
this.selectArticleByListIndex(articleIndex - 1)
|
||||
} else {
|
||||
this.selectArticleByListIndex(articleIndex)
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({planet: this.state.planet})
|
||||
}
|
||||
break
|
||||
case 'noteDestroyed':
|
||||
note = res.data
|
||||
if (note.PlanetId === this.state.planet.id) {
|
||||
this.state.planet.Notes = this.deleteItemFromTargetArray(note, this.state.planet.Notes)
|
||||
|
||||
if (this.refs.detail.props.article != null && this.refs.detail.props.article.type === note.type && this.refs.detail.props.article.localId === note.localId) {
|
||||
articleIndex = this.getFilteredIndexOfCurrentArticle()
|
||||
articlesCount = this.refs.list.props.articles.length
|
||||
|
||||
this.setState({planet: this.state.planet}, function () {
|
||||
if (articlesCount > 1) {
|
||||
if (articleIndex > 0) {
|
||||
this.selectArticleByListIndex(articleIndex - 1)
|
||||
} else {
|
||||
this.selectArticleByListIndex(articleIndex)
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({planet: this.state.planet})
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
onUserChange: function () {
|
||||
|
||||
},
|
||||
fetchPlanet: function (userName, planetName) {
|
||||
if (userName == null) userName = this.props.params.userName
|
||||
if (planetName == null) planetName = this.props.params.planetName
|
||||
|
||||
Hq.fetchPlanet(userName, planetName)
|
||||
.then(function (res) {
|
||||
var planet = res.body
|
||||
|
||||
planet.Codes.forEach(function (code) {
|
||||
code.type = 'code'
|
||||
})
|
||||
|
||||
planet.Notes.forEach(function (note) {
|
||||
note.type = 'note'
|
||||
})
|
||||
|
||||
localStorage.setItem('planet-' + planet.id, JSON.stringify(planet))
|
||||
|
||||
this.setState({planet: planet})
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
})
|
||||
},
|
||||
getFilteredIndexOfCurrentArticle: function () {
|
||||
var params = this.props.params
|
||||
var index = 0
|
||||
|
||||
if (this.isActive('codes')) {
|
||||
this.refs.list.props.articles.some(function (_article, _index) {
|
||||
if (_article.type === 'code' && _article.localId === parseInt(params.localId, 10)) {
|
||||
index = _index
|
||||
}
|
||||
})
|
||||
} else if (this.isActive('notes')) {
|
||||
this.refs.list.props.articles.some(function (_article, _index) {
|
||||
if (_article.type === 'note' && _article.localId === parseInt(params.localId, 10)) {
|
||||
index = _index
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
return index
|
||||
},
|
||||
selectArticleByListIndex: function (index) {
|
||||
var article = this.refs.list.props.articles[index]
|
||||
var params = this.props.params
|
||||
|
||||
if (article == null) {
|
||||
this.transitionTo('planetHome', params)
|
||||
return
|
||||
}
|
||||
|
||||
var listElement = this.refs.list.refs.articles.getDOMNode()
|
||||
var articleElement = listElement.querySelectorAll('li')[index]
|
||||
|
||||
var overflowBelow = listElement.clientHeight + listElement.scrollTop < articleElement.offsetTop + articleElement.clientHeight
|
||||
if (overflowBelow) {
|
||||
listElement.scrollTop = articleElement.offsetTop + articleElement.clientHeight - listElement.clientHeight
|
||||
}
|
||||
var overflowAbove = listElement.scrollTop > articleElement.offsetTop
|
||||
if (overflowAbove) {
|
||||
listElement.scrollTop = articleElement.offsetTop
|
||||
}
|
||||
|
||||
if (article.type === 'code') {
|
||||
params.localId = article.localId
|
||||
this.transitionTo('codes', params)
|
||||
return
|
||||
}
|
||||
|
||||
if (article.type === 'note') {
|
||||
params.localId = article.localId
|
||||
this.transitionTo('notes', params)
|
||||
return
|
||||
}
|
||||
},
|
||||
selectNextArticle: function () {
|
||||
if (this.state.planet == null) return
|
||||
|
||||
var index = this.getFilteredIndexOfCurrentArticle()
|
||||
|
||||
if (index < this.refs.list.props.articles.length - 1) {
|
||||
this.selectArticleByListIndex(index + 1)
|
||||
}
|
||||
},
|
||||
selectPriorArticle: function () {
|
||||
if (this.state.planet == null) {
|
||||
return
|
||||
}
|
||||
var index = this.getFilteredIndexOfCurrentArticle()
|
||||
|
||||
if (index > 0) {
|
||||
this.selectArticleByListIndex(index - 1)
|
||||
} else {
|
||||
React.findDOMNode(this.refs.header.refs.search).focus()
|
||||
}
|
||||
},
|
||||
toggleFocusSearchInput: function () {
|
||||
var search = React.findDOMNode(this.refs.header.refs.search)
|
||||
if (document.activeElement === search) {
|
||||
React.findDOMNode(this.refs.header.refs.search).blur()
|
||||
return
|
||||
}
|
||||
React.findDOMNode(this.refs.header.refs.search).focus()
|
||||
},
|
||||
handleSearchChange: function (e) {
|
||||
this.setState({search: e.target.value}, function () {
|
||||
this.selectArticleByListIndex(0)
|
||||
})
|
||||
},
|
||||
showAll: function () {
|
||||
this.setState({search: ''})
|
||||
},
|
||||
toggleCodeFilter: function () {
|
||||
var keywords = typeof this.state.search === 'string' ? this.state.search.split(' ') : []
|
||||
|
||||
var usingCodeFilter = false
|
||||
var usingNoteFilter = false
|
||||
keywords = keywords.filter(function (keyword) {
|
||||
if (keyword === '$n') {
|
||||
usingNoteFilter = true
|
||||
return false
|
||||
}
|
||||
if (keyword === '$c') usingCodeFilter = true
|
||||
return true
|
||||
})
|
||||
|
||||
if (usingCodeFilter && !usingNoteFilter) {
|
||||
keywords = keywords.filter(function (keyword) {
|
||||
return keyword !== '$c'
|
||||
})
|
||||
}
|
||||
|
||||
if (!usingCodeFilter) {
|
||||
keywords.unshift('$c')
|
||||
}
|
||||
|
||||
this.setState({search: keywords.join(' ')}, function () {
|
||||
this.selectArticleByListIndex(0)
|
||||
})
|
||||
},
|
||||
toggleNoteFilter: function () {
|
||||
var keywords = typeof this.state.search === 'string' ? this.state.search.split(' ') : []
|
||||
|
||||
var usingCodeFilter = false
|
||||
var usingNoteFilter = false
|
||||
keywords = keywords.filter(function (keyword) {
|
||||
if (keyword === '$c') {
|
||||
usingCodeFilter = true
|
||||
return false
|
||||
}
|
||||
if (keyword === '$n') usingNoteFilter = true
|
||||
return true
|
||||
})
|
||||
|
||||
if (usingNoteFilter && !usingCodeFilter) {
|
||||
keywords = keywords.filter(function (keyword) {
|
||||
return keyword !== '$n'
|
||||
})
|
||||
}
|
||||
|
||||
if (!usingNoteFilter) {
|
||||
keywords.unshift('$n')
|
||||
}
|
||||
|
||||
this.setState({search: keywords.join(' ')}, function () {
|
||||
this.selectArticleByListIndex(0)
|
||||
})
|
||||
},
|
||||
applyTagFilter: function (tag) {
|
||||
return function () {
|
||||
this.setState({search: '#' + tag})
|
||||
}.bind(this)
|
||||
},
|
||||
render: function () {
|
||||
if (this.state.planet == null) return (<div/>)
|
||||
|
||||
var localId = parseInt(this.props.params.localId, 10)
|
||||
|
||||
var codes = this.state.planet.Codes
|
||||
var notes = this.state.planet.Notes
|
||||
|
||||
var article
|
||||
if (this.isActive('codes')) {
|
||||
codes.some(function (_article) {
|
||||
if (localId === _article.localId) {
|
||||
article = _article
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
} else if (this.isActive('notes')) {
|
||||
notes.some(function (_article) {
|
||||
if (localId === _article.localId) {
|
||||
article = _article
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
var articles = codes.concat(notes)
|
||||
|
||||
var filteredArticles = this.searchArticle(this.state.search, articles)
|
||||
|
||||
return (
|
||||
<div className='PlanetContainer'>
|
||||
<PlanetHeader
|
||||
ref='header'
|
||||
search={this.state.search}
|
||||
fetchPlanet={this.fetchPlanet}
|
||||
onSearchChange={this.handleSearchChange}
|
||||
currentPlanet={this.state.planet}
|
||||
/>
|
||||
|
||||
<PlanetNavigator
|
||||
ref='navigator'
|
||||
search={this.state.search}
|
||||
showAll={this.showAll}
|
||||
toggleCodeFilter={this.toggleCodeFilter}
|
||||
toggleNoteFilter={this.toggleNoteFilter}
|
||||
planet={this.state.planet}
|
||||
currentUser={this.state.currentUser}/>
|
||||
|
||||
<PlanetArticleList showOnlyWithTag={this.applyTagFilter} ref='list' articles={filteredArticles}/>
|
||||
|
||||
<PlanetArticleDetail
|
||||
ref='detail'
|
||||
article={article}
|
||||
planet={this.state.planet}
|
||||
showOnlyWithTag={this.applyTagFilter}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,137 +0,0 @@
|
||||
/* global localStorage */
|
||||
|
||||
var React = require('react/addons')
|
||||
var ReactRouter = require('react-router')
|
||||
var Link = ReactRouter.Link
|
||||
|
||||
var AuthFilter = require('../Mixins/AuthFilter')
|
||||
var LinkedState = require('../Mixins/LinkedState')
|
||||
var ExternalLink = require('../Mixins/ExternalLink')
|
||||
var Hq = require('../Services/Hq')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [LinkedState, ReactRouter.Navigation, AuthFilter.OnlyGuest, ExternalLink],
|
||||
getInitialState: function () {
|
||||
return {
|
||||
user: {},
|
||||
connectionFailed: false,
|
||||
emailConflicted: false,
|
||||
nameConflicted: false,
|
||||
validationFailed: false,
|
||||
isSending: false
|
||||
}
|
||||
},
|
||||
handleSubmit: function (e) {
|
||||
this.setState({
|
||||
connectionFailed: false,
|
||||
emailConflicted: false,
|
||||
nameConflicted: false,
|
||||
validationFailed: false,
|
||||
isSending: true
|
||||
}, function () {
|
||||
Hq.signup(this.state.user)
|
||||
.then(function (res) {
|
||||
localStorage.setItem('token', res.body.token)
|
||||
localStorage.setItem('currentUser', JSON.stringify(res.body.user))
|
||||
|
||||
this.transitionTo('userHome', {userName: res.body.user.name})
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
var res = err.response
|
||||
if (err.status === 409) {
|
||||
// Confliction
|
||||
var emailConflicted = res.body.errors[0].path === 'email'
|
||||
var nameConflicted = res.body.errors[0].path === 'name'
|
||||
|
||||
this.setState({
|
||||
connectionFailed: false,
|
||||
emailConflicted: emailConflicted,
|
||||
nameConflicted: nameConflicted,
|
||||
validationFailed: false,
|
||||
isSending: false
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (err.status === 422) {
|
||||
// Validation Failed
|
||||
this.setState({
|
||||
connectionFailed: false,
|
||||
emailConflicted: false,
|
||||
nameConflicted: false,
|
||||
validationFailed: {
|
||||
errors: res.body.errors.map(function (error) {
|
||||
return error.path
|
||||
})
|
||||
},
|
||||
isSending: false
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Connection Failed or Whatever
|
||||
this.setState({
|
||||
connectionFailed: true,
|
||||
emailConflicted: false,
|
||||
nameConflicted: false,
|
||||
validationFailed: false,
|
||||
isSending: false
|
||||
})
|
||||
return
|
||||
}.bind(this))
|
||||
})
|
||||
|
||||
e.preventDefault()
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<div className='SignupContainer'>
|
||||
<img className='logo' src='resources/favicon-230x230.png'/>
|
||||
|
||||
<nav className='authNavigator text-center'><Link to='login'>Log In</Link> / <Link to='signup'>Sign Up</Link></nav>
|
||||
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className='form-group'>
|
||||
<input className='stripInput' valueLink={this.linkState('user.email')} type='text' placeholder='E-mail'/>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<input className='stripInput' valueLink={this.linkState('user.password')} type='password' placeholder='Password'/>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<input className='stripInput' valueLink={this.linkState('user.name')} type='text' placeholder='name'/>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<input className='stripInput' valueLink={this.linkState('user.profileName')} type='text' placeholder='Profile name'/>
|
||||
</div>
|
||||
|
||||
{this.state.isSending ? (
|
||||
<p className='alertInfo'>Signing up...</p>
|
||||
) : null}
|
||||
|
||||
{this.state.connectionFailed ? (
|
||||
<p className='alertError'>Please try again.</p>
|
||||
) : null}
|
||||
|
||||
{this.state.emailConflicted ? (
|
||||
<p className='alertError'>E-mail already exists.</p>
|
||||
) : null}
|
||||
|
||||
{this.state.nameConflicted ? (
|
||||
<p className='alertError'>Username already exists.</p>
|
||||
) : null}
|
||||
|
||||
{this.state.validationFailed ? (
|
||||
<p className='alertError'>Please fill every field correctly: {this.state.validationFailed.errors.join(', ')}</p>
|
||||
) : null}
|
||||
|
||||
<div className='form-group'>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,367 +0,0 @@
|
||||
/* global localStorage */
|
||||
|
||||
var React = require('react/addons')
|
||||
var ReactRouter = require('react-router')
|
||||
var Navigation = ReactRouter.Navigation
|
||||
var State = ReactRouter.State
|
||||
var RouteHandler = ReactRouter.RouteHandler
|
||||
var Link = ReactRouter.Link
|
||||
var Reflux = require('reflux')
|
||||
|
||||
var LinkedState = require('../Mixins/LinkedState')
|
||||
var Modal = require('../Mixins/Modal')
|
||||
var Helper = require('../Mixins/Helper')
|
||||
|
||||
var Hq = require('../Services/Hq')
|
||||
|
||||
var ProfileImage = require('../Components/ProfileImage')
|
||||
var EditProfileModal = require('../Components/EditProfileModal')
|
||||
var TeamSettingsModal = require('../Components/TeamSettingsModal')
|
||||
var PlanetCreateModal = require('../Components/PlanetCreateModal')
|
||||
var AddMemberModal = require('../Components/AddMemberModal')
|
||||
var TeamCreateModal = require('../Components/TeamCreateModal')
|
||||
|
||||
var UserStore = require('../Stores/UserStore')
|
||||
var PlanetStore = require('../Stores/PlanetStore')
|
||||
|
||||
module.exports = React.createClass({
|
||||
mixins: [LinkedState, State, Navigation, Modal, Reflux.listenTo(UserStore, 'onUserChange'), Reflux.listenTo(PlanetStore, 'onPlanetChange'), Helper],
|
||||
propTypes: {
|
||||
params: React.PropTypes.shape({
|
||||
userName: React.PropTypes.string,
|
||||
planetName: React.PropTypes.string
|
||||
})
|
||||
},
|
||||
getInitialState: function () {
|
||||
return {
|
||||
user: null
|
||||
}
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this.fetchUser()
|
||||
},
|
||||
componentWillReceiveProps: function (nextProps) {
|
||||
if (this.state.user == null) {
|
||||
this.fetchUser(nextProps.params.userName)
|
||||
return
|
||||
}
|
||||
|
||||
if (nextProps.params.userName !== this.state.user.name) {
|
||||
this.setState({
|
||||
user: null
|
||||
}, function () {
|
||||
this.fetchUser(nextProps.params.userName)
|
||||
})
|
||||
}
|
||||
},
|
||||
onUserChange: function (res) {
|
||||
if (this.state.user == null) return
|
||||
|
||||
var member
|
||||
switch (res.status) {
|
||||
case 'userUpdated':
|
||||
if (this.state.user.id === res.data.id) {
|
||||
this.setState({user: res.data})
|
||||
}
|
||||
break
|
||||
case 'memberAdded':
|
||||
member = res.data
|
||||
if (this.state.user.userType === 'team' && member.TeamMember.TeamId === this.state.user.id) {
|
||||
this.state.user.Members = this.updateItemToTargetArray(member, this.state.user.Members)
|
||||
|
||||
this.setState({user: this.state.user})
|
||||
}
|
||||
break
|
||||
case 'memberRemoved':
|
||||
member = res.data
|
||||
if (this.state.user.userType === 'team' && member.TeamMember.TeamId === this.state.user.id) {
|
||||
this.state.user.Members = this.deleteItemFromTargetArray(member, this.state.user.Members)
|
||||
|
||||
this.setState({user: this.state.user})
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
onPlanetChange: function (res) {
|
||||
if (this.state.user == null) return
|
||||
|
||||
var currentUser, planet, isOwner, team
|
||||
switch (res.status) {
|
||||
case 'updated':
|
||||
// if state.user is currentUser, planet will be fetched by UserStore
|
||||
currentUser = JSON.parse(localStorage.getItem('currentUser'))
|
||||
if (currentUser.id === this.state.user.id) return
|
||||
|
||||
planet = res.data
|
||||
isOwner = planet.Owner.id === this.state.user.id
|
||||
if (isOwner) {
|
||||
this.state.user.Planets = this.updateItemToTargetArray(planet, this.state.user.Planets)
|
||||
this.setState({user: this.state.user})
|
||||
return
|
||||
}
|
||||
// check if team of user has this planet
|
||||
team = null
|
||||
this.state.user.userType !== 'team' && this.state.user.Teams.some(function (_team) {
|
||||
if (planet.Owner.id === _team.id) {
|
||||
team = _team
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
if (team != null) {
|
||||
team.Planets = this.updateItemToTargetArray(planet, team.Planets)
|
||||
this.setState({user: this.state.user})
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
case 'destroyed':
|
||||
// if state.user is currentUser, planet will be fetched by UserStore
|
||||
currentUser = JSON.parse(localStorage.getItem('currentUser'))
|
||||
if (currentUser.id === this.state.user.id) return
|
||||
|
||||
planet = res.data
|
||||
isOwner = planet.Owner.id === this.state.user.id
|
||||
if (isOwner) {
|
||||
this.state.user.Planets = this.deleteItemFromTargetArray(planet, this.state.user.Planets)
|
||||
this.setState({user: this.state.user})
|
||||
return
|
||||
}
|
||||
// check if team of user has this planet
|
||||
team = null
|
||||
this.state.user.userType !== 'team' && this.state.user.Teams.some(function (_team) {
|
||||
if (planet.Owner.id === _team.id) {
|
||||
team = _team
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
if (team != null) {
|
||||
team.Planets = this.deleteItemFromTargetArray(planet, team.Planets)
|
||||
this.setState({user: this.state.user})
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
fetchUser: function (userName) {
|
||||
if (userName == null) userName = this.props.params.userName
|
||||
|
||||
Hq.fetchUser(userName)
|
||||
.then(function (res) {
|
||||
this.setState({user: res.body})
|
||||
}.bind(this))
|
||||
.catch(function (err) {
|
||||
console.error(err)
|
||||
})
|
||||
},
|
||||
openEditProfileModal: function () {
|
||||
this.openModal(EditProfileModal, {user: this.state.user})
|
||||
},
|
||||
openTeamSettingsModal: function () {
|
||||
this.openModal(TeamSettingsModal, {team: this.state.user})
|
||||
},
|
||||
openAddUserModal: function () {
|
||||
this.openModal(AddMemberModal, {team: this.state.user})
|
||||
},
|
||||
openTeamCreateModal: function () {
|
||||
this.openModal(TeamCreateModal, {user: this.state.user})
|
||||
},
|
||||
openPlanetCreateModalWithOwnerName: function (name) {
|
||||
return function () {
|
||||
this.openModal(PlanetCreateModal, {ownerName: name})
|
||||
}.bind(this)
|
||||
},
|
||||
render: function () {
|
||||
var user = this.state.user
|
||||
|
||||
var currentUser = JSON.parse(localStorage.getItem('currentUser'))
|
||||
|
||||
if (this.isActive('userHome')) {
|
||||
if (user == null) {
|
||||
return (
|
||||
<div className='UserContainer'>
|
||||
User Loading...
|
||||
</div>
|
||||
)
|
||||
} else if (user.userType === 'team') {
|
||||
return this.renderTeamHome(currentUser)
|
||||
} else {
|
||||
return this.renderUserHome(currentUser)
|
||||
}
|
||||
} else if (this.isActive('planet') && user != null && user.userType === 'team') {
|
||||
var members = user.Members.map(function (member) {
|
||||
return (
|
||||
<li key={'user-' + member.id}><Link to='userHome' params={{userName: member.name}}>
|
||||
<ProfileImage className='memberImage' size='22' email={member.email}/>
|
||||
<div className='memberInfo'>
|
||||
<div className='memberProfileName'>{member.profileName}</div>
|
||||
<div className='memberName'>@{member.name}</div>
|
||||
</div>
|
||||
</Link></li>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<div className='UserContainer'>
|
||||
<RouteHandler/>
|
||||
<div className='memberPopup'>
|
||||
<div className='label'>Members</div>
|
||||
<ul className='members'>
|
||||
{members}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div className='UserContainer'>
|
||||
<RouteHandler/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
renderTeamHome: function (currentUser) {
|
||||
var user = this.state.user
|
||||
|
||||
var isOwner = user.Members == null ? false : user.Members.some(function (member) {
|
||||
return member.id === currentUser.id && member.TeamMember.role === 'owner'
|
||||
})
|
||||
|
||||
var userPlanets = user.Planets.map(function (planet) {
|
||||
return (
|
||||
<li key={'planet-' + planet.id}>
|
||||
<Link to='planet' params={{userName: planet.userName, planetName: planet.name}}>{planet.userName}/{planet.name}</Link>
|
||||
{!planet.public ? (<i className='fa fa-lock'/>) : null}
|
||||
</li>
|
||||
)
|
||||
})
|
||||
|
||||
var members = user.Members == null ? [] : user.Members.map(function (member) {
|
||||
return (
|
||||
<li key={'user-' + member.id}>
|
||||
<Link to='userHome' params={{userName: member.name}}>
|
||||
<ProfileImage size='22' className='memberImage' email={member.email}/>
|
||||
<div className='memberInfo'>
|
||||
<div className='memberProfileName'>{member.profileName} <span className='memberRole'>({member.TeamMember.role})</span></div>
|
||||
<div className='memberName'>@{member.name}</div>
|
||||
</div>
|
||||
</Link>
|
||||
<div className='role'></div>
|
||||
</li>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<div className='UserContainer'>
|
||||
<div className='userProfile'>
|
||||
<ProfileImage className='userPhoto' size='75' email={user.email}/>
|
||||
<div className='userInfo'>
|
||||
<div className='userProfileName'>{user.profileName}</div>
|
||||
<div className='userName'>{user.name}</div>
|
||||
</div>
|
||||
|
||||
{isOwner ? (<button onClick={this.openTeamSettingsModal} className='editProfileButton'>Team settings</button>) : null}
|
||||
</div>
|
||||
<div className='memberList'>
|
||||
<div className='memberLabel'>{members.length} {members.length > 1 ? 'Members' : 'Member'}</div>
|
||||
<ul className='members'>
|
||||
{members}
|
||||
{isOwner ? (<li><button onClick={this.openAddUserModal} className='addMemberButton'><i className='fa fa-plus-square-o'/> add Member</button></li>) : null}
|
||||
</ul>
|
||||
</div>
|
||||
<div className='planetList'>
|
||||
<div className='planetLabel'>{userPlanets.length} {userPlanets.length > 0 ? 'Planets' : 'Planet'}</div>
|
||||
<div className='planetGroup'>
|
||||
<ul className='planets'>
|
||||
{userPlanets}
|
||||
{isOwner ? (<li><button onClick={this.openPlanetCreateModalWithOwnerName(user.name)} className='createPlanetButton'><i className='fa fa-plus-square-o'/> Create new planet</button></li>) : null}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
renderUserHome: function (currentUser) {
|
||||
var user = this.state.user
|
||||
|
||||
var isOwner = currentUser.id === user.id
|
||||
|
||||
var userPlanets = user.Planets.map(function (planet) {
|
||||
return (
|
||||
<li key={'planet-' + planet.id}>
|
||||
<Link to='planet' params={{userName: planet.userName, planetName: planet.name}}>{planet.userName}/{planet.name}</Link>
|
||||
{!planet.public ? (<i className='fa fa-lock'/>) : null}
|
||||
</li>
|
||||
)
|
||||
})
|
||||
|
||||
var teams = user.Teams == null ? [] : user.Teams.map(function (team) {
|
||||
return (
|
||||
<li key={'user-' + team.id}>
|
||||
<Link to='userHome' params={{userName: team.name}}>
|
||||
<div className='teamInfo'>
|
||||
<div className='teamProfileName'>{team.profileName}</div>
|
||||
<div className='teamName'>@{team.name}</div>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})
|
||||
|
||||
var teamPlanets = user.Teams == null ? [] : user.Teams.map(function (team) {
|
||||
var planets = (team.Planets == null ? [] : team.Planets).map(function (planet) {
|
||||
return (
|
||||
<li key={'planet-' + planet.id}>
|
||||
<Link to='planet' params={{userName: planet.userName, planetName: planet.name}}>{planet.userName}/{planet.name}</Link>
|
||||
{!planet.public ? (<i className='fa fa-lock'/>) : null}
|
||||
</li>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<div key={'user-' + team.id} className='planetGroup'>
|
||||
<div className='planetGroupLabel'>{team.profileName} <small>@{team.name}</small></div>
|
||||
<ul className='planets'>
|
||||
{planets}
|
||||
{isOwner ? (<li><button onClick={this.openPlanetCreateModalWithOwnerName(team.name)} className='createPlanetButton'><i className='fa fa-plus-square-o'/> Create new planet</button></li>) : null}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}.bind(this))
|
||||
|
||||
var planetCount = userPlanets.length + user.Teams.reduce(function (sum, team) {
|
||||
return sum + (team.Planets != null ? team.Planets.length : 0)
|
||||
}, 0)
|
||||
|
||||
return (
|
||||
<div className='UserContainer'>
|
||||
<div className='userProfile'>
|
||||
<ProfileImage className='userPhoto' size='75' email={user.email}/>
|
||||
<div className='userInfo'>
|
||||
<div className='userProfileName'>{user.profileName}</div>
|
||||
<div className='userName'>{user.name}</div>
|
||||
</div>
|
||||
|
||||
{isOwner ? (
|
||||
<button onClick={this.openEditProfileModal} className='editProfileButton'>Edit profile</button>) : null}
|
||||
</div>
|
||||
<div className='teamList'>
|
||||
<div className='teamLabel'>{teams.length} {teams.length > 1 ? 'Teams' : 'Team'}</div>
|
||||
<ul className='teams'>
|
||||
{teams}
|
||||
{isOwner ? (<li><button onClick={this.openTeamCreateModal} className='createTeamButton'><i className='fa fa-plus-square-o'/> Create new team</button></li>) : null}
|
||||
</ul>
|
||||
</div>
|
||||
<div className='planetList'>
|
||||
<div className='planetLabel'>{planetCount} {planetCount > 1 ? 'Planets' : 'Planet'}</div>
|
||||
<div className='planetGroup'>
|
||||
<div className='planetGroupLabel'>{user.profileName} <small>@{user.name}</small></div>
|
||||
<ul className='planets'>
|
||||
{userPlanets}
|
||||
{isOwner ? (<li><button onClick={this.openPlanetCreateModalWithOwnerName(user.name)} className='createPlanetButton'><i className='fa fa-plus-square-o'/> Create new planet</button></li>) : null}
|
||||
</ul>
|
||||
</div>
|
||||
{teamPlanets}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
243
browser/main/HomePage/ArticleDetail/ArticleEditor.js
Normal file
243
browser/main/HomePage/ArticleDetail/ArticleEditor.js
Normal file
@@ -0,0 +1,243 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import activityRecord from 'browser/lib/activityRecord'
|
||||
import fetchConfig from 'browser/lib/fetchConfig'
|
||||
|
||||
const electron = require('electron')
|
||||
const ipc = electron.ipcRenderer
|
||||
|
||||
export const PREVIEW_MODE = 'PREVIEW_MODE'
|
||||
export const EDIT_MODE = 'EDIT_MODE'
|
||||
|
||||
let config = fetchConfig()
|
||||
ipc.on('config-apply', function (e, newConfig) {
|
||||
config = newConfig
|
||||
})
|
||||
|
||||
export default class ArticleEditor extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
|
||||
this.isMouseDown = false
|
||||
this.state = {
|
||||
status: PREVIEW_MODE,
|
||||
cursorPosition: null,
|
||||
firstVisibleRow: null,
|
||||
switchPreview: config['switch-preview'],
|
||||
isTemporary: false
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
ipc.on('config-apply', this.configApplyHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
ipc.removeListener('config-apply', this.configApplyHandler)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.article.key !== this.props.article.key) {
|
||||
this.setState({
|
||||
content: this.props.article.content
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleConfigApply (e, newConfig) {
|
||||
this.setState({
|
||||
switchPreview: newConfig['switch-preview']
|
||||
})
|
||||
}
|
||||
|
||||
resetCursorPosition () {
|
||||
this.setState({
|
||||
cursorPosition: null,
|
||||
firstVisibleRow: null
|
||||
}, function () {
|
||||
let previewEl = ReactDOM.findDOMNode(this.refs.preview)
|
||||
if (previewEl) previewEl.scrollTop = 0
|
||||
})
|
||||
}
|
||||
|
||||
switchPreviewMode (isTemporary = false) {
|
||||
if (this.props.article.mode !== 'markdown') return true
|
||||
let cursorPosition = this.refs.editor.getCursorPosition()
|
||||
let firstVisibleRow = this.refs.editor.getFirstVisibleRow()
|
||||
this.setState({
|
||||
status: PREVIEW_MODE,
|
||||
cursorPosition,
|
||||
firstVisibleRow,
|
||||
isTemporary: isTemporary
|
||||
}, function () {
|
||||
let previewEl = ReactDOM.findDOMNode(this.refs.preview)
|
||||
let anchors = previewEl.querySelectorAll('.lineAnchor')
|
||||
for (let i = 0; i < anchors.length; i++) {
|
||||
if (parseInt(anchors[i].dataset.key, 10) > cursorPosition.row || i === anchors.length - 1) {
|
||||
var targetAnchor = anchors[i > 0 ? i - 1 : 0]
|
||||
previewEl.scrollTop = targetAnchor.offsetTop - 100
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
switchEditMode (isTemporary = false) {
|
||||
this.setState({
|
||||
status: EDIT_MODE,
|
||||
isTemporary: false
|
||||
}, function () {
|
||||
if (this.state.cursorPosition != null) {
|
||||
this.refs.editor.moveCursorTo(this.state.cursorPosition.row, this.state.cursorPosition.column)
|
||||
this.refs.editor.scrollToLine(this.state.firstVisibleRow)
|
||||
}
|
||||
this.refs.editor.editor.focus()
|
||||
|
||||
if (!isTemporary) activityRecord.emit('ARTICLE_UPDATE', this.props.article)
|
||||
})
|
||||
}
|
||||
|
||||
handleBlurCodeEditor (e) {
|
||||
let isFocusingToThis = e.relatedTarget === ReactDOM.findDOMNode(this)
|
||||
if (isFocusingToThis || this.state.switchPreview !== 'blur') {
|
||||
return
|
||||
}
|
||||
|
||||
let { article } = this.props
|
||||
if (article.mode === 'markdown') {
|
||||
this.switchPreviewMode()
|
||||
}
|
||||
}
|
||||
|
||||
handleCodeEditorChange (value) {
|
||||
this.props.onChange(value)
|
||||
}
|
||||
|
||||
handleRightClick (e) {
|
||||
let { article } = this.props
|
||||
if (this.state.switchPreview === 'rightclick' && article.mode === 'markdown') {
|
||||
if (this.state.status === EDIT_MODE) this.switchPreviewMode()
|
||||
else this.switchEditMode()
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseUp (e) {
|
||||
let { article } = this.props
|
||||
let showPreview = article.mode === 'markdown' && this.state.status === PREVIEW_MODE
|
||||
if (!showPreview) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch (this.state.switchPreview) {
|
||||
case 'blur':
|
||||
switch (e.button) {
|
||||
case 0:
|
||||
this.isMouseDown = false
|
||||
this.moveCount = 0
|
||||
if (!this.isDrag) {
|
||||
this.switchEditMode()
|
||||
}
|
||||
break
|
||||
case 2:
|
||||
if (this.state.isTemporary) this.switchEditMode(true)
|
||||
}
|
||||
break
|
||||
case 'rightclick':
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseMove (e) {
|
||||
let { article } = this.props
|
||||
let showPreview = article.mode === 'markdown' && this.state.status === PREVIEW_MODE
|
||||
if (!showPreview) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.state.switchPreview === 'blur' && this.isMouseDown) {
|
||||
this.moveCount++
|
||||
if (this.moveCount > 5) {
|
||||
this.isDrag = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseDowm (e) {
|
||||
let { article } = this.props
|
||||
let showPreview = article.mode === 'markdown' && this.state.status === PREVIEW_MODE
|
||||
if (!showPreview) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch (this.state.switchPreview) {
|
||||
case 'blur':
|
||||
switch (e.button) {
|
||||
case 0:
|
||||
this.isDrag = false
|
||||
this.isMouseDown = true
|
||||
this.moveCount = 0
|
||||
break
|
||||
case 2:
|
||||
if (this.state.status === EDIT_MODE && this.props.article.mode === 'markdown') {
|
||||
this.switchPreviewMode(true)
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'rightclick':
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { article } = this.props
|
||||
let showPreview = article.mode === 'markdown' && this.state.status === PREVIEW_MODE
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex='5'
|
||||
onContextMenu={e => this.handleRightClick(e)}
|
||||
onMouseUp={e => this.handleMouseUp(e)}
|
||||
onMouseMove={e => this.handleMouseMove(e)}
|
||||
onMouseDown={e => this.handleMouseDowm(e)}
|
||||
className='ArticleEditor'
|
||||
>
|
||||
{showPreview
|
||||
? <MarkdownPreview
|
||||
ref='preview'
|
||||
content={article.content}
|
||||
/>
|
||||
: <CodeEditor
|
||||
ref='editor'
|
||||
onBlur={e => this.handleBlurCodeEditor(e)}
|
||||
onChange={value => this.handleCodeEditorChange(value)}
|
||||
article={article}
|
||||
/>
|
||||
}
|
||||
{article.mode === 'markdown'
|
||||
? <div className='ArticleDetail-panel-content-tooltip' children={
|
||||
showPreview
|
||||
? this.state.switchPreview === 'blur'
|
||||
? 'Click to Edit'
|
||||
: 'Right Click to Edit'
|
||||
: this.state.switchPreview === 'blur'
|
||||
? 'Press ESC to Watch Preview'
|
||||
: 'Right Click to Watch Preview'
|
||||
}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ArticleEditor.propTypes = {
|
||||
article: PropTypes.shape({
|
||||
content: PropTypes.string,
|
||||
key: PropTypes.string,
|
||||
mode: PropTypes.string
|
||||
}),
|
||||
onChange: PropTypes.func,
|
||||
parent: PropTypes.object
|
||||
}
|
||||
168
browser/main/HomePage/ArticleDetail/ShareButton.js
Normal file
168
browser/main/HomePage/ArticleDetail/ShareButton.js
Normal file
@@ -0,0 +1,168 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import api from 'browser/lib/api'
|
||||
import clientKey from 'browser/lib/clientKey'
|
||||
import activityRecord from 'browser/lib/activityRecord'
|
||||
const clipboard = require('electron').clipboard
|
||||
|
||||
function notify (...args) {
|
||||
return new window.Notification(...args)
|
||||
}
|
||||
|
||||
function getDefault () {
|
||||
return {
|
||||
openDropdown: false,
|
||||
isSharing: false,
|
||||
// Fetched url
|
||||
url: null,
|
||||
// for tooltip Copy -> Copied!
|
||||
copied: false,
|
||||
failed: false
|
||||
}
|
||||
}
|
||||
|
||||
export default class ShareButton extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = getDefault()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
this.setState(getDefault())
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.dropdownInterceptor = e => {
|
||||
this.dropdownClicked = true
|
||||
}
|
||||
ReactDOM.findDOMNode(this.refs.dropdown).addEventListener('click', this.dropdownInterceptor)
|
||||
this.shareViaPublicURLHandler = e => {
|
||||
this.handleShareViaPublicURLClick(e)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('click', this.dropdownHandler)
|
||||
ReactDOM.findDOMNode(this.refs.dropdown).removeEventListener('click', this.dropdownInterceptor)
|
||||
}
|
||||
|
||||
handleOpenButtonClick (e) {
|
||||
this.openDropdown()
|
||||
if (this.dropdownHandler == null) {
|
||||
this.dropdownHandler = e => {
|
||||
if (!this.dropdownClicked) {
|
||||
this.closeDropdown()
|
||||
} else {
|
||||
this.dropdownClicked = false
|
||||
}
|
||||
}
|
||||
}
|
||||
document.removeEventListener('click', this.dropdownHandler)
|
||||
document.addEventListener('click', this.dropdownHandler)
|
||||
}
|
||||
|
||||
openDropdown () {
|
||||
this.setState({openDropdown: true})
|
||||
}
|
||||
|
||||
closeDropdown () {
|
||||
document.removeEventListener('click', this.dropdownHandler)
|
||||
this.setState({openDropdown: false})
|
||||
}
|
||||
|
||||
handleClipboardButtonClick (e) {
|
||||
activityRecord.emit('MAIN_DETAIL_COPY')
|
||||
clipboard.writeText(this.props.article.content)
|
||||
notify('Saved to Clipboard!', {
|
||||
body: 'Paste it wherever you want!'
|
||||
})
|
||||
this.setState({openDropdown: false})
|
||||
}
|
||||
|
||||
handleShareViaPublicURLClick (e) {
|
||||
let { user } = this.props
|
||||
let input = Object.assign({}, this.props.article, {
|
||||
clientKey: clientKey.get(),
|
||||
writerName: user.name
|
||||
})
|
||||
this.setState({
|
||||
isSharing: true,
|
||||
failed: false
|
||||
}, () => {
|
||||
api.shareViaPublicURL(input)
|
||||
.then(res => {
|
||||
let url = res.body.url
|
||||
this.setState({url: url, isSharing: false})
|
||||
activityRecord.emit('ARTICLE_SHARE')
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
this.setState({isSharing: false, failed: true})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
handleCopyURLClick () {
|
||||
clipboard.writeText(this.state.url)
|
||||
this.setState({copied: true})
|
||||
}
|
||||
|
||||
// Restore copy url tooltip
|
||||
handleCopyURLMouseLeave () {
|
||||
this.setState({copied: false})
|
||||
}
|
||||
|
||||
render () {
|
||||
let hasPublicURL = this.state.url != null
|
||||
return (
|
||||
<div className='ShareButton'>
|
||||
<button ref='openButton' onClick={e => this.handleOpenButtonClick(e)} className='ShareButton-open-button'>
|
||||
<i className='fa fa-fw fa-share-alt'/>
|
||||
{
|
||||
this.state.openDropdown ? null : (
|
||||
<span className='tooltip'>Share</span>
|
||||
)
|
||||
}
|
||||
</button>
|
||||
<div ref='dropdown' className={'ShareButton-dropdown' + (this.state.openDropdown ? '' : ' hide')}>
|
||||
{
|
||||
!hasPublicURL ? (
|
||||
<button
|
||||
onClick={e => this.shareViaPublicURLHandler(e)}
|
||||
ref='sharePublicURL'
|
||||
disabled={this.state.isSharing}>
|
||||
<i className='fa fa-fw fa-external-link'/> {this.state.failed ? 'Failed : Click to Try again' : !this.state.isSharing ? 'Share via public URL' : 'Sharing...'}
|
||||
</button>
|
||||
) : (
|
||||
<div className='ShareButton-url'>
|
||||
<input className='ShareButton-url-input' value={this.state.url} readOnly/>
|
||||
<button
|
||||
onClick={e => this.handleCopyURLClick(e)}
|
||||
className='ShareButton-url-button'
|
||||
onMouseLeave={e => this.handleCopyURLMouseLeave(e)}
|
||||
>
|
||||
<i className='fa fa-fw fa-clipboard'/>
|
||||
<div className='ShareButton-url-button-tooltip'>{this.state.copied ? 'Copied!' : 'Copy URL'}</div>
|
||||
</button>
|
||||
<div className='ShareButton-url-alert'>This url is valid for 7 days.</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<button onClick={e => this.handleClipboardButtonClick(e)}>
|
||||
<i className='fa fa-fw fa-clipboard'/> Copy to clipboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ShareButton.propTypes = {
|
||||
article: PropTypes.shape({
|
||||
publicURL: PropTypes.string,
|
||||
content: PropTypes.string
|
||||
}),
|
||||
user: PropTypes.shape({
|
||||
name: PropTypes.string
|
||||
})
|
||||
}
|
||||
361
browser/main/HomePage/ArticleDetail/index.js
Normal file
361
browser/main/HomePage/ArticleDetail/index.js
Normal file
@@ -0,0 +1,361 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import moment from 'moment'
|
||||
import _ from 'lodash'
|
||||
import {
|
||||
switchFolder,
|
||||
updateArticle
|
||||
} from '../../actions'
|
||||
import linkState from 'browser/lib/linkState'
|
||||
import TagSelect from 'browser/components/TagSelect'
|
||||
import ModeSelect from 'browser/components/ModeSelect'
|
||||
import ShareButton from './ShareButton'
|
||||
import { openModal, isModalOpen } from 'browser/lib/modal'
|
||||
import DeleteArticleModal from '../../modal/DeleteArticleModal'
|
||||
import ArticleEditor from './ArticleEditor'
|
||||
const electron = require('electron')
|
||||
const ipc = electron.ipcRenderer
|
||||
import fetchConfig from 'browser/lib/fetchConfig'
|
||||
|
||||
let config = fetchConfig()
|
||||
ipc.on('config-apply', function (e, newConfig) {
|
||||
config = newConfig
|
||||
})
|
||||
|
||||
const BRAND_COLOR = '#18AF90'
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
const tagSelectTutorialElement = (
|
||||
<svg width='500' height='500' className='tutorial'>
|
||||
<text x='155' y='50' fill={BRAND_COLOR} fontSize='24'>Attach some tags here!</text>
|
||||
|
||||
<svg x='0' y='-15'>
|
||||
<path fill='white' d='M15.5,22.2c77.8-0.7,155.6-1.3,233.5-2c22.2-0.2,44.4-0.4,66.6-0.6c1.9,0,1.9-3,0-3
|
||||
c-77.8,0.7-155.6,1.3-233.5,2c-22.2,0.2-44.4,0.4-66.6,0.6C13.6,19.2,13.6,22.2,15.5,22.2L15.5,22.2z'/>
|
||||
<path fill='white' d='M130.8,25c-5.4,6.8-10.3,14-14.6,21.5c-0.8,1.4,1.2,3.2,2.4,1.8c1-1.2,2-2.4,3.1-3.7c1.2-1.5-0.9-3.6-2.1-2.1
|
||||
c-1,1.2-2,2.4-3.1,3.7c0.8,0.6,1.6,1.2,2.4,1.8c4.2-7.3,8.9-14.3,14.2-20.9C134.1,25.6,132,23.4,130.8,25L130.8,25z'/>
|
||||
<path fill='white' d='M132.6,22.1c8.4,5.9,16.8,11.9,25.2,17.8c1.6,1.1,3.1-1.5,1.5-2.6c-8.4-5.9-16.8-11.9-25.2-17.8
|
||||
C132.5,18.4,131,21,132.6,22.1L132.6,22.1z'/>
|
||||
<path fill='white' d='M132.9,18.6c0.4,6.7-0.7,13.3-3.5,19.3c-1.5,3.1-3.9,6.4-3.1,10c0.7,3.1,3.4,4.4,6.2,5.5
|
||||
c5.1,2.1,10.5,3.1,16.1,3.2c1.9,0,1.9-3,0-3c-4.7-0.1-9.2-0.8-13.6-2.4c-3-1.1-6.2-1.9-5.4-6.6c0.4-2,2-4.1,2.8-5.9
|
||||
c2.9-6.3,4-13.1,3.6-20.1C135.8,16.7,132.8,16.7,132.9,18.6L132.9,18.6z'/>
|
||||
</svg>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const modeSelectTutorialElement = (
|
||||
<svg width='500' height='500' className='tutorial'>
|
||||
<text x='195' y='130' fill={BRAND_COLOR} fontSize='24'>Select code syntax!!</text>
|
||||
|
||||
<svg x='300' y='0'>
|
||||
<path fill='white' d='M99.9,58.8c-14.5-0.5-29-2.2-43.1-5.6c-12.3-2.9-27.9-6.4-37.1-15.5C7.9,26,28.2,18.9,37,16.7
|
||||
c13.8-3.5,28.3-4.7,42.4-5.8c29.6-2.2,59.3-1.7,89-1c3,0.1,7.5-0.6,10.2,0.6c3.1,1.4,3.1,5.3,3.3,8.1c0.3,5.2-0.2,10.7-2.4,15.4
|
||||
c-4.4,9.6-18.4,14.7-27.5,18.1c-27.1,10.1-56.7,12.8-85.3,15.6c-1.9,0.2-1.9,3.2,0,3c29.3-2.9,59.8-5.6,87.5-16.2
|
||||
c9.6-3.7,22.8-8.7,27.7-18.4c2.3-4.6,3.2-9.9,3.2-15c0-3.6,0-9.4-2.9-12c-1.9-1.7-4.7-1.8-7.1-2c-4.8-0.2-9.6-0.2-14.4-0.3
|
||||
c-8.7-0.2-17.5-0.3-26.2-0.4C116.7,6.3,99,6.5,81.3,7.8c-15.8,1.1-32.1,2.3-47.4,6.6c-7.7,2.2-22.1,6.9-20.9,17.4
|
||||
c0.6,5.4,5.6,9.4,9.9,12.1c6.7,4.3,14.4,6.9,22,9.2c17.8,5.4,36.4,8,54.9,8.6C101.8,61.8,101.8,58.8,99.9,58.8L99.9,58.8z'/>
|
||||
<path fill='white' d='M11.1,67.8c9.2-6.1,18.6-11.9,28.2-17.2c-0.7-0.3-1.5-0.6-2.2-0.9c0.9,5.3,0.7,10.3-0.5,15.5
|
||||
c-0.4,1.9,2.4,2.7,2.9,0.8c1.4-5.7,1.5-11.3,0.5-17.1c-0.2-1-1.4-1.3-2.2-0.9c-9.7,5.3-19.1,11.1-28.2,17.2
|
||||
C8,66.3,9.5,68.9,11.1,67.8L11.1,67.8z'/>
|
||||
<path fill='white' d='M31.5,52.8C23.4,68.9,0.2,83.2,7.9,104c0.7,1.8,3.6,1,2.9-0.8C3.6,83.7,26.4,69.7,34.1,54.3
|
||||
C35,52.6,32.4,51.1,31.5,52.8L31.5,52.8z'/>
|
||||
</svg>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default class ArticleDetail extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.deleteHandler = e => {
|
||||
if (isModalOpen()) return true
|
||||
this.handleDeleteButtonClick()
|
||||
}
|
||||
this.uncacheHandler = e => {
|
||||
if (isModalOpen()) return true
|
||||
this.handleUncache()
|
||||
}
|
||||
this.titleHandler = e => {
|
||||
if (isModalOpen()) return true
|
||||
if (this.refs.title) {
|
||||
this.focusTitle()
|
||||
}
|
||||
}
|
||||
this.editHandler = e => {
|
||||
if (isModalOpen()) return true
|
||||
if (this.refs.editor) this.refs.editor.switchEditMode()
|
||||
}
|
||||
this.previewHandler = e => {
|
||||
if (isModalOpen()) return true
|
||||
if (this.refs.editor) this.refs.editor.switchPreviewMode()
|
||||
}
|
||||
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
|
||||
|
||||
this.state = {
|
||||
article: Object.assign({content: ''}, props.activeArticle),
|
||||
openShareDropdown: false,
|
||||
fontFamily: config['editor-font-family']
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
|
||||
this.shareDropdownInterceptor = e => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
ipc.on('detail-delete', this.deleteHandler)
|
||||
ipc.on('detail-uncache', this.uncacheHandler)
|
||||
ipc.on('detail-title', this.titleHandler)
|
||||
ipc.on('detail-edit', this.editHandler)
|
||||
ipc.on('detail-preview', this.previewHandler)
|
||||
ipc.on('config-apply', this.configApplyHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearInterval(this.refreshTimer)
|
||||
|
||||
ipc.removeListener('detail-delete', this.deleteHandler)
|
||||
ipc.removeListener('detail-uncache', this.uncacheHandler)
|
||||
ipc.removeListener('detail-title', this.titleHandler)
|
||||
ipc.removeListener('detail-edit', this.editHandler)
|
||||
ipc.removeListener('detail-preview', this.previewHandler)
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
if (this.props.activeArticle == null || prevProps.activeArticle == null || this.props.activeArticle.key !== prevProps.activeArticle.key) {
|
||||
if (this.refs.editor) this.refs.editor.resetCursorPosition()
|
||||
}
|
||||
|
||||
if (prevProps.activeArticle == null && this.props.activeArticle) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
handleConfigApply (e, config) {
|
||||
this.setState({
|
||||
fontFamily: config['editor-font-family']
|
||||
})
|
||||
}
|
||||
|
||||
renderEmpty () {
|
||||
return (
|
||||
<div className='ArticleDetail empty'>
|
||||
<div className='ArticleDetail-empty-box'>
|
||||
<div className='ArticleDetail-empty-box-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br/>to create a new post</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
handleOthersButtonClick (e) {
|
||||
this.deleteHandler()
|
||||
}
|
||||
|
||||
handleFolderKeyChange (e) {
|
||||
let { dispatch, activeArticle, status, folders } = this.props
|
||||
let article = Object.assign({}, activeArticle, {
|
||||
FolderKey: e.target.value,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
|
||||
dispatch(updateArticle(article))
|
||||
|
||||
let targetFolderKey = e.target.value
|
||||
if (status.targetFolders.length > 0) {
|
||||
let targetFolder = _.findWhere(folders, {key: targetFolderKey})
|
||||
dispatch(switchFolder(targetFolder.name))
|
||||
}
|
||||
}
|
||||
|
||||
handleTitleChange (e) {
|
||||
let { dispatch, activeArticle } = this.props
|
||||
let article = Object.assign({}, activeArticle, {
|
||||
title: e.target.value,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
dispatch(updateArticle(article))
|
||||
}
|
||||
|
||||
handleTagsChange (newTag, tags) {
|
||||
let { dispatch, activeArticle } = this.props
|
||||
let article = Object.assign({}, activeArticle, {
|
||||
tags: tags,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
|
||||
dispatch(updateArticle(article))
|
||||
}
|
||||
|
||||
handleModeChange (value) {
|
||||
let { dispatch, activeArticle } = this.props
|
||||
let article = Object.assign({}, activeArticle, {
|
||||
mode: value,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
|
||||
dispatch(updateArticle(article))
|
||||
this.switchEditMode()
|
||||
}
|
||||
|
||||
handleContentChange (value) {
|
||||
let { dispatch, activeArticle } = this.props
|
||||
if (activeArticle.content !== value) {
|
||||
let article = Object.assign({}, activeArticle, {
|
||||
content: value,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
|
||||
dispatch(updateArticle(article))
|
||||
}
|
||||
}
|
||||
|
||||
handleDeleteButtonClick (e) {
|
||||
if (this.props.activeArticle) {
|
||||
openModal(DeleteArticleModal, {articleKey: this.props.activeArticle.key})
|
||||
}
|
||||
}
|
||||
|
||||
handleTitleKeyDown (e) {
|
||||
if (e.keyCode === 9 && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
this.refs.mode.handleIdleSelectClick()
|
||||
}
|
||||
}
|
||||
|
||||
handleModeSelectKeyDown (e) {
|
||||
if (e.keyCode === 9 && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
this.switchEditMode()
|
||||
}
|
||||
|
||||
if (e.keyCode === 9 && e.shiftKey) {
|
||||
e.preventDefault()
|
||||
this.focusTitle()
|
||||
}
|
||||
|
||||
if (e.keyCode === 27) {
|
||||
this.focusTitle()
|
||||
}
|
||||
}
|
||||
|
||||
switchEditMode () {
|
||||
this.refs.editor.switchEditMode()
|
||||
}
|
||||
|
||||
focusTitle () {
|
||||
if (this.refs.title) {
|
||||
let titleEl = ReactDOM.findDOMNode(this.refs.title)
|
||||
titleEl.focus()
|
||||
titleEl.select()
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { folders, status, tags, activeArticle, modified, user } = this.props
|
||||
if (activeArticle == null) return this.renderEmpty()
|
||||
let folderOptions = folders.map(folder => {
|
||||
return (
|
||||
<option key={folder.key} value={folder.key}>{folder.name}</option>
|
||||
)
|
||||
})
|
||||
|
||||
let isUnsaved = !!_.findWhere(modified, {key: activeArticle.key})
|
||||
|
||||
return (
|
||||
<div tabIndex='4' className='ArticleDetail'>
|
||||
<div className='ArticleDetail-info'>
|
||||
<div className='ArticleDetail-info-row'>
|
||||
<select
|
||||
className='ArticleDetail-info-folder'
|
||||
value={activeArticle.FolderKey}
|
||||
onChange={e => this.handleFolderKeyChange(e)}
|
||||
>
|
||||
{folderOptions}
|
||||
</select>
|
||||
<span className='ArticleDetail-info-status'
|
||||
children={
|
||||
isUnsaved
|
||||
? <span> <span className='unsaved-mark'>●</span> Unsaved</span>
|
||||
: `Created : ${moment(activeArticle.createdAt).format('YYYY/MM/DD')} Updated : ${moment(activeArticle.updatedAt).format('YYYY/MM/DD')}`
|
||||
}
|
||||
/>
|
||||
|
||||
<div className='ArticleDetail-info-control'>
|
||||
{/*<div className={'ArticleDetail-info-control-save' + (!isUnsaved ? ' hide' : '')}>
|
||||
<button
|
||||
onClick={e => this.handleSaveButtonClick(e)}
|
||||
className='ArticleDetail-info-control-save-button'
|
||||
disabled={!isUnsaved}
|
||||
>
|
||||
<i className='fa fa-fw fa-save'/> Save
|
||||
<span className='tooltip' children={`Save Post (${OSX ? '⌘' : '^'} + S)`}/>
|
||||
</button>
|
||||
</div>*/}
|
||||
|
||||
<ShareButton
|
||||
article={activeArticle}
|
||||
user={user}
|
||||
/>
|
||||
|
||||
<button className='ArticleDetail-info-control-delete-button' onClick={e => this.handleOthersButtonClick(e)}>
|
||||
<i className='fa fa-fw fa-trash'/>
|
||||
<span className='tooltip' children={`Delete Post (^ + Del)`}/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='ArticleDetail-info-row2'>
|
||||
<TagSelect
|
||||
tags={activeArticle.tags}
|
||||
onChange={(tags, tag) => this.handleTagsChange(tags, tag)}
|
||||
suggestTags={tags}
|
||||
/>
|
||||
|
||||
{status.isTutorialOpen ? tagSelectTutorialElement : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='ArticleDetail-panel'>
|
||||
<div className='ArticleDetail-panel-header'>
|
||||
<div className='ArticleDetail-panel-header-title'>
|
||||
<input
|
||||
onKeyDown={e => this.handleTitleKeyDown(e)}
|
||||
placeholder='(Untitled)'
|
||||
ref='title'
|
||||
value={activeArticle.title}
|
||||
onChange={e => this.handleTitleChange(e)}
|
||||
style={{
|
||||
fontFamily: this.state.fontFamily
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ModeSelect
|
||||
ref='mode'
|
||||
onChange={e => this.handleModeChange(e)}
|
||||
onKeyDown={e => this.handleModeSelectKeyDown(e)}
|
||||
value={activeArticle.mode}
|
||||
className='ArticleDetail-panel-header-mode'
|
||||
/>
|
||||
{status.isTutorialOpen ? modeSelectTutorialElement : null}
|
||||
</div>
|
||||
<ArticleEditor
|
||||
ref='editor'
|
||||
article={activeArticle}
|
||||
onChange={content => this.handleContentChange(content)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ArticleDetail.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
status: PropTypes.shape(),
|
||||
tags: PropTypes.array,
|
||||
user: PropTypes.shape(),
|
||||
folders: PropTypes.array,
|
||||
modified: PropTypes.array,
|
||||
activeArticle: PropTypes.shape()
|
||||
}
|
||||
ArticleDetail.prototype.linkState = linkState
|
||||
207
browser/main/HomePage/ArticleList.js
Normal file
207
browser/main/HomePage/ArticleList.js
Normal file
@@ -0,0 +1,207 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ModeIcon from 'browser/components/ModeIcon'
|
||||
import moment from 'moment'
|
||||
import { switchArticle } from '../actions'
|
||||
import FolderMark from 'browser/components/FolderMark'
|
||||
import TagLink from './TagLink'
|
||||
import _ from 'lodash'
|
||||
|
||||
const electron = require('electron')
|
||||
const remote = electron.remote
|
||||
const ipc = electron.ipcRenderer
|
||||
|
||||
export default class ArticleList extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.focusHandler = e => this.focus()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
|
||||
ipc.on('list-focus', this.focusHandler)
|
||||
this.focus()
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearInterval(this.refreshTimer)
|
||||
ipc.removeListener('list-focus', this.focusHandler)
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
let { articles, activeArticle } = this.props
|
||||
var index = articles.indexOf(activeArticle)
|
||||
var el = ReactDOM.findDOMNode(this)
|
||||
var li = el.querySelectorAll('.ArticleList>div')[index]
|
||||
|
||||
if (li == null) {
|
||||
return
|
||||
}
|
||||
|
||||
var overflowBelow = el.clientHeight + el.scrollTop < li.offsetTop + li.clientHeight
|
||||
if (overflowBelow) {
|
||||
el.scrollTop = li.offsetTop + li.clientHeight - el.clientHeight
|
||||
}
|
||||
var overflowAbove = el.scrollTop > li.offsetTop
|
||||
if (overflowAbove) {
|
||||
el.scrollTop = li.offsetTop
|
||||
}
|
||||
}
|
||||
|
||||
focus () {
|
||||
ReactDOM.findDOMNode(this).focus()
|
||||
}
|
||||
|
||||
// 移動ができなかったらfalseを返す:
|
||||
selectPriorArticle () {
|
||||
let { articles, activeArticle, dispatch } = this.props
|
||||
let targetIndex = articles.indexOf(activeArticle) - 1
|
||||
let targetArticle = articles[targetIndex]
|
||||
|
||||
if (targetArticle != null) {
|
||||
dispatch(switchArticle(targetArticle.key))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
selectNextArticle () {
|
||||
let { articles, activeArticle, dispatch } = this.props
|
||||
let targetIndex = articles.indexOf(activeArticle) + 1
|
||||
let targetArticle = articles[targetIndex]
|
||||
|
||||
if (targetArticle != null) {
|
||||
dispatch(switchArticle(targetArticle.key))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
handleArticleClick (article) {
|
||||
let { dispatch } = this.props
|
||||
return function (e) {
|
||||
dispatch(switchArticle(article.key))
|
||||
}
|
||||
}
|
||||
|
||||
handleArticleListKeyDown (e) {
|
||||
if (e.metaKey || e.ctrlKey) return true
|
||||
|
||||
if (e.keyCode === 65 && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
remote.getCurrentWebContents().send('top-new-post')
|
||||
}
|
||||
|
||||
if (e.keyCode === 65 && e.shiftKey) {
|
||||
e.preventDefault()
|
||||
remote.getCurrentWebContents().send('nav-new-folder')
|
||||
}
|
||||
|
||||
if (e.keyCode === 68) {
|
||||
e.preventDefault()
|
||||
remote.getCurrentWebContents().send('detail-delete')
|
||||
}
|
||||
|
||||
if (e.keyCode === 84) {
|
||||
e.preventDefault()
|
||||
remote.getCurrentWebContents().send('detail-title')
|
||||
}
|
||||
|
||||
if (e.keyCode === 69) {
|
||||
e.preventDefault()
|
||||
remote.getCurrentWebContents().send('detail-edit')
|
||||
}
|
||||
|
||||
if (e.keyCode === 83) {
|
||||
e.preventDefault()
|
||||
remote.getCurrentWebContents().send('detail-save')
|
||||
}
|
||||
|
||||
if (e.keyCode === 38) {
|
||||
e.preventDefault()
|
||||
this.selectPriorArticle()
|
||||
}
|
||||
|
||||
if (e.keyCode === 40) {
|
||||
e.preventDefault()
|
||||
this.selectNextArticle()
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { articles, modified, activeArticle, folders } = this.props
|
||||
|
||||
let articleElements = articles.map(article => {
|
||||
let modifiedArticle = _.findWhere(modified, {key: article.key})
|
||||
let originalArticle = article
|
||||
if (modifiedArticle) {
|
||||
article = Object.assign({}, article)
|
||||
}
|
||||
let tagElements = Array.isArray(article.tags) && article.tags.length > 0
|
||||
? article.tags.slice().map(tag => {
|
||||
return (<TagLink key={tag} tag={tag}/>)
|
||||
})
|
||||
: (<span>Not tagged yet</span>)
|
||||
let folder = _.findWhere(folders, {key: article.FolderKey})
|
||||
let folderChanged = originalArticle.FolderKey !== article.FolderKey
|
||||
let originalFolder = folderChanged ? _.findWhere(folders, {key: originalArticle.FolderKey}) : null
|
||||
|
||||
let title = article.title.trim().length === 0
|
||||
? <small>(Untitled)</small>
|
||||
: article.title
|
||||
|
||||
return (
|
||||
<div key={'article-' + article.key}>
|
||||
<div onClick={e => this.handleArticleClick(article)(e)} className={'ArticleList-item' + (activeArticle.key === article.key ? ' active' : '')}>
|
||||
<div className='ArticleList-item-top'>
|
||||
{folder != null
|
||||
? folderChanged
|
||||
? <span className='folderName'>
|
||||
<FolderMark color={originalFolder.color}/>{originalFolder.name}
|
||||
->
|
||||
<FolderMark color={folder.color}/>{folder.name}
|
||||
</span>
|
||||
: <span className='folderName'>
|
||||
<FolderMark color={folder.color}/>{folder.name}
|
||||
</span>
|
||||
: <span><FolderMark color={-1}/>Unknown</span>
|
||||
}
|
||||
<span className='updatedAt'
|
||||
children={
|
||||
modifiedArticle != null
|
||||
? <span><span className='unsaved-mark'>●</span> Unsaved</span>
|
||||
: moment(article.updatedAt).fromNow()
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='ArticleList-item-middle'>
|
||||
<ModeIcon className='mode' mode={article.mode}/> <div className='title' children={title}/>
|
||||
</div>
|
||||
<div className='ArticleList-item-middle2'>
|
||||
<pre><code children={article.content.trim().length === 0 ? '(Empty content)' : article.content.substring(0, 50)}/></pre>
|
||||
</div>
|
||||
<div className='ArticleList-item-bottom'>
|
||||
<div className='tags'><i className='fa fa-fw fa-tags'/>{tagElements}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='divider'></div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div tabIndex='3' onKeyDown={e => this.handleArticleListKeyDown(e)} className='ArticleList'>
|
||||
{articleElements}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ArticleList.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
folders: PropTypes.array,
|
||||
articles: PropTypes.array,
|
||||
modified: PropTypes.array,
|
||||
activeArticle: PropTypes.shape()
|
||||
}
|
||||
202
browser/main/HomePage/ArticleNavigator.js
Normal file
202
browser/main/HomePage/ArticleNavigator.js
Normal file
@@ -0,0 +1,202 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import { findWhere } from 'lodash'
|
||||
import { setSearchFilter, switchFolder, uncacheArticle, saveAllArticles, switchArticle, clearSearch } from '../actions'
|
||||
import { openModal, isModalOpen } from 'browser/lib/modal'
|
||||
import FolderMark from 'browser/components/FolderMark'
|
||||
import Preferences from '../modal/Preferences'
|
||||
import CreateNewFolder from '../modal/CreateNewFolder'
|
||||
import _ from 'lodash'
|
||||
import ModeIcon from 'browser/components/ModeIcon'
|
||||
|
||||
const ipc = require('electron').ipcRenderer
|
||||
|
||||
const BRAND_COLOR = '#18AF90'
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
const preferenceTutorialElement = (
|
||||
<svg width='300' height='300' className='tutorial'>
|
||||
<text x='15' y='30' fill={BRAND_COLOR} fontSize='24'>Preference</text>
|
||||
<svg x='-30' y='-270' width='400' height='400'>
|
||||
<path fill='white' d='M165.9,297c5.3,0,10.6,0.1,15.8,0.1c3.3,0,7.7,0.8,10.7-1c2.3-1.4,3.1-4,4.5-6.2c3.5-5.5,9.6-5.2,14.6-1.9
|
||||
c4.6,3.1,8.7,8,8.4,13.8c-0.3,5.2-3.3,10.1-6.1,14.3c-3.1,4.7-6.6,7-12.2,7.9c-5.2,0.8-11.7,1.6-15.4-3
|
||||
c-6.6-8.2,2.1-20.5,7.4-27.1c6.5-8.1,20.1-14,26.4-2.1c5.4,10.3-3.1,21.7-13,24.8c-5.7,1.8-11,0.9-16.2-1.9c-2-1.1-5-2.6-6.6-4.4
|
||||
c-3.9-4.3-0.3-8.2,2.5-11.2c1.3-1.4-0.8-3.6-2.1-2.1c-2.7,2.9-5.8,6.6-5.1,10.9c0.7,4.4,5.6,6.9,9,8.9c8.6,5.1,18.7,4.8,26.8-1.2
|
||||
c7.3-5.4,11.6-15,8-23.7c-3.3-8.1-11.7-11.8-20-9c-12.5,4.1-33.7,33.5-15.9,43.1c6.8,3.7,19.8,1.8,25.3-3.6
|
||||
c6.1-5.8,12.1-17.2,9.5-25.7c-2.6-8.4-13.7-17-22.6-13.3c-1.6,0.7-3,1.7-4.1,3c-1.6,1.9-2.2,5.1-4.1,6.6c-3.1,2.4-10.1,1-13.7,1
|
||||
c-4,0-7.9,0-11.9-0.1C164,294,164,297,165.9,297L165.9,297z'/>
|
||||
</svg>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const newFolderTutorialElement = (
|
||||
<svg width='800' height='500' className='tutorial'>
|
||||
<text x='30' y='110' fill={BRAND_COLOR} fontSize='24'>Create a new folder!!</text>
|
||||
<text x='50' y='135' fill={BRAND_COLOR} fontSize='16'>{'press ' + (OSX ? '`⌘ + Shift + n`' : '`^ + Shift + n`')}</text>
|
||||
<svg x='50' y='10' width='300' height='400'>
|
||||
<path fill='white' d='M94.1,10.9C77.7,15.6,62,22.7,47.8,32.1c-13.6,9-27.7,20.4-37.1,33.9c-1.1,1.6,1.5,3.1,2.6,1.5
|
||||
C22.6,54.1,37,42.7,50.6,33.8c13.7-8.8,28.6-15.5,44.2-20C96.7,13.3,95.9,10.4,94.1,10.9L94.1,10.9z'/>
|
||||
<path fill='white' d='M71.1,8.6c7.9,1.6,15.8,3.2,23.6,4.7c-0.1-0.9-0.2-1.8-0.4-2.7c-4.6,3.4-5.4,7.7-4.4,13.2
|
||||
c0.8,4.4,0.8,10.9,5.6,12.8c1.8,0.7,2.6-2.2,0.8-2.9c-2.3-1-2.6-6.2-3-8.3c-0.9-4.5-1.7-9,2.5-12.1c0.9-0.7,1-2.5-0.4-2.7
|
||||
C87.5,9,79.6,7.4,71.8,5.9C70,5.4,69.2,8.3,71.1,8.6L71.1,8.6z'/>
|
||||
</svg>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default class ArticleNavigator extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.newFolderHandler = e => {
|
||||
if (isModalOpen()) return true
|
||||
this.handleNewFolderButton(e)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
ipc.on('nav-new-folder', this.newFolderHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
ipc.removeListener('nav-new-folder', this.newFolderHandler)
|
||||
}
|
||||
|
||||
handlePreferencesButtonClick (e) {
|
||||
openModal(Preferences)
|
||||
}
|
||||
|
||||
handleNewFolderButton (e) {
|
||||
let { user } = this.props
|
||||
openModal(CreateNewFolder, {user: user})
|
||||
}
|
||||
|
||||
handleFolderButtonClick (name) {
|
||||
return e => {
|
||||
let { dispatch } = this.props
|
||||
dispatch(switchFolder(name))
|
||||
}
|
||||
}
|
||||
|
||||
handleAllFoldersButtonClick (e) {
|
||||
let { dispatch } = this.props
|
||||
dispatch(setSearchFilter(''))
|
||||
}
|
||||
|
||||
handleUnsavedItemClick (article) {
|
||||
let { dispatch } = this.props
|
||||
return e => {
|
||||
let { articles } = this.props
|
||||
let isInArticleList = articles.some(_article => _article.key === article.key)
|
||||
if (!isInArticleList) dispatch(clearSearch())
|
||||
dispatch(switchArticle(article.key))
|
||||
}
|
||||
}
|
||||
|
||||
handleUncacheButtonClick (article) {
|
||||
let { dispatch } = this.props
|
||||
return e => {
|
||||
dispatch(uncacheArticle(article.key))
|
||||
}
|
||||
}
|
||||
|
||||
handleSaveAllClick (e) {
|
||||
let { dispatch } = this.props
|
||||
dispatch(saveAllArticles())
|
||||
}
|
||||
|
||||
render () {
|
||||
let { status, user, folders, allArticles, modified, activeArticle } = this.props
|
||||
let { targetFolders } = status
|
||||
if (targetFolders == null) targetFolders = []
|
||||
|
||||
let modifiedElements = modified.map(modifiedArticle => {
|
||||
let originalArticle = _.findWhere(allArticles, {key: modifiedArticle.key})
|
||||
if (originalArticle == null) return false
|
||||
let combinedArticle = Object.assign({}, originalArticle, modifiedArticle)
|
||||
|
||||
let className = 'ArticleNavigator-unsaved-list-item'
|
||||
if (activeArticle && activeArticle.key === combinedArticle.key) className += ' active'
|
||||
|
||||
return (
|
||||
<div key={modifiedArticle.key} onClick={e => this.handleUnsavedItemClick(combinedArticle)(e)} className={className}>
|
||||
<div className='ArticleNavigator-unsaved-list-item-label'>
|
||||
<ModeIcon mode={combinedArticle.mode}/>
|
||||
{combinedArticle.title.trim().length > 0
|
||||
? combinedArticle.title
|
||||
: <span className='ArticleNavigator-unsaved-list-item-label-untitled'>(Untitled)</span>}
|
||||
</div>
|
||||
<button onClick={e => this.handleUncacheButtonClick(combinedArticle)(e)} className='ArticleNavigator-unsaved-list-item-discard-button'><i className='fa fa-times'/></button>
|
||||
</div>
|
||||
)
|
||||
}).filter(modifiedArticle => modifiedArticle).sort((a, b) => a.updatedAt - b.updatedAt)
|
||||
let hasModified = modifiedElements.length > 0
|
||||
|
||||
let folderElememts = folders.map((folder, index) => {
|
||||
let isActive = findWhere(targetFolders, {key: folder.key})
|
||||
let articleCount = allArticles.filter(article => article.FolderKey === folder.key && article.status !== 'NEW').length
|
||||
|
||||
return (
|
||||
<button onClick={e => this.handleFolderButtonClick(folder.name)(e)} key={'folder-' + folder.key} className={isActive ? 'active' : ''}>
|
||||
<FolderMark color={folder.color}/> {folder.name} <span className='articleCount'>{articleCount}</span>
|
||||
</button>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div tabIndex='1' className='ArticleNavigator'>
|
||||
<div className='userInfo'>
|
||||
<div className='userProfileName'>{user.name}</div>
|
||||
<div className='userName'>localStorage</div>
|
||||
<button onClick={e => this.handlePreferencesButtonClick(e)} className='settingBtn'>
|
||||
<i className='fa fa-fw fa-chevron-down'/>
|
||||
<span className='tooltip'>Preferences</span>
|
||||
</button>
|
||||
|
||||
{status.isTutorialOpen ? preferenceTutorialElement : null}
|
||||
|
||||
</div>
|
||||
|
||||
{/*<div className={'ArticleNavigator-unsaved' + (hasModified ? '' : ' hide')}>
|
||||
<div className='ArticleNavigator-unsaved-header'>Work in progress</div>
|
||||
<div className='ArticleNavigator-unsaved-list'>
|
||||
{modifiedElements}
|
||||
</div>
|
||||
<div className='ArticleNavigator-unsaved-control'>
|
||||
<button onClick={e => this.handleSaveAllClick()} className='ArticleNavigator-unsaved-control-save-all-button' disabled={modifiedElements.length === 0}>Save all</button>
|
||||
</div>
|
||||
</div>*/}
|
||||
|
||||
|
||||
|
||||
<div className={'ArticleNavigator-folders expand'}>
|
||||
{status.isTutorialOpen ? newFolderTutorialElement : null}
|
||||
<div className='ArticleNavigator-folders-header'>
|
||||
<div className='title'>Folders</div>
|
||||
<button onClick={e => this.handleNewFolderButton(e)} className='addBtn'>
|
||||
<i className='fa fa-fw fa-plus'/>
|
||||
<span className='tooltip'>Create a new folder ({OSX ? '⌘' : '^'} + Shift + n)</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className='folderList'>
|
||||
<button onClick={e => this.handleAllFoldersButtonClick(e)} className={targetFolders.length === 0 ? 'active' : ''}>All folders</button>
|
||||
{folderElememts}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ArticleNavigator.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
status: PropTypes.shape({
|
||||
folderId: PropTypes.number
|
||||
}),
|
||||
user: PropTypes.object,
|
||||
folders: PropTypes.array,
|
||||
allArticles: PropTypes.array,
|
||||
articles: PropTypes.array,
|
||||
modified: PropTypes.array,
|
||||
activeArticle: PropTypes.shape({
|
||||
key: PropTypes.string
|
||||
})
|
||||
}
|
||||
|
||||
269
browser/main/HomePage/ArticleTopBar.js
Normal file
269
browser/main/HomePage/ArticleTopBar.js
Normal file
@@ -0,0 +1,269 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ExternalLink from 'browser/components/ExternalLink'
|
||||
import { setSearchFilter, clearSearch, toggleTutorial, saveArticle, switchFolder } from '../actions'
|
||||
import { isModalOpen } from 'browser/lib/modal'
|
||||
import keygen from 'browser/lib/keygen'
|
||||
import activityRecord from 'browser/lib/activityRecord'
|
||||
|
||||
const electron = require('electron')
|
||||
const remote = electron.remote
|
||||
const ipc = electron.ipcRenderer
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
const BRAND_COLOR = '#18AF90'
|
||||
|
||||
const searchTutorialElement = (
|
||||
<svg width='750' height='300' className='tutorial'>
|
||||
<text x='125' y='63' fill={BRAND_COLOR} fontSize='24'>Search some posts!!</text>
|
||||
<text x='125' y='90' fill={BRAND_COLOR} fontSize='18'>{'- Search by tag : #{string}'}</text>
|
||||
<text x='125' y='115' fill={BRAND_COLOR} fontSize='18'>
|
||||
{'- Search by folder : /{folder_name}\n'}</text>
|
||||
<text x='140' y='135' fill={BRAND_COLOR} fontSize='14'>
|
||||
{'exact match : //{folder_name}'}</text>
|
||||
|
||||
<svg x='90' width='500' height='300'>
|
||||
<path fill='white' d='M27.2,6.9c-1.7,3.5-6,4.8-8,8.2c-1.8,3.1-2.1,6.8-1.8,10.2c0.7,7,4.2,16.7,10.3,20.7c0.5,0.4,1.4,0.2,1.8-0.2
|
||||
c0.1-0.1,0.2-0.2,0.3-0.3c0.6-0.6,0.6-1.5,0-2.1c-0.2-0.2-0.3-0.4-0.5-0.5c-1.3-1.4-3.2,0.7-1.9,2.1c0.2,0.2-0.3,0.4,0.7,0.5
|
||||
c0-0.7,0-1.4,0-2.1c0,0.1-0.4,0.2-0.5,0.3c0.6-0.1,1.1-0.2,1.7-0.2c-5.7-3.7-9.2-14.5-9-20.9c0.1-4,1.6-6.7,4.8-9.1
|
||||
c2-1.5,3.6-2.6,4.7-4.9C30.6,6.7,28,5.2,27.2,6.9L27.2,6.9z'/>
|
||||
<path fill='white' d='M9.5,24.4c2.4-2.7,4.9-5.4,7.3-8c2.5-2.8,5.7-7.6,9.9-7.8c-0.5-0.5-1-1-1.5-1.5c0.1,6.8,1.9,13.1,5.3,18.9
|
||||
c1,1.7,3.6,0.2,2.6-1.5c-3.2-5.4-4.8-11.1-4.9-17.4c0-0.8-0.7-1.5-1.5-1.5c-3.6,0.2-5.9,2.1-8.3,4.7c-3.7,3.9-7.3,8-11,12
|
||||
C6.1,23.7,8.2,25.9,9.5,24.4L9.5,24.4z'/>
|
||||
</svg>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const newPostTutorialElement = (
|
||||
<svg width='900' height='900' className='tutorial'>
|
||||
<text x='470' y='50' fill={BRAND_COLOR} fontSize='24'>Create a new post!!</text>
|
||||
<text x='490' y='75' fill={BRAND_COLOR} fontSize='16' children={`press \`${OSX ? '⌘' : '^'} + n\``}/>
|
||||
<svg x='415' y='20' width='400' height='400'>
|
||||
<path fill='white' d='M11.6,14.7c1,5.5,2.9,10.7,5.7,15.5c1,1.7,3.5,0.2,2.6-1.5c-2.6-4.7-4.4-9.6-5.4-14.8
|
||||
C14.1,12,11.3,12.8,11.6,14.7L11.6,14.7z'/>
|
||||
<path fill='white' d='M16.8,17.1c4,0.2,7.6-1.1,10.7-3.6c1.5-1.2-0.6-3.3-2.1-2.1c-2.4,2-5.4,2.9-8.6,2.7C14.9,14,14.9,17,16.8,17.1
|
||||
L16.8,17.1z'/>
|
||||
<path fill='white' d='M13.8,17.6c11.9,3.5,24.1,4.9,36.4,3.9c1.9-0.1,1.9-3.1,0-3c-12.1,0.9-24-0.3-35.6-3.8
|
||||
C12.7,14.1,11.9,17,13.8,17.6L13.8,17.6z'/>
|
||||
</svg>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default class ArticleTopBar extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.saveAllHandler = e => {
|
||||
if (isModalOpen()) return true
|
||||
this.handleSaveAllButtonClick(e)
|
||||
}
|
||||
this.focusSearchHandler = e => {
|
||||
if (isModalOpen()) return true
|
||||
this.focusInput(e)
|
||||
}
|
||||
this.newPostHandler = e => {
|
||||
if (isModalOpen()) return true
|
||||
this.handleNewPostButtonClick(e)
|
||||
}
|
||||
|
||||
this.state = {
|
||||
isTooltipHidden: true,
|
||||
isLinksDropdownOpen: false
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.searchInput = ReactDOM.findDOMNode(this.refs.searchInput)
|
||||
this.linksButton = ReactDOM.findDOMNode(this.refs.links)
|
||||
this.showLinksDropdown = e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (!this.state.isLinksDropdownOpen) {
|
||||
this.setState({isLinksDropdownOpen: true})
|
||||
}
|
||||
}
|
||||
this.linksButton.addEventListener('click', this.showLinksDropdown)
|
||||
this.hideLinksDropdown = e => {
|
||||
if (this.state.isLinksDropdownOpen) {
|
||||
this.setState({isLinksDropdownOpen: false})
|
||||
}
|
||||
}
|
||||
document.addEventListener('click', this.hideLinksDropdown)
|
||||
|
||||
// ipc.on('top-save-all', this.saveAllHandler)
|
||||
ipc.on('top-focus-search', this.focusSearchHandler)
|
||||
ipc.on('top-new-post', this.newPostHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('click', this.hideLinksDropdown)
|
||||
this.linksButton.removeEventListener('click', this.showLinksDropdown())
|
||||
|
||||
// ipc.removeListener('top-save-all', this.saveAllHandler)
|
||||
ipc.removeListener('top-focus-search', this.focusSearchHandler)
|
||||
ipc.removeListener('top-new-post', this.newPostHandler)
|
||||
}
|
||||
|
||||
handleTooltipRequest (e) {
|
||||
if (this.searchInput.value.length === 0 && (document.activeElement === this.searchInput)) {
|
||||
this.setState({isTooltipHidden: false})
|
||||
} else {
|
||||
this.setState({isTooltipHidden: true})
|
||||
}
|
||||
}
|
||||
|
||||
isInputFocused () {
|
||||
return document.activeElement === ReactDOM.findDOMNode(this.refs.searchInput)
|
||||
}
|
||||
|
||||
escape () {
|
||||
let { status, dispatch } = this.props
|
||||
if (status.search.length > 0) {
|
||||
dispatch(clearSearch())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
focusInput () {
|
||||
this.searchInput.focus()
|
||||
}
|
||||
|
||||
blurInput () {
|
||||
this.searchInput.blur()
|
||||
}
|
||||
|
||||
handleSearchChange (e) {
|
||||
let { dispatch } = this.props
|
||||
|
||||
dispatch(setSearchFilter(e.target.value))
|
||||
this.handleTooltipRequest()
|
||||
}
|
||||
|
||||
handleSearchClearButton (e) {
|
||||
this.searchInput.value = ''
|
||||
this.focusInput()
|
||||
}
|
||||
|
||||
handleNewPostButtonClick (e) {
|
||||
let { dispatch, folders, status } = this.props
|
||||
let { targetFolders } = status
|
||||
|
||||
let isFolderFilterApplied = targetFolders.length > 0
|
||||
let FolderKey = isFolderFilterApplied
|
||||
? targetFolders[0].key
|
||||
: folders[0].key
|
||||
|
||||
let newArticle = {
|
||||
key: keygen(),
|
||||
title: '',
|
||||
content: '',
|
||||
mode: 'markdown',
|
||||
tags: [],
|
||||
FolderKey: FolderKey,
|
||||
craetedAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
|
||||
dispatch(saveArticle(newArticle.key, newArticle, true))
|
||||
if (isFolderFilterApplied) dispatch(switchFolder(targetFolders[0].name))
|
||||
remote.getCurrentWebContents().send('detail-title')
|
||||
activityRecord.emit('ARTICLE_CREATE')
|
||||
}
|
||||
|
||||
handleTutorialButtonClick (e) {
|
||||
let { dispatch } = this.props
|
||||
|
||||
dispatch(toggleTutorial())
|
||||
}
|
||||
|
||||
render () {
|
||||
let { status } = this.props
|
||||
return (
|
||||
<div tabIndex='2' className='ArticleTopBar'>
|
||||
<div className='ArticleTopBar-left'>
|
||||
<div className='ArticleTopBar-left-search'>
|
||||
<i className='fa fa-search fa-fw' />
|
||||
<input
|
||||
ref='searchInput'
|
||||
onFocus={e => this.handleSearchChange(e)}
|
||||
onBlur={e => this.handleSearchChange(e)}
|
||||
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='ArticleTopBar-left-search-clear-button'><i className='fa fa-times'/></button>
|
||||
: null
|
||||
}
|
||||
<div className={'tooltip' + (this.state.isTooltipHidden ? ' hide' : '')}>
|
||||
<ul>
|
||||
<li>- Search by tag : #{'{string}'}</li>
|
||||
<li>- Search by folder : /{'{folder_name}'}<br/><small>exact match : //{'{folder_name}'}</small></li>
|
||||
<li>- Only unsaved : --unsaved</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{status.isTutorialOpen ? searchTutorialElement : null}
|
||||
|
||||
<div className={'ArticleTopBar-left-control'}>
|
||||
<button className='ArticleTopBar-left-control-new-post-button' onClick={e => this.handleNewPostButtonClick(e)}>
|
||||
<i className='fa fa-plus'/>
|
||||
<span className='tooltip'>New Post ({OSX ? '⌘' : '^'} + n)</span>
|
||||
</button>
|
||||
{status.isTutorialOpen ? newPostTutorialElement : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='ArticleTopBar-right'>
|
||||
<button onClick={e => this.handleTutorialButtonClick(e)}>?<span className='tooltip'>How to use</span>
|
||||
</button>
|
||||
<a ref='links' className='ArticleTopBar-right-links-button' href>
|
||||
<img src='../resources/app.png' width='44' height='44'/>
|
||||
</a>
|
||||
{
|
||||
this.state.isLinksDropdownOpen
|
||||
? (
|
||||
<div className='ArticleTopBar-right-links-button-dropdown'>
|
||||
<ExternalLink className='ArticleTopBar-right-links-button-dropdown-item' href='https://b00st.io'>
|
||||
<i className='fa fa-fw fa-home'/>Boost official page
|
||||
</ExternalLink>
|
||||
<ExternalLink className='ArticleTopBar-right-links-button-dropdown-item' href='https://github.com/BoostIO/Boostnote/issues'>
|
||||
<i className='fa fa-fw fa-github'/> Issues
|
||||
</ExternalLink>
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
|
||||
{status.isTutorialOpen ? (
|
||||
<div className='tutorial'>
|
||||
<div onClick={e => this.handleTutorialButtonClick(e)} className='clickJammer'/>
|
||||
<svg width='500' height='250' className='finder'>
|
||||
<text x='100' y='25' fontSize='32' fill={BRAND_COLOR}>Also, you can open Finder!!</text>
|
||||
<text x='150' y='55' fontSize='18' fill={BRAND_COLOR} children={'with pressing ' + (OSX ? '`⌘ + Alt + s`' : '`Win + Alt + s`')}/>
|
||||
</svg>
|
||||
<svg width='450' className='global'>
|
||||
<text x='100' y='45' fontSize='24' fill={BRAND_COLOR}>Hope you to enjoy our app :D</text>
|
||||
<text x='50' y='75' fontSize='18' fill={BRAND_COLOR}>Press any key or click to escape tutorial mode</text>
|
||||
</svg>
|
||||
<div className='back'></div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ArticleTopBar.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
status: PropTypes.shape({
|
||||
search: PropTypes.string
|
||||
}),
|
||||
folders: PropTypes.array
|
||||
}
|
||||
18
browser/main/HomePage/TagLink.js
Normal file
18
browser/main/HomePage/TagLink.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import store from '../store'
|
||||
import { setTagFilter } from '../actions'
|
||||
|
||||
export default class TagLink extends React.Component {
|
||||
handleClick (e) {
|
||||
store.dispatch(setTagFilter(this.props.tag))
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<a onClick={e => this.handleClick(e)}>{this.props.tag}</a>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TagLink.propTypes = {
|
||||
tag: PropTypes.string
|
||||
}
|
||||
41
browser/main/HomePage/UserNavigator.js
Normal file
41
browser/main/HomePage/UserNavigator.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import { Link } from 'react-router'
|
||||
import ProfileImage from 'browser/components/ProfileImage'
|
||||
|
||||
export default class UserNavigator extends Component {
|
||||
renderUserList () {
|
||||
if (this.props.users == null) return null
|
||||
|
||||
var users = this.props.users.map((user, index) => (
|
||||
<li key={'user-' + user.id}>
|
||||
<Link to={'/users/' + user.id} activeClassName='active'>
|
||||
<ProfileImage email={user.email} size='44'/>
|
||||
<div className='userTooltip'>{user.name}</div>
|
||||
{index < 9 ? <div className='keyLabel'>{'⌘' + (index + 1)}</div> : null}
|
||||
</Link>
|
||||
</li>
|
||||
))
|
||||
|
||||
return (
|
||||
<ul className='userList'>
|
||||
{users}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='UserNavigator'>
|
||||
{this.renderUserList()}
|
||||
<button className='createTeamBtn'>
|
||||
+
|
||||
<div className='tooltip'>Create a new team</div>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
UserNavigator.propTypes = {
|
||||
users: PropTypes.array
|
||||
}
|
||||
241
browser/main/HomePage/index.js
Normal file
241
browser/main/HomePage/index.js
Normal file
@@ -0,0 +1,241 @@
|
||||
import React, { PropTypes} from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { toggleTutorial } from '../actions'
|
||||
import ArticleNavigator from './ArticleNavigator'
|
||||
import ArticleTopBar from './ArticleTopBar'
|
||||
import ArticleList from './ArticleList'
|
||||
import ArticleDetail from './ArticleDetail'
|
||||
import _ from 'lodash'
|
||||
import { isModalOpen, closeModal } from 'browser/lib/modal'
|
||||
|
||||
const electron = require('electron')
|
||||
const remote = electron.remote
|
||||
|
||||
const TEXT_FILTER = 'TEXT_FILTER'
|
||||
const FOLDER_FILTER = 'FOLDER_FILTER'
|
||||
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
|
||||
const TAG_FILTER = 'TAG_FILTER'
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
class HomePage extends React.Component {
|
||||
componentDidMount () {
|
||||
// React自体のKey入力はfocusされていないElementからは動かないため、
|
||||
// `window`に直接かける
|
||||
this.keyHandler = e => this.handleKeyDown(e)
|
||||
window.addEventListener('keydown', this.keyHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('keydown', this.keyHandler)
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
if (isModalOpen()) {
|
||||
if (e.keyCode === 13 && (OSX ? e.metaKey : e.ctrlKey)) {
|
||||
remote.getCurrentWebContents().send('modal-confirm')
|
||||
}
|
||||
if (e.keyCode === 27) closeModal()
|
||||
return
|
||||
}
|
||||
|
||||
let { status, dispatch } = this.props
|
||||
let { top, list } = this.refs
|
||||
let listElement = ReactDOM.findDOMNode(list)
|
||||
|
||||
if (status.isTutorialOpen) {
|
||||
dispatch(toggleTutorial())
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (e.keyCode === 13 && top.isInputFocused()) {
|
||||
listElement.focus()
|
||||
return
|
||||
}
|
||||
if (e.keyCode === 27 && top.isInputFocused()) {
|
||||
if (status.search.length > 0) top.escape()
|
||||
else listElement.focus()
|
||||
return
|
||||
}
|
||||
|
||||
// Search inputがfocusされていたら大体のキー入力は無視される。
|
||||
if (e.keyCode === 27) {
|
||||
if (document.activeElement !== listElement) {
|
||||
listElement.focus()
|
||||
} else {
|
||||
top.focusInput()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { dispatch, status, user, articles, allArticles, modified, activeArticle, folders, tags } = this.props
|
||||
|
||||
return (
|
||||
<div className='HomePage'>
|
||||
<ArticleNavigator
|
||||
ref='nav'
|
||||
dispatch={dispatch}
|
||||
status={status}
|
||||
user={user}
|
||||
folders={folders}
|
||||
allArticles={allArticles}
|
||||
articles={articles}
|
||||
modified={modified}
|
||||
activeArticle={activeArticle}
|
||||
/>
|
||||
<ArticleTopBar
|
||||
ref='top'
|
||||
dispatch={dispatch}
|
||||
status={status}
|
||||
folders={folders}
|
||||
/>
|
||||
<ArticleList
|
||||
ref='list'
|
||||
dispatch={dispatch}
|
||||
folders={folders}
|
||||
articles={articles}
|
||||
modified={modified}
|
||||
activeArticle={activeArticle}
|
||||
/>
|
||||
<ArticleDetail
|
||||
ref='detail'
|
||||
dispatch={dispatch}
|
||||
status={status}
|
||||
tags={tags}
|
||||
user={user}
|
||||
folders={folders}
|
||||
modified={modified}
|
||||
activeArticle={activeArticle}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore invalid key
|
||||
function ignoreInvalidKey (key) {
|
||||
return key.length > 0 && !key.match(/^\/\/$/) && !key.match(/^\/$/) && !key.match(/^#$/) && !key.match(/^--/)
|
||||
}
|
||||
|
||||
// Build filter object by key
|
||||
function buildFilter (key) {
|
||||
if (key.match(/^\/\/.+/)) {
|
||||
return {type: FOLDER_EXACT_FILTER, value: key.match(/^\/\/(.+)$/)[1]}
|
||||
}
|
||||
if (key.match(/^\/.+/)) {
|
||||
return {type: FOLDER_FILTER, value: key.match(/^\/(.+)$/)[1]}
|
||||
}
|
||||
if (key.match(/^#(.+)/)) {
|
||||
return {type: TAG_FILTER, value: key.match(/^#(.+)$/)[1]}
|
||||
}
|
||||
return {type: TEXT_FILTER, value: key}
|
||||
}
|
||||
|
||||
function isContaining (target, needle) {
|
||||
return target.match(new RegExp(_.escapeRegExp(needle), 'i'))
|
||||
}
|
||||
|
||||
function startsWith (target, needle) {
|
||||
return target.match(new RegExp('^' + _.escapeRegExp(needle), 'i'))
|
||||
}
|
||||
|
||||
function remap (state) {
|
||||
let { user, folders, status } = state
|
||||
let _articles = state.articles
|
||||
|
||||
let articles = _articles != null ? _articles.data : []
|
||||
let modified = _articles != null ? _articles.modified : []
|
||||
|
||||
articles.sort((a, b) => {
|
||||
let match = new Date(b.updatedAt) - new Date(a.updatedAt)
|
||||
if (match === 0) match = b.title.localeCompare(a.title)
|
||||
if (match === 0) match = b.key.localeCompare(a.key)
|
||||
return match
|
||||
})
|
||||
let allArticles = articles.slice()
|
||||
|
||||
let tags = _.uniq(allArticles.reduce((sum, article) => {
|
||||
if (!_.isArray(article.tags)) return sum
|
||||
return sum.concat(article.tags)
|
||||
}, []))
|
||||
|
||||
if (status.search.split(' ').some(key => key === '--unsaved')) articles = articles.filter(article => _.findWhere(modified, {key: article.key}))
|
||||
// Filter articles
|
||||
let filters = status.search.split(' ')
|
||||
.map(key => key.trim())
|
||||
.filter(ignoreInvalidKey)
|
||||
.map(buildFilter)
|
||||
|
||||
let folderExactFilters = filters.filter(filter => filter.type === FOLDER_EXACT_FILTER)
|
||||
let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER)
|
||||
let textFilters = filters.filter(filter => filter.type === TEXT_FILTER)
|
||||
let tagFilters = filters.filter(filter => filter.type === TAG_FILTER)
|
||||
|
||||
let targetFolders
|
||||
if (folders != null) {
|
||||
let exactTargetFolders = folders.filter(folder => {
|
||||
return _.find(folderExactFilters, filter => filter.value.toLowerCase() === folder.name.toLowerCase())
|
||||
})
|
||||
let fuzzyTargetFolders = folders.filter(folder => {
|
||||
return _.find(folderFilters, filter => startsWith(folder.name.replace(/_/g, ''), filter.value.replace(/_/g, '')))
|
||||
})
|
||||
targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders)
|
||||
|
||||
if (targetFolders.length > 0) {
|
||||
articles = articles.filter(article => {
|
||||
return _.findWhere(targetFolders, {key: article.FolderKey})
|
||||
})
|
||||
}
|
||||
|
||||
if (textFilters.length > 0) {
|
||||
articles = textFilters.reduce((articles, textFilter) => {
|
||||
return articles.filter(article => {
|
||||
return isContaining(article.title, textFilter.value) || isContaining(article.content, textFilter.value)
|
||||
})
|
||||
}, articles)
|
||||
}
|
||||
|
||||
if (tagFilters.length > 0) {
|
||||
articles = tagFilters.reduce((articles, tagFilter) => {
|
||||
return articles.filter(article => {
|
||||
return _.find(article.tags, tag => isContaining(tag, tagFilter.value))
|
||||
})
|
||||
}, articles)
|
||||
}
|
||||
}
|
||||
|
||||
// Grab active article
|
||||
let activeArticle = _.findWhere(articles, {key: status.articleKey})
|
||||
if (activeArticle == null) activeArticle = articles[0]
|
||||
|
||||
return {
|
||||
user,
|
||||
folders,
|
||||
status,
|
||||
articles,
|
||||
allArticles,
|
||||
modified,
|
||||
activeArticle,
|
||||
tags
|
||||
}
|
||||
}
|
||||
|
||||
HomePage.propTypes = {
|
||||
status: PropTypes.shape(),
|
||||
user: PropTypes.shape({
|
||||
name: PropTypes.string
|
||||
}),
|
||||
articles: PropTypes.array,
|
||||
allArticles: PropTypes.array,
|
||||
modified: PropTypes.array,
|
||||
activeArticle: PropTypes.shape(),
|
||||
dispatch: PropTypes.func,
|
||||
folders: PropTypes.array,
|
||||
tags: PropTypes.array
|
||||
}
|
||||
|
||||
export default connect(remap)(HomePage)
|
||||
54
browser/main/MainPage.js
Normal file
54
browser/main/MainPage.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const electron = require('electron')
|
||||
const ipc = electron.ipcRenderer
|
||||
import React, { PropTypes } from 'react'
|
||||
import HomePage from './HomePage'
|
||||
|
||||
export default class MainContainer extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {updateAvailable: false}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
ipc.on('update-available', function (message) {
|
||||
this.setState({updateAvailable: true})
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
updateApp () {
|
||||
ipc.send('update-app', 'Deal with it.')
|
||||
}
|
||||
|
||||
handleWheel (e) {
|
||||
if (e.ctrlKey && global.process.platform !== 'darwin') {
|
||||
if (window.document.body.style.zoom == null) {
|
||||
window.document.body.style.zoom = 1
|
||||
}
|
||||
|
||||
let zoom = Number(window.document.body.style.zoom)
|
||||
if (e.deltaY > 0 && zoom < 4) {
|
||||
document.body.style.zoom = zoom + 0.05
|
||||
} else if (e.deltaY < 0 && zoom > 0.5) {
|
||||
document.body.style.zoom = zoom - 0.05
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div
|
||||
className='Main'
|
||||
onWheel={(e) => this.handleWheel(e)}
|
||||
>
|
||||
{this.state.updateAvailable ? (
|
||||
<button onClick={this.updateApp} className='appUpdateButton'><i className='fa fa-cloud-download'/> Update available!</button>
|
||||
) : null}
|
||||
<HomePage/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MainContainer.propTypes = {
|
||||
children: PropTypes.element
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
function basicFilter (keyword, articles) {
|
||||
if (keyword === '' || keyword == null) return articles
|
||||
var firstFiltered = articles.filter(function (article) {
|
||||
|
||||
var first = article.type === 'code' ? article.description : article.title
|
||||
if (first.match(new RegExp(keyword, 'i'))) return true
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
var secondFiltered = articles.filter(function (article) {
|
||||
var second = article.type === 'code' ? article.content : article.content
|
||||
if (second.match(new RegExp(keyword, 'i'))) return true
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return firstFiltered.concat(secondFiltered).filter(function (value, index, self) {
|
||||
return self.indexOf(value) === index
|
||||
})
|
||||
}
|
||||
|
||||
function codeFilter (articles) {
|
||||
return articles.filter(function (article) {
|
||||
return article.type === 'code'
|
||||
})
|
||||
}
|
||||
|
||||
function noteFilter (articles) {
|
||||
return articles.filter(function (article) {
|
||||
return article.type === 'note'
|
||||
})
|
||||
}
|
||||
|
||||
function tagFilter (keyword, articles) {
|
||||
return articles.filter(function (article) {
|
||||
return article.Tags.some(function (tag) {
|
||||
return tag.name.match(new RegExp('^' + keyword, 'i'))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function searchArticle (search, articles) {
|
||||
var keywords = search.split(' ')
|
||||
|
||||
for (var keyword of keywords) {
|
||||
if (keyword.match(/^\$c/, 'i')) {
|
||||
articles = codeFilter(articles)
|
||||
continue
|
||||
} else if (keyword.match(/^\$n/, 'i')) {
|
||||
articles = noteFilter(articles)
|
||||
continue
|
||||
} else if (keyword.match(/^#[A-Za-z0-9]+/)) {
|
||||
articles = tagFilter(keyword.substring(1, keyword.length), articles)
|
||||
continue
|
||||
}
|
||||
articles = basicFilter(keyword, articles)
|
||||
}
|
||||
|
||||
return articles.sort(function (a, b) {
|
||||
return new Date(b.updatedAt) - new Date(a.updatedAt)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
searchArticle: searchArticle
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/* global localStorage*/
|
||||
|
||||
var mixin = {}
|
||||
|
||||
mixin.OnlyGuest = {
|
||||
componentDidMount: function () {
|
||||
var currentUser = localStorage.getItem('currentUser')
|
||||
|
||||
if (currentUser == null) {
|
||||
return
|
||||
}
|
||||
this.transitionTo('userHome', {userName: currentUser.name})
|
||||
}
|
||||
}
|
||||
|
||||
mixin.OnlyUser = {
|
||||
componentDidMount: function () {
|
||||
var currentUser = localStorage.getItem('currentUser')
|
||||
|
||||
if (currentUser == null) {
|
||||
this.transitionTo('login')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = mixin
|
||||
@@ -1,8 +0,0 @@
|
||||
var shell = require('shell')
|
||||
|
||||
module.exports = {
|
||||
openExternal: function (e) {
|
||||
shell.openExternal(e.currentTarget.href)
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
var ForceUpdate = function (interval) {
|
||||
return {
|
||||
componentDidMount: function () {
|
||||
this.refreshTimer = setInterval(function () {
|
||||
this.forceUpdate()
|
||||
}.bind(this), interval)
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
clearInterval(this.refreshTimer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ForceUpdate
|
||||
@@ -1,33 +0,0 @@
|
||||
function deleteItemFromTargetArray (item, targetArray) {
|
||||
if (targetArray == null) targetArray = []
|
||||
targetArray.some(function (_item, index) {
|
||||
if (_item.id === item.id) {
|
||||
targetArray.splice(index, 1)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return targetArray
|
||||
}
|
||||
|
||||
function updateItemToTargetArray (item, targetArray) {
|
||||
if (targetArray == null) targetArray = []
|
||||
|
||||
var isNew = !targetArray.some(function (_item, index) {
|
||||
if (_item.id === item.id) {
|
||||
targetArray.splice(index, 1, item)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if (isNew) targetArray.push(item)
|
||||
|
||||
return targetArray
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
deleteItemFromTargetArray: deleteItemFromTargetArray,
|
||||
updateItemToTargetArray: updateItemToTargetArray
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
var Reflux = require('reflux')
|
||||
|
||||
var state = {
|
||||
|
||||
}
|
||||
|
||||
var keyDown = Reflux.createAction()
|
||||
|
||||
var KeyStore = Reflux.createStore({
|
||||
init: function () {
|
||||
this.listenTo(keyDown, this.onKeyDown)
|
||||
document.addEventListener('keydown', function (e) {
|
||||
keyDown(e)
|
||||
})
|
||||
},
|
||||
setState: function (newState, cb) {
|
||||
for (var key in newState) {
|
||||
state[key] = newState[key]
|
||||
}
|
||||
if (typeof cb === 'function') cb()
|
||||
},
|
||||
onKeyDown: function (e) {
|
||||
/*
|
||||
Modals
|
||||
*/
|
||||
if (state.codeForm || state.noteForm || state.noteDeleteModal || state.codeDeleteModal || state.addMemberModal || state.aboutModal || state.editProfileModal || state.contactModal || state.teamCreateModal || state.planetCreateModal || state.planetSettingModal || state.teamSettingsModal || state.logoutModal) {
|
||||
// ESC
|
||||
if (e.keyCode === 27) this.cast('closeModal')
|
||||
|
||||
// Cmd + Enter
|
||||
if (e.keyCode === 13 && e.metaKey) {
|
||||
if (state.codeForm) this.cast('submitCodeForm')
|
||||
if (state.noteForm) this.cast('submitNoteForm')
|
||||
if (state.codeDeleteModal) this.cast('submitCodeDeleteModal')
|
||||
if (state.noteDeleteModal) this.cast('submitNoteDeleteModal')
|
||||
if (state.addMemberModal) this.cast('submitAddMemberModal')
|
||||
if (state.contactModal) this.cast('submitContactModal')
|
||||
if (state.teamCreateModal) this.cast('submitTeamCreateModal')
|
||||
if (state.planetCreateModal) this.cast('submitPlanetCreateModal')
|
||||
if (state.logoutModal) this.cast('submitLogoutModal')
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
PlanetContainer
|
||||
*/
|
||||
if (state.planetContainer) {
|
||||
// Cmd + Enter, A
|
||||
if ((e.keyCode === 13 && e.metaKey) || e.keyCode === 65) this.cast('openLaunchModal')
|
||||
|
||||
// Esc
|
||||
if (e.keyCode === 27) this.cast('toggleFocusSearchInput')
|
||||
|
||||
// Up
|
||||
if (e.keyCode === 38) this.cast('selectPriorArticle')
|
||||
|
||||
// Down
|
||||
if (e.keyCode === 40) this.cast('selectNextArticle')
|
||||
|
||||
// E
|
||||
if (e.keyCode === 69) this.cast('openEditModal')
|
||||
|
||||
// D
|
||||
if (e.keyCode === 68) this.cast('openDeleteModal')
|
||||
}
|
||||
|
||||
/*
|
||||
HomeContainer
|
||||
*/
|
||||
if (state.homeContainer) {
|
||||
if (e.keyCode > 48 && e.keyCode < 58 && e.metaKey) {
|
||||
this.cast('switchPlanet', e.keyCode - 48)
|
||||
}
|
||||
}
|
||||
},
|
||||
cast: function (status, data) {
|
||||
this.trigger({
|
||||
status: status,
|
||||
data: data
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = function (stateKey) {
|
||||
return {
|
||||
mixins: [Reflux.listenTo(KeyStore, 'onKeyCast')],
|
||||
componentDidMount: function () {
|
||||
var newState = {}
|
||||
newState[stateKey] = true
|
||||
KeyStore.setState(newState)
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
var newState = {}
|
||||
newState[stateKey] = false
|
||||
KeyStore.setState(newState)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
var markdownit = require('markdown-it')
|
||||
var md = markdownit({
|
||||
typographer: true,
|
||||
linkify: true
|
||||
})
|
||||
|
||||
var Markdown = {
|
||||
markdown: function (content) {
|
||||
return md.render(content)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Markdown
|
||||
@@ -1,42 +0,0 @@
|
||||
var React = require('react/addons')
|
||||
var ModalBase = React.createClass({
|
||||
getInitialState: function () {
|
||||
return {
|
||||
component: null,
|
||||
componentProps: {},
|
||||
isHidden: true
|
||||
}
|
||||
},
|
||||
close: function () {
|
||||
this.setState({component: null, componentProps: null, isHidden: true})
|
||||
},
|
||||
render: function () {
|
||||
var componentProps = this.state.componentProps
|
||||
return (
|
||||
<div className={'ModalBase' + (this.state.isHidden ? ' hide' : '')}>
|
||||
<div onClick={this.close} className='modalBack'/>
|
||||
{this.state.component == null ? null : (
|
||||
<this.state.component {...componentProps} close={this.close}/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
var modalBase = null
|
||||
|
||||
module.exports = {
|
||||
componentDidMount: function () {
|
||||
if (modalBase == null) {
|
||||
var el = document.createElement('div')
|
||||
document.body.appendChild(el)
|
||||
modalBase = React.render(<ModalBase/>, el)
|
||||
}
|
||||
},
|
||||
openModal: function (component, props) {
|
||||
modalBase.setState({component: component, componentProps: props, isHidden: false})
|
||||
},
|
||||
closeModal: function () {
|
||||
modalBase.setState({isHidden: true})
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
/* global localStorage */
|
||||
|
||||
var request = require('superagent-promise')(require('superagent'), Promise)
|
||||
var apiUrl = require('../../../config').apiUrl
|
||||
|
||||
module.exports = {
|
||||
// Auth
|
||||
login: function (input) {
|
||||
return request
|
||||
.post(apiUrl + 'auth')
|
||||
.send(input)
|
||||
},
|
||||
signup: function (input) {
|
||||
return request
|
||||
.post(apiUrl + 'auth/signup')
|
||||
.send(input)
|
||||
},
|
||||
getUser: function () {
|
||||
return request
|
||||
.get(apiUrl + 'auth/user')
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
},
|
||||
changePassword: function (input) {
|
||||
return request
|
||||
.post(apiUrl + 'auth/password')
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
.send(input)
|
||||
},
|
||||
|
||||
// Resources
|
||||
fetchUser: function (userName) {
|
||||
return request
|
||||
.get(apiUrl + 'resources/' + userName)
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
},
|
||||
updateUser: function (userName, input) {
|
||||
return request
|
||||
.put(apiUrl + 'resources/' + userName)
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
.send(input)
|
||||
},
|
||||
createTeam: function (userName, input) {
|
||||
return request
|
||||
.post(apiUrl + 'resources/' + userName + '/teams')
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
.send(input)
|
||||
},
|
||||
addMember: function (userName, input) {
|
||||
return request
|
||||
.post(apiUrl + 'resources/' + userName + '/members')
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
.send(input)
|
||||
},
|
||||
removeMember: function (userName, input) {
|
||||
return request
|
||||
.del(apiUrl + 'resources/' + userName + '/members')
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
.send(input)
|
||||
},
|
||||
createPlanet: function (userName, input) {
|
||||
return request
|
||||
.post(apiUrl + 'resources/' + userName + '/planets')
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
.send(input)
|
||||
},
|
||||
fetchPlanet: function (userName, planetName) {
|
||||
return request
|
||||
.get(apiUrl + 'resources/' + userName + '/planets/' + planetName)
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
},
|
||||
updatePlanet: function (userName, planetName, input) {
|
||||
return request
|
||||
.put(apiUrl + 'resources/' + userName + '/planets/' + planetName)
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
.send(input)
|
||||
},
|
||||
destroyPlanet: function (userName, planetName) {
|
||||
return request
|
||||
.del(apiUrl + 'resources/' + userName + '/planets/' + planetName)
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
},
|
||||
createCode: function (userName, planetName, input) {
|
||||
return request
|
||||
.post(apiUrl + 'resources/' + userName + '/planets/' + planetName + '/codes')
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
.send(input)
|
||||
},
|
||||
updateCode: function (userName, planetName, localId, input) {
|
||||
return request
|
||||
.put(apiUrl + 'resources/' + userName + '/planets/' + planetName + '/codes/' + localId)
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
.send(input)
|
||||
},
|
||||
destroyCode: function (userName, planetName, localId) {
|
||||
return request
|
||||
.del(apiUrl + 'resources/' + userName + '/planets/' + planetName + '/codes/' + localId)
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
},
|
||||
createNote: function (userName, planetName, input) {
|
||||
return request
|
||||
.post(apiUrl + 'resources/' + userName + '/planets/' + planetName + '/notes')
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
.send(input)
|
||||
},
|
||||
updateNote: function (userName, planetName, localId, input) {
|
||||
return request
|
||||
.put(apiUrl + 'resources/' + userName + '/planets/' + planetName + '/notes/' + localId)
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
.send(input)
|
||||
},
|
||||
destroyNote: function (userName, planetName, localId) {
|
||||
return request
|
||||
.del(apiUrl + 'resources/' + userName + '/planets/' + planetName + '/notes/' + localId)
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
},
|
||||
|
||||
// Search
|
||||
searchTag: function (tagName) {
|
||||
return request
|
||||
.get(apiUrl + 'search/tags')
|
||||
.query({name: tagName})
|
||||
},
|
||||
searchUser: function (userName) {
|
||||
return request
|
||||
.get(apiUrl + 'search/users')
|
||||
.query({name: userName})
|
||||
},
|
||||
|
||||
// Mail
|
||||
sendEmail: function (input) {
|
||||
return request
|
||||
.post(apiUrl + 'mail')
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
.send(input)
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
/* global localStorage */
|
||||
var Reflux = require('reflux')
|
||||
var request = require('superagent')
|
||||
|
||||
var apiUrl = require('../../../config').apiUrl
|
||||
|
||||
var AuthStore = Reflux.createStore({
|
||||
init: function () {
|
||||
},
|
||||
// Reflux Store
|
||||
login: function (input) {
|
||||
request
|
||||
.post(apiUrl + 'auth/login')
|
||||
.send(input)
|
||||
.set('Accept', 'application/json')
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
this.trigger({
|
||||
status: 'failedToLogIn',
|
||||
data: res
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var user = res.body.user
|
||||
localStorage.setItem('token', res.body.token)
|
||||
localStorage.setItem('user', JSON.stringify(res.body.user))
|
||||
|
||||
this.trigger({
|
||||
status: 'loggedIn',
|
||||
data: user
|
||||
})
|
||||
}.bind(this))
|
||||
},
|
||||
register: function (input) {
|
||||
request
|
||||
.post(apiUrl + 'auth/signup')
|
||||
.send(input)
|
||||
.set('Accept', 'application/json')
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
console.error(res)
|
||||
this.trigger({
|
||||
status: 'failedToRegister',
|
||||
data: res
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var user = res.body.user
|
||||
localStorage.setItem('token', res.body.token)
|
||||
localStorage.setItem('user', JSON.stringify(res.body.user))
|
||||
|
||||
this.trigger({
|
||||
status: 'registered',
|
||||
data: user
|
||||
})
|
||||
}.bind(this))
|
||||
},
|
||||
refreshUser: function () {
|
||||
request
|
||||
.get(apiUrl + 'auth/user')
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
AuthActions.logout()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var user = res.body
|
||||
localStorage.setItem('user', JSON.stringify(user))
|
||||
|
||||
this.trigger({
|
||||
status: 'userRefreshed',
|
||||
data: user
|
||||
})
|
||||
}.bind(this))
|
||||
},
|
||||
logout: function () {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('currentUser')
|
||||
|
||||
this.trigger({
|
||||
status: 'loggedOut'
|
||||
})
|
||||
},
|
||||
updateProfile: function (input) {
|
||||
request
|
||||
.put(apiUrl + 'auth/user')
|
||||
.set({
|
||||
Authorization: 'Bearer ' + localStorage.getItem('token')
|
||||
})
|
||||
.send(input)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
this.trigger({
|
||||
status: 'userProfileUpdatingFailed',
|
||||
data: err
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var user = res.body
|
||||
localStorage.setItem('user', JSON.stringify(user))
|
||||
|
||||
this.trigger({
|
||||
status: 'userProfileUpdated',
|
||||
data: user
|
||||
})
|
||||
}.bind(this))
|
||||
},
|
||||
// Methods
|
||||
check: function () {
|
||||
if (localStorage.getItem('token')) return true
|
||||
return false
|
||||
},
|
||||
getUser: function () {
|
||||
var userJSON = localStorage.getItem('currentUser')
|
||||
if (userJSON == null) return null
|
||||
return JSON.parse(userJSON)
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = AuthStore
|
||||
@@ -1,167 +0,0 @@
|
||||
/* global localStorage */
|
||||
|
||||
var Reflux = require('reflux')
|
||||
|
||||
var UserStore = require('./UserStore')
|
||||
|
||||
var Helper = require('../Mixins/Helper')
|
||||
|
||||
var actions = Reflux.createActions([
|
||||
'update',
|
||||
'destroy',
|
||||
'updateCode',
|
||||
'destroyCode',
|
||||
'updateNote',
|
||||
'destroyNote'
|
||||
])
|
||||
|
||||
module.exports = Reflux.createStore({
|
||||
mixins: [Helper],
|
||||
listenables: [actions],
|
||||
Actions: actions,
|
||||
onUpdate: function (planet) {
|
||||
// Copy the planet object
|
||||
var aPlanet = Object.assign({}, planet)
|
||||
delete aPlanet.Codes
|
||||
delete aPlanet.Notes
|
||||
|
||||
// Check if the planet should be updated to currentUser
|
||||
var currentUser = JSON.parse(localStorage.getItem('currentUser'))
|
||||
|
||||
var ownedByCurrentUser = currentUser.id === aPlanet.OwnerId
|
||||
|
||||
if (ownedByCurrentUser) {
|
||||
currentUser.Planets = this.updateItemToTargetArray(aPlanet, currentUser.Planets)
|
||||
}
|
||||
|
||||
if (!ownedByCurrentUser) {
|
||||
var team = null
|
||||
currentUser.Teams.some(function (_team) {
|
||||
if (_team.id === aPlanet.OwnerId) {
|
||||
team = _team
|
||||
return true
|
||||
}
|
||||
return
|
||||
})
|
||||
|
||||
if (team) {
|
||||
team.Planets = this.updateItemToTargetArray(aPlanet, team.Planets)
|
||||
}
|
||||
}
|
||||
|
||||
// Update currentUser
|
||||
localStorage.setItem('currentUser', JSON.stringify(currentUser))
|
||||
UserStore.Actions.update(currentUser)
|
||||
|
||||
planet.Codes.forEach(function (code) {
|
||||
code.type = 'code'
|
||||
})
|
||||
|
||||
planet.Notes.forEach(function (note) {
|
||||
note.type = 'note'
|
||||
})
|
||||
|
||||
// Update the planet
|
||||
localStorage.setItem('planet-' + planet.id, JSON.stringify(planet))
|
||||
|
||||
this.trigger({
|
||||
status: 'updated',
|
||||
data: planet
|
||||
})
|
||||
},
|
||||
onDestroy: function (planet) {
|
||||
// Check if the planet should be updated to currentUser
|
||||
var currentUser = JSON.parse(localStorage.getItem('currentUser'))
|
||||
|
||||
var ownedByCurrentUser = currentUser.id === planet.OwnerId
|
||||
|
||||
if (ownedByCurrentUser) {
|
||||
currentUser.Planets = this.deleteItemFromTargetArray(planet, currentUser.Planets)
|
||||
}
|
||||
|
||||
if (!ownedByCurrentUser) {
|
||||
var team = null
|
||||
currentUser.Teams.some(function (_team) {
|
||||
if (_team.id === planet.OwnerId) {
|
||||
team = _team
|
||||
return true
|
||||
}
|
||||
return
|
||||
})
|
||||
|
||||
if (team) {
|
||||
team.Planets = this.deleteItemFromTargetArray(planet, team.Planets)
|
||||
}
|
||||
}
|
||||
|
||||
// Update currentUser
|
||||
localStorage.setItem('currentUser', JSON.stringify(currentUser))
|
||||
UserStore.Actions.update(currentUser)
|
||||
|
||||
// Update the planet
|
||||
localStorage.setItem('planet-' + planet.id, JSON.stringify(planet))
|
||||
|
||||
this.trigger({
|
||||
status: 'destroyed',
|
||||
data: planet
|
||||
})
|
||||
},
|
||||
onUpdateCode: function (code) {
|
||||
code.type = 'code'
|
||||
|
||||
var planet = JSON.parse(localStorage.getItem('planet-' + code.PlanetId))
|
||||
if (planet != null) {
|
||||
planet.Codes = this.updateItemToTargetArray(code, planet.Codes)
|
||||
|
||||
localStorage.setItem('planet-' + code.PlanetId, JSON.stringify(planet))
|
||||
}
|
||||
|
||||
this.trigger({
|
||||
status: 'codeUpdated',
|
||||
data: code
|
||||
})
|
||||
},
|
||||
onDestroyCode: function (code) {
|
||||
var planet = JSON.parse(localStorage.getItem('planet-' + code.PlanetId))
|
||||
if (planet != null) {
|
||||
planet.Codes = this.deleteItemFromTargetArray(code, planet.Codes)
|
||||
|
||||
localStorage.setItem('planet-' + code.PlanetId, JSON.stringify(planet))
|
||||
}
|
||||
code.type = 'code'
|
||||
|
||||
this.trigger({
|
||||
status: 'codeDestroyed',
|
||||
data: code
|
||||
})
|
||||
},
|
||||
onUpdateNote: function (note) {
|
||||
note.type = 'note'
|
||||
|
||||
var planet = JSON.parse(localStorage.getItem('planet-' + note.PlanetId))
|
||||
if (planet != null) {
|
||||
planet.Notes = this.updateItemToTargetArray(note, planet.Notes)
|
||||
|
||||
localStorage.setItem('planet-' + note.PlanetId, JSON.stringify(planet))
|
||||
}
|
||||
|
||||
this.trigger({
|
||||
status: 'noteUpdated',
|
||||
data: note
|
||||
})
|
||||
},
|
||||
onDestroyNote: function (note) {
|
||||
var planet = JSON.parse(localStorage.getItem('planet-' + note.PlanetId))
|
||||
if (planet != null) {
|
||||
planet.Notes = this.deleteItemFromTargetArray(note, planet.Notes)
|
||||
|
||||
localStorage.setItem('planet-' + note.PlanetId, JSON.stringify(planet))
|
||||
}
|
||||
note.type = 'note'
|
||||
|
||||
this.trigger({
|
||||
status: 'noteDestroyed',
|
||||
data: note
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,69 +0,0 @@
|
||||
/* global localStorage */
|
||||
|
||||
var Reflux = require('reflux')
|
||||
|
||||
var actions = Reflux.createActions([
|
||||
'update',
|
||||
'destroy',
|
||||
'addMember',
|
||||
'removeMember'
|
||||
])
|
||||
|
||||
module.exports = Reflux.createStore({
|
||||
listenables: [actions],
|
||||
onUpdate: function (user) {
|
||||
var currentUser = JSON.parse(localStorage.getItem('currentUser'))
|
||||
|
||||
if (currentUser.id === user.id) {
|
||||
localStorage.setItem('currentUser', JSON.stringify(user))
|
||||
}
|
||||
|
||||
if (user.userType === 'team') {
|
||||
var isMyTeam = user.Members.some(function (member) {
|
||||
if (currentUser.id === member.id) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if (isMyTeam) {
|
||||
var isNew = !currentUser.Teams.some(function (team, index) {
|
||||
if (user.id === team.id) {
|
||||
currentUser.Teams.splice(index, 1, user)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if (isNew) {
|
||||
currentUser.Teams.push(user)
|
||||
}
|
||||
localStorage.setItem('currentUser', JSON.stringify(currentUser))
|
||||
}
|
||||
}
|
||||
|
||||
this.trigger({
|
||||
status: 'userUpdated',
|
||||
data: user
|
||||
})
|
||||
},
|
||||
onDestroy: function (user) {
|
||||
this.trigger({
|
||||
status: 'userDestroyed',
|
||||
data: user
|
||||
})
|
||||
},
|
||||
onAddMember: function (member) {
|
||||
this.trigger({
|
||||
status: 'memberAdded',
|
||||
data: member
|
||||
})
|
||||
},
|
||||
onRemoveMember: function (member) {
|
||||
this.trigger({
|
||||
status: 'memberRemoved',
|
||||
data: member
|
||||
})
|
||||
},
|
||||
Actions: actions
|
||||
})
|
||||
178
browser/main/actions.js
Normal file
178
browser/main/actions.js
Normal file
@@ -0,0 +1,178 @@
|
||||
// Action types
|
||||
export const USER_UPDATE = 'USER_UPDATE'
|
||||
|
||||
export const ARTICLE_UPDATE = 'ARTICLE_UPDATE'
|
||||
export const ARTICLE_DESTROY = 'ARTICLE_DESTROY'
|
||||
export const ARTICLE_SAVE = 'ARTICLE_SAVE'
|
||||
export const ARTICLE_SAVE_ALL = 'ARTICLE_SAVE_ALL'
|
||||
export const ARTICLE_CACHE = 'ARTICLE_CACHE'
|
||||
export const ARTICLE_UNCACHE = 'ARTICLE_UNCACHE'
|
||||
export const ARTICLE_UNCACHE_ALL = 'ARTICLE_UNCACHE_ALL'
|
||||
|
||||
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_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 TOGGLE_TUTORIAL = 'TOGGLE_TUTORIAL'
|
||||
|
||||
// Article status
|
||||
export const NEW = 'NEW'
|
||||
|
||||
export function updateUser (input) {
|
||||
return {
|
||||
type: USER_UPDATE,
|
||||
data: input
|
||||
}
|
||||
}
|
||||
|
||||
// DB
|
||||
export function cacheArticle (key, article) {
|
||||
return {
|
||||
type: ARTICLE_CACHE,
|
||||
data: { key, article }
|
||||
}
|
||||
}
|
||||
|
||||
export function uncacheArticle (key) {
|
||||
return {
|
||||
type: ARTICLE_UNCACHE,
|
||||
data: { key }
|
||||
}
|
||||
}
|
||||
|
||||
export function uncacheAllArticles () {
|
||||
return {
|
||||
type: ARTICLE_UNCACHE_ALL
|
||||
}
|
||||
}
|
||||
|
||||
export function saveArticle (key, article, forceSwitch) {
|
||||
return {
|
||||
type: ARTICLE_SAVE,
|
||||
data: { key, article, forceSwitch }
|
||||
}
|
||||
}
|
||||
|
||||
export function saveAllArticles () {
|
||||
return {
|
||||
type: ARTICLE_SAVE_ALL
|
||||
}
|
||||
}
|
||||
|
||||
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 switchArticle (articleKey) {
|
||||
return {
|
||||
type: SWITCH_ARTICLE,
|
||||
data: {
|
||||
key: articleKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 toggleTutorial () {
|
||||
return {
|
||||
type: TOGGLE_TUTORIAL
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
updateUser,
|
||||
|
||||
updateArticle,
|
||||
destroyArticle,
|
||||
cacheArticle,
|
||||
uncacheArticle,
|
||||
uncacheAllArticles,
|
||||
saveArticle,
|
||||
saveAllArticles,
|
||||
|
||||
createFolder,
|
||||
updateFolder,
|
||||
destroyFolder,
|
||||
replaceFolder,
|
||||
|
||||
switchFolder,
|
||||
switchArticle,
|
||||
setSearchFilter,
|
||||
setTagFilter,
|
||||
clearSearch,
|
||||
toggleTutorial
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,99 +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" title="no title" charset="utf-8">
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
src: url('../../Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
|
||||
url('../../Lato-Regular.woff') format('woff'), /* Modern Browsers */
|
||||
url('../../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/favicon-230x230.png">
|
||||
<div class='message'>Loading...</div>
|
||||
</div>
|
||||
|
||||
<div id="content"></div>
|
||||
|
||||
<script>
|
||||
if (!Object.assign) {
|
||||
Object.defineProperty(Object, 'assign', {
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: function(target) {
|
||||
'use strict';
|
||||
if (target === undefined || target === null) {
|
||||
throw new TypeError('Cannot convert first argument to object');
|
||||
}
|
||||
|
||||
var to = Object(target);
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
var nextSource = arguments[i];
|
||||
if (nextSource === undefined || nextSource === null) {
|
||||
continue;
|
||||
}
|
||||
nextSource = Object(nextSource);
|
||||
|
||||
var keysArray = Object.keys(Object(nextSource));
|
||||
for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
|
||||
var nextKey = keysArray[nextIndex];
|
||||
var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
|
||||
if (desc !== undefined && desc.enumerable) {
|
||||
to[nextKey] = nextSource[nextKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
return to;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
<script src="../ace/src-min/ace.js"></script>
|
||||
<script>
|
||||
var version = require('remote').require('app').getVersion()
|
||||
global.version = version
|
||||
document.title = 'Boost ' + ((version == null || version.length === 0) ? 'DEV version' : 'v' + version)
|
||||
require('../electron-stylus')(__dirname + '/../styles/main/index.styl', 'mainCss')
|
||||
// require('../electron-stylus')(__dirname + '/../styles/main/index.styl')
|
||||
require('node-jsx').install({ harmony: true, extension: '.jsx' })
|
||||
require('./index.jsx')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user