1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-14 10:16:26 +00:00

Compare commits

...

102 Commits

Author SHA1 Message Date
Rokt33r
ee91daad7e Merge branch 'dev'
* dev:
  hotfix: Edited alertが変な時に出る
2015-11-18 18:51:39 +09:00
Rokt33r
ee78c0d33b hotfix: Edited alertが変な時に出る 2015-11-18 18:39:27 +09:00
Dick Choi
09482ebcf3 fix wrong dependency 2015-11-17 05:36:08 +09:00
Rokt33r
67424f2d3a Merge branch 'dev'
* dev:
  bump up version 0.4.1
  記事が編集された状態で他の記事を見ようとすると警告をだす
  updateが準備できたら、再起動されるまで改めてUpdateの確認をしない
  Tag suggest
  EnterでSubmitができる - Hotkey, folder edit, folder create(preference/create new folder modal両方)
  Folderの位置を変えることができる
  Preferenceからもフォルダーの色の選択ができる。
  // filterを使うと確実にFolder名が一致するもののみを表示する
  fix style
  new folder modalにcolor select追加
  auto update確認
  FinderからCopyした時、通知を出す
  FinderにCopy to clipboard button追加
  Folder リストに articleの数をだす
  フォルダーで検索するときに in:じゃなくて /にする +バグ修正
  IntroのFinder説明変更
2015-11-16 07:03:46 +09:00
Rokt33r
51f530ffbe bump up version 0.4.1 2015-11-16 05:36:37 +09:00
Rokt33r
013f96a754 記事が編集された状態で他の記事を見ようとすると警告をだす 2015-11-16 05:34:37 +09:00
Rokt33r
df6a018fb6 updateが準備できたら、再起動されるまで改めてUpdateの確認をしない 2015-11-16 04:17:56 +09:00
Rokt33r
409eaf54c1 Tag suggest 2015-11-16 04:06:14 +09:00
Rokt33r
7e04fd342c EnterでSubmitができる - Hotkey, folder edit, folder create(preference/create new folder modal両方) 2015-11-16 02:45:46 +09:00
Rokt33r
1fe15bc6a5 Folderの位置を変えることができる 2015-11-16 02:38:36 +09:00
Rokt33r
ff1bffbb55 Preferenceからもフォルダーの色の選択ができる。 2015-11-16 01:22:22 +09:00
Rokt33r
b28b18a19a // filterを使うと確実にFolder名が一致するもののみを表示する 2015-11-15 23:20:06 +09:00
Rokt33r
bbc3c85212 fix style 2015-11-15 20:40:43 +09:00
Rokt33r
26a08fac06 new folder modalにcolor select追加 2015-11-15 20:32:02 +09:00
Rokt33r
da9d7a4336 auto update確認 2015-11-15 03:52:34 +09:00
Rokt33r
46c6555f94 FinderからCopyした時、通知を出す 2015-11-15 01:39:40 +09:00
Rokt33r
3e980fd2d4 FinderにCopy to clipboard button追加 2015-11-15 01:22:56 +09:00
Rokt33r
fb1462f669 Folder リストに articleの数をだす 2015-11-15 01:07:46 +09:00
Rokt33r
41e1630aac フォルダーで検索するときに in:じゃなくて /にする +バグ修正 2015-11-15 00:57:29 +09:00
Rokt33r
ef84c4e3da IntroのFinder説明変更 2015-11-14 23:49:49 +09:00
Rokt33r
61cc44cc83 Merge branch 'dev'
* dev:
  0.4.0-beta.2
  tutorial 追加
  ModeSelect
  行動データ, contact form, default articleに英語文追加, Intro fix
  Hotkey settingの時Alertで結果を出す Folder nameが長すぎ雨時のlayout崩れ解決 ArticleNavigatorの余計なスペースをなくす Default articleの誤字直し
  Cmd+Sで保存 debug craetedAt
  fix Title overflow behaviour
  0.4.0-beta.1

Conflicts:
	package.json
2015-11-11 03:09:28 +09:00
Rokt33r
c20cbe7d66 0.4.0-beta.2 2015-11-11 03:01:07 +09:00
Rokt33r
2f4af3223b tutorial 追加 2015-11-11 00:59:14 +09:00
Rokt33r
e4b2c42897 ModeSelect 2015-11-10 04:26:11 +09:00
Rokt33r
746df9277c 行動データ, contact form, default articleに英語文追加, Intro fix 2015-11-09 15:07:17 +09:00
Rokt33r
8428588a4c Hotkey settingの時Alertで結果を出す
Folder nameが長すぎ雨時のlayout崩れ解決
ArticleNavigatorの余計なスペースをなくす
Default articleの誤字直し
2015-11-06 23:45:00 +09:00
Rokt33r
83a8f4b911 Cmd+Sで保存
debug craetedAt
2015-11-06 21:46:29 +09:00
Rokt33r
2736024cb7 fix Title overflow behaviour 2015-11-05 14:47:53 +09:00
Rokt33r
9a32ca893e 0.4.0-beta.1 2015-11-05 10:36:14 +09:00
Rokt33r
59d3c6c94f 0.4.0-beta.1 2015-11-05 10:33:14 +09:00
Rokt33r
388027b731 Merge branch 'dev'
* dev:
  beta - add error alert(folder editing) - debug clear button of search input
2015-11-05 09:50:39 +09:00
Rokt33r
8abdedc11d beta
- add error alert(folder editing)
- debug clear button of search input
2015-11-05 09:50:07 +09:00
Rokt33r
9758f5baa8 release version 0.4.0-alpha.7 2015-11-02 16:13:31 +09:00
Rokt33r
248262a597 Merge branch 'dev'
* dev: (43 commits)
  webpack bugfix, tooltip modified, preview button string changed(toggle Preview -> Preview / Edit)
  tooltip, tutorial追加
  ArticleListに自動scroll機能追加
  Tooltip追加、キー反応改善、Pinch2Zoom使用禁止、Webpack config debug
  編集状態でのMarkdown preview追加
  Key入力の動き改善 - Searchに内容がある時にEscを押すと内容をSearchの内容を削除する - Cmd + Fを押すとSearch inputがfocusされる
  Key入力追加
  prepare alpha.5 (remain work: MD preview, keybind)
  Going LIte
  add Team destroy
  add text:ellipsis to some labels & add member deletion confirm
  bump alpha.3
  add folder create
  add Member setting, Team setting, FolderSetting(80%) bump react-select
  bumpup alpha.2
  addTag search
  add Invalid token handler
  bumpup react (0.13.3->0.14.0), reacthmr work perfectly
  fix updater bug
  Folder indexing
  ...
2015-11-02 16:12:08 +09:00
Rokt33r
cc0f2c7c7f webpack bugfix, tooltip modified, preview button string changed(toggle Preview -> Preview / Edit) 2015-11-01 23:15:22 +09:00
Rokt33r
72f6468d12 tooltip, tutorial追加 2015-11-01 21:59:59 +09:00
Rokt33r
522c0edd90 ArticleListに自動scroll機能追加 2015-11-01 02:37:22 +09:00
Rokt33r
d338f217fe Tooltip追加、キー反応改善、Pinch2Zoom使用禁止、Webpack config debug 2015-11-01 02:24:17 +09:00
Rokt33r
ca79857386 編集状態でのMarkdown preview追加 2015-10-31 19:06:14 +09:00
Rokt33r
60e551e273 Key入力の動き改善
- Searchに内容がある時にEscを押すと内容をSearchの内容を削除する
- Cmd + Fを押すとSearch inputがfocusされる
2015-10-31 18:29:45 +09:00
Rokt33r
954e148be3 Key入力追加 2015-10-31 18:21:42 +09:00
Rokt33r
3d0b79f674 prepare alpha.5 (remain work: MD preview, keybind) 2015-10-31 13:05:22 +09:00
Rokt33r
d9442aa23c Going LIte 2015-10-30 14:53:09 +09:00
Rokt33r
ba0daf4452 add Team destroy 2015-10-26 13:56:43 +09:00
Rokt33r
8d9cd5bbd1 add text:ellipsis to some labels & add member deletion confirm 2015-10-26 13:01:04 +09:00
Rokt33r
186b877c09 bump alpha.3 2015-10-24 21:37:08 +09:00
Rokt33r
5ed2dfccd1 add folder create 2015-10-24 21:32:53 +09:00
Rokt33r
911cfd8642 add Member setting, Team setting, FolderSetting(80%)
bump react-select
2015-10-24 20:52:10 +09:00
Rokt33r
3539bd1e79 bumpup alpha.2 2015-10-22 08:34:09 +09:00
Rokt33r
f56df7c16d addTag search 2015-10-22 08:30:39 +09:00
Rokt33r
c507dfa6c4 add Invalid token handler 2015-10-21 09:25:42 +09:00
Rokt33r
f6d2e898dc bumpup react (0.13.3->0.14.0), reacthmr work perfectly 2015-10-21 02:47:21 +09:00
Rokt33r
326c7a93fb fix updater bug 2015-10-20 01:51:51 +09:00
Rokt33r
58381b8062 Folder indexing 2015-10-20 00:55:18 +09:00
Rokt33r
0899cea4b4 fix build env(redux devtools removed) 2015-10-19 22:10:22 +09:00
Rokt33r
7459e937b5 fix build env 2015-10-19 11:46:58 +09:00
Rokt33r
55db0bebbb exclude finder 2015-10-18 20:55:42 +09:00
Rokt33r
0bdb8142c6 USER auto refresh 2015-10-18 20:18:16 +09:00
Rokt33r
88ee94d4b6 add Preferences modal(30%done) & move some modules (actions, reducer, socket, store -> lib/~) 2015-10-18 17:17:25 +09:00
Rokt33r
1df4ed0fe9 sort by time & add FolderMark 2015-10-17 16:46:10 +09:00
Rokt33r
2a339a2935 article CRUD with socket 2015-10-16 22:12:49 +09:00
Rokt33r
a1810e6023 new folder modal / key indexing for article / login bugfix 2015-10-16 13:32:58 +09:00
Rokt33r
832ca3347c CRUD done 2015-10-15 10:46:22 +09:00
Rokt33r
9d2b64e82b CREATE_MODE(1/2) 2015-10-14 15:20:16 +09:00
Rokt33r
9a5e4b3f54 restructure DONE 2015-10-13 18:07:33 +09:00
Rokt33r
e5e8032ba1 revive articledetail 2015-10-13 16:09:37 +09:00
Rokt33r
5356e68b51 set app status 2015-10-13 02:49:59 +09:00
Rokt33r
cd94c625a7 set style to articlenavigator 2015-10-13 01:28:16 +09:00
Rokt33r
972a3746a1 on making usernavigator 2015-10-12 22:29:24 +09:00
Rokt33r
a9e12e4384 usernavigator done 2015-10-12 15:52:54 +09:00
Rokt33r
1690e6420f set design create team modal 2015-10-11 18:11:08 +09:00
Rokt33r
acdf61f7ab add redirect home -> user(current user), enhance UserNavigation style 2015-10-09 20:45:06 +09:00
Rokt33r
2e4fc557ea use webpack & add some styles 2015-10-09 20:12:01 +09:00
Rokt33r
979dcead49 before applying redux 2015-10-08 20:40:19 +09:00
Rokt33r
116ddf345d remove all submodules 2015-09-23 01:25:42 +09:00
Rokt33r
366805a64f remove submodule 2015-09-22 20:59:30 +09:00
Rokt33r
1fee2a846a - cleanup root directory
- improve window behaviour
2015-09-11 17:20:16 +09:00
Rokt33r
3f54eb52b2 Merge branch 'dev'
* dev:
  リアルタイム(SocketIO)実装 / Markdown style改善

Conflicts:
	package.json
2015-09-08 15:30:02 +09:00
Rokt33r
a3847ce1c9 リアルタイム(SocketIO)実装 / Markdown style改善 2015-09-08 15:28:36 +09:00
Rokt33r
1e7415b692 fix version 0.2.11 2015-09-06 19:48:44 +09:00
Rokt33r
ff950ef28a Merge branch 'dev'
* dev:
  improve markdown style
2015-09-06 12:19:20 +09:00
Rokt33r
51bd12c6cf improve markdown style 2015-09-06 12:18:54 +09:00
Rokt33r
5fa37dbffb Merge branch 'dev'
* dev:
  version 0.2.10 - Hotkeyの設定機能 - Stylus refactor
2015-09-02 01:02:32 +09:00
Rokt33r
c6307e4ad3 version 0.2.10
- Hotkeyの設定機能
- Stylus refactor
2015-09-02 01:02:04 +09:00
Rokt33r
06a54d451c Merge branch 'dev'
* dev:
  0.2.9 API server changed, bump electron version 0.31.0
2015-08-31 01:34:15 +09:00
Rokt33r
e317075815 0.2.9 API server changed, bump electron version 0.31.0 2015-08-31 01:33:32 +09:00
Rokt33r
45541a255b Merge branch 'dev'
* dev:
  - StylusでコンパイルされたCSSをCachingする(ロディングが短くなる) - Planet name changeのときにエラーハンドリング追加 + Bug fix - TeamのMemberを編集する場合、自分を編集することはできない - FinderにMarkdownのリンクがちゃんと外部に飛ぶように - Tray iconがちゃんと表示 - ArticleDetailのCodeアイコンがちゃんと表示されない
2015-08-30 05:31:13 +09:00
Rokt33r
3ab423d695 - StylusでコンパイルされたCSSをCachingする(ロディングが短くなる)
- Planet name changeのときにエラーハンドリング追加 + Bug fix
- TeamのMemberを編集する場合、自分を編集することはできない
- FinderにMarkdownのリンクがちゃんと外部に飛ぶように
- Tray iconがちゃんと表示
- ArticleDetailのCodeアイコンがちゃんと表示されない
2015-08-30 05:30:54 +09:00
Rokt33r
345d7b427a Merge branch 'dev'
* dev:
  Loading font 微調整
2015-08-26 18:27:42 +09:00
Rokt33r
de6d6b692e Loading font 微調整 2015-08-26 18:27:19 +09:00
Rokt33r
b2845e2284 Merge branch 'dev'
* dev:
  version 0.2.7  - Planet, Team作成の時Error message表示  - MarkdownのCode blockの背景を薄い灰色にする  - 権限なしのPlanetには  - SignUpに規約/Privacyの外部リンク追加  - Loading画面追加  - Font 添付(Lato regular)  - UserContainerでのTeam Label変更
2015-08-26 18:23:04 +09:00
Rokt33r
47383c347c version 0.2.7
- Planet, Team作成の時Error message表示
 - MarkdownのCode blockの背景を薄い灰色にする
 - 権限なしのPlanetには
 - SignUpに規約/Privacyの外部リンク追加
 - Loading画面追加
 - Font 添付(Lato regular)
 - UserContainerでのTeam Label変更
2015-08-26 18:22:46 +09:00
Rokt33r
4bda84d69c cleanup old files 2015-08-24 17:42:43 +09:00
Rokt33r
b510aa11f5 Merge branch 'dev'
* dev:
  fix bugs - ArticleList text overflow behaviour in Finder, PlanetContainer - No DevTools
2015-08-24 17:42:00 +09:00
Rokt33r
8dab6d5e04 fix bugs
- ArticleList text overflow behaviour in Finder, PlanetContainer
- No DevTools
2015-08-24 17:41:38 +09:00
Rokt33r
85f833c865 Merge branch 'dev'
* dev:
  v0.2.5 - bugfix - Alert message added(Private planet/Team member)

Conflicts:
	package.json
2015-08-24 13:46:32 +09:00
Rokt33r
15133d00c7 v0.2.5
- bugfix
- Alert message added(Private planet/Team member)
2015-08-24 13:45:28 +09:00
Rokt33r
b93990d10b bump version 2015-08-24 06:23:41 +09:00
Rokt33r
a0bcb8edbe Merge branch 'dev'
* dev:
  実装 - MemberがOwnerではないときにTeam設定ボターンを全部隠す
  v0.2.4 - Minor fix
  fix minor bug
2015-08-24 06:22:32 +09:00
Rokt33r
bfdf691bed 実装 - MemberがOwnerではないときにTeam設定ボターンを全部隠す 2015-08-24 06:22:16 +09:00
Rokt33r
f60856b998 v0.2.4 - Minor fix 2015-08-24 06:14:13 +09:00
Rokt33r
3308eeaf82 fix minor bug 2015-08-23 04:00:18 +09:00
175 changed files with 9190 additions and 7240 deletions

20
.babelrc Normal file
View File

@@ -0,0 +1,20 @@
{
"stage": 0,
"env": {
"development": {
"plugins": ["react-transform"],
"extra": {
"react-transform": {
"transforms": [{
"transform": "react-transform-hmr",
"imports": ["react"],
"locals": ["module"]
}, {
"transform": "react-transform-catch-errors",
"imports": ["react", "redbox-react"]
}]
}
}
}
}
}

View File

@@ -1,3 +0,0 @@
{
"directory": "browser/vendor/"
}

10
.gitignore vendored
View File

@@ -1,6 +1,6 @@
build/
node_modules/
electron_build/
.env .env
dist/ node_modules/*
vendor/ !node_modules/boost
Boost-darwin-x64/
backup/
compiled

5
.gitmodules vendored
View File

@@ -1,3 +1,4 @@
[submodule "browser/ace"] [submodule "submodules/ace"]
path = browser/ace path = submodules/ace
url = https://github.com/ajaxorg/ace-builds.git url = https://github.com/ajaxorg/ace-builds.git
branch = master

29
atom-lib/finder-window.js Normal file
View File

@@ -0,0 +1,29 @@
var BrowserWindow = require('browser-window')
var path = require('path')
var finderWindow = new BrowserWindow({
width: 640,
height: 400,
show: false,
frame: false,
resizable: false,
'zoom-factor': 1.0,
'always-on-top': true,
'web-preferences': {
'overlay-scrollbars': true,
'skip-taskbar': true
},
'standard-window': false
})
var url = path.resolve(__dirname, '../browser/finder/index.html')
finderWindow.loadUrl('file://' + url)
finderWindow.on('blur', function () {
finderWindow.hide()
})
finderWindow.setVisibleOnAllWorkspaces(true)
module.exports = finderWindow

24
atom-lib/main-window.js Normal file
View File

@@ -0,0 +1,24 @@
var BrowserWindow = require('browser-window')
var path = require('path')
var mainWindow = new BrowserWindow({
width: 1080,
height: 720,
'zoom-factor': 1.0,
'web-preferences': {
'overlay-scrollbars': true
},
'standard-window': false
})
var url = path.resolve(__dirname, '../browser/main/index.html')
mainWindow.loadUrl('file://' + url)
mainWindow.setVisibleOnAllWorkspaces(true)
mainWindow.webContents.on('new-window', function (e) {
e.preventDefault()
})
module.exports = mainWindow

View File

@@ -5,7 +5,7 @@ module.exports = [
label: 'Electron', label: 'Electron',
submenu: [ submenu: [
{ {
label: 'About Electron', label: 'About Boost',
selector: 'orderFrontStandardAboutPanel:' selector: 'orderFrontStandardAboutPanel:'
}, },
{ {
@@ -19,7 +19,7 @@ module.exports = [
type: 'separator' type: 'separator'
}, },
{ {
label: 'Hide Electron', label: 'Hide Boost',
accelerator: 'Command+H', accelerator: 'Command+H',
selector: 'hide:' selector: 'hide:'
}, },

42
atom-lib/updater.js Normal file
View File

@@ -0,0 +1,42 @@
var autoUpdater = require('auto-updater')
var nn = require('node-notifier')
var app = require('app')
var path = require('path')
var version = app.getVersion()
var versionText = (version == null || version.length === 0) ? 'DEV version' : 'v' + version
var versionNotified = false
autoUpdater
.on('error', function (err, message) {
console.error(err)
console.error(message)
console.log(path.resolve(__dirname, '../resources/favicon-230x230.png'))
nn.notify({
title: 'Error! ' + versionText,
icon: path.resolve(__dirname, '../resources/favicon-230x230.png'),
message: message
})
})
// .on('checking-for-update', function () {
// // Connecting
// console.log('checking...')
// })
.on('update-available', function () {
nn.notify({
title: 'Update is available!! ' + versionText,
icon: path.resolve(__dirname, '../resources/favicon-230x230.png'),
message: 'Download started.. wait for the update ready.'
})
})
.on('update-not-available', function () {
if (!versionNotified) {
nn.notify({
title: 'Latest Build!! ' + versionText,
icon: path.resolve(__dirname, '../resources/favicon-230x230.png'),
message: 'Hope you to enjoy our app :D'
})
versionNotified = true
}
})
module.exports = autoUpdater

View File

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

View File

@@ -1,43 +0,0 @@
var React = require('react/addons')
var CodeViewer = require('../../main/Components/CodeViewer')
var Markdown = require('../../main/Mixins/Markdown')
module.exports = React.createClass({
mixins: [Markdown],
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'>
<div className='marked' dangerouslySetInnerHTML={{__html: ' ' + this.markdown(article.content)}}></div>
</div>
</div>
)
}
}
return (
<div className='FinderDetail'>
<div className='nothing'>Nothing selected</div>
</div>
)
}
})

View File

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

View File

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

View File

@@ -0,0 +1,44 @@
import React, { PropTypes } from 'react'
import CodeEditor from 'boost/components/CodeEditor'
import MarkdownPreview from 'boost/components/MarkdownPreview'
import ModeIcon from 'boost/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 mode={activeArticle.mode} code={activeArticle.content}/>
}
</div>
</div>
)
}
return (
<div className='FinderDetail'>
<div className='nothing'>Nothing selected</div>
</div>
)
}
}
FinderDetail.propTypes = {
activeArticle: PropTypes.shape(),
saveToClipboard: PropTypes.func
}

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

View File

@@ -0,0 +1,71 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ModeIcon from 'boost/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
}

33
browser/finder/actions.js Normal file
View File

@@ -0,0 +1,33 @@
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 () {
console.log('refreshing data')
let data = JSON.parse(localStorage.getItem('local'))
if (data == null) return null
let { folders, articles } = data
return {
type: REFRESH_DATA,
data: {
articles,
folders
}
}
}

View File

@@ -1,62 +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">
<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;
}
});
}
require('electron-stylus')(__dirname + '/../styles/finder/index.styl')
</script>
</head>
<body>
<div id="content"></div>
<script src="../ace/src-min/ace.js"></script>
<script>
require('node-jsx').install({ harmony: true, extension: '.jsx' })
require('./index.jsx')
</script>
</body>
</html>

View File

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

228
browser/finder/index.js Normal file
View File

@@ -0,0 +1,228 @@
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 { selectArticle, searchArticle, refreshData } from './actions'
import _ from 'lodash'
import activityRecord from 'boost/activityRecord'
import remote from 'remote'
var hideFinder = remote.getGlobal('hideFinder')
import clipboard from 'clipboard'
var notifier = require('node-notifier')
var path = require('path')
function getIconPath () {
return path.resolve(global.__dirname, '../../resources/favicon-230x230.png')
}
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 () {
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
}
handleClick (e) {
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
}
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()
}
}
saveToClipboard () {
let { activeArticle } = this.props
clipboard.writeText(activeArticle.content)
activityRecord.emit('FINDER_COPY')
notifier.notify({
icon: getIconPath(),
'title': 'Saved to Clipboard!',
'message': 'Paste it wherever you want!'
})
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 onClick={e => this.handleClick(e)} onKeyDown={e => this.handleKeyDown(e)} 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 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 => folder.name.match(new RegExp(`^${filter.value}$`)))
})
let fuzzyTargetFolders = folders.filter(folder => {
return _.find(folderFilters, filter => folder.name.match(new RegExp(`^${filter.value}`)))
})
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 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.match(new RegExp(tagFilter.value, 'i')))
})
}, articles)
}
}
let activeArticle = _.findWhere(articles, {key: status.articleKey})
if (activeArticle == null) activeArticle = articles[0]
console.log(status.search)
return {
articles,
activeArticle,
status
}
}
var Finder = connect(remap)(FinderMain)
var store = createStore(reducer)
window.onfocus = e => {
store.dispatch(refreshData())
activityRecord.emit('FINDER_OPEN')
}
ReactDOM.render((
<Provider store={store}>
<Finder/>
</Provider>
), document.getElementById('content'))

View File

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

49
browser/finder/reducer.js Normal file
View File

@@ -0,0 +1,49 @@
import { combineReducers } from 'redux'
import { SELECT_ARTICLE, SEARCH_ARTICLE, REFRESH_DATA } from './actions'
let data = JSON.parse(localStorage.getItem('local'))
let initialArticles = data != null ? data.articles : []
let initialFolders = data != null ? data.folders : []
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
})

View File

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

View File

@@ -1,40 +0,0 @@
var remote = require('remote')
var version = remote.getGlobal('version')
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 () {
return (
<div className='AboutModal 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>
)
}
})

View File

@@ -1,95 +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 () {
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)
})
},
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>
<button onClick={this.handleSubmit} className='submitButton'><i className='fa fa-check'/></button>
</div>
)
}
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,171 +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 () {
console.log(this.state.password)
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 modal tabModal'>
<div className='leftPane'>
<div className='tabLabel'>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'>
<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'>
<div className='formField'>
<label>Current password</label>
<input valueLink={this.linkState('password.currentPassword')}/>
</div>
<div className='formField'>
<label>New password</label>
<input valueLink={this.linkState('password.newPassword')}/>
</div>
<div className='formField'>
<label>Confirmation</label>
<input 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>
)
}
})

View File

@@ -1,190 +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 AboutModal = require('./AboutModal')
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})
},
openAboutModal: function () {
this.openModal(AboutModal)
},
openPlanetCreateModal: function () {
this.openModal(PlanetCreateModal, {transitionTo: this.transitionTo})
},
handleKeyDown: function (e) {
if (this.state.currentUser == null) return
if (e.metaKey && e.keyCode > 48 && e.keyCode < 58) {
var planet = this.state.currentUser.Planets[e.keyCode - 49]
if (planet != null) {
this.transitionTo('planet', {userName: planet.userName, planetName: planet.name})
}
e.preventDefault()
}
},
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.openAboutModal}><i className='fa fa-info-circle fa-fw'/> About this app</button>
</li>
<li>
<button onClick={this.handleLogoutClick}><i className='fa fa-sign-out fa-fw'/> Log out</button>
</li>
</ul>
</div>
)
}
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.length > 50 ? article.description.substring(0, 50) + ' …' : 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>
)
}
})

View File

@@ -1,90 +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
}
},
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 () {
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)
})
},
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>
<button onClick={this.handleSubmit} className='submitButton'>
<i className='fa fa-check'/>
</button>
</div>
)
}
})

View File

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

View File

@@ -1,54 +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
}),
search: React.PropTypes.string,
toggleCodeFilter: React.PropTypes.func,
toggleNoteFilter: React.PropTypes.func
},
getInitialState: function () {
return {
isLaunchModalOpen: false
}
},
openLaunchModal: function () {
this.openModal(LaunchModal, {planet: this.props.planet, transitionTo: this.transitionTo})
},
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'>
<button onClick={this.openLaunchModal} className='launchButton btn-primary btn-block'>
<i className='fa fa-rocket fa-fw'/> Launch
</button>
<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>
)
}
})

View File

@@ -1,162 +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({profileSubmitStatus: 'sending'}, function () {
Hq.updatePlanet(planet.userName, planet.name, this.state.planet)
.then(function (res) {
var planet = res.body
this.setState({profileSubmitStatus: 'done'})
PlanetStore.Actions.update(planet)
}.bind(this))
.catch(function (err) {
this.setState({profileSubmitStatus: 'error'})
console.error(err)
}.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 modal tabModal'>
<div className='leftPane'>
<h1 className='tabLabel'>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'>
<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.profileSubmitStatus === 'sending' ? '' : ' hide')}>on Sending...</div>
<div className={'alertError' + (this.state.profileSubmitStatus === 'error' ? '' : ' hide')}>Connection failed.. Try again.</div>
<div className={'alertSuccess' + (this.state.profileSubmitStatus === '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'>
<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>
)
}
})

View File

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

View File

@@ -1,71 +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: ''
}
}
},
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 () {
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)
})
},
render: function () {
return (
<div className='TeamCreateModal modal'>
<input ref='teamName' valueLink={this.linkState('team.name')} className='nameInput stripInput' placeholder='Create new team'/>
<button onClick={this.handleSubmit} className='submitButton'>
<i className='fa fa-check'/>
</button>
</div>
)
}
})

View File

@@ -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 modal tabModal'>
<div className='leftPane'>
<div className='tabLabel'>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'>
<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'>
<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>
)
}
})

View File

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

View File

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

View File

@@ -1,107 +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')
return
}
}
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>
)
}
})

View File

@@ -1,414 +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}/>
<PlanetArticleList showOnlyWithTag={this.applyTagFilter} ref='list' articles={filteredArticles}/>
<PlanetArticleDetail
ref='detail'
article={article}
planet={this.state.planet}
showOnlyWithTag={this.applyTagFilter}/>
</div>
)
}
})

View File

@@ -1,136 +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: {},
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'>会員登録することで当サイトの利用規約及びCookieの使用を含むデータに関するポリシーに同意するものとします</p>
</div>
)
}
})

View File

@@ -1,365 +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 = true
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>
&nbsp;{!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>
<button onClick={this.openTeamSettingsModal} className='editProfileButton'>Team settings</button>
</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>
&nbsp;{!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>
&nbsp;{!planet.public ? (<i className='fa fa-lock'/>) : null}
</li>
)
})
return (
<div key={'user-' + team.id} className='planetGroup'>
<div className='planetGroupLabel'>{team.name}</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}</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>
)
}
})

291
browser/main/HomePage.js Normal file
View File

@@ -0,0 +1,291 @@
import React, { PropTypes} from 'react'
import { connect } from 'react-redux'
import { CREATE_MODE, EDIT_MODE, IDLE_MODE, NEW, toggleTutorial } from 'boost/actions'
// import UserNavigator from './HomePage/UserNavigator'
import ArticleNavigator from './HomePage/ArticleNavigator'
import ArticleTopBar from './HomePage/ArticleTopBar'
import ArticleList from './HomePage/ArticleList'
import ArticleDetail from './HomePage/ArticleDetail'
import _ from 'lodash'
import keygen from 'boost/keygen'
import { isModalOpen, closeModal } from 'boost/modal'
const TEXT_FILTER = 'TEXT_FILTER'
const FOLDER_FILTER = 'FOLDER_FILTER'
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
const TAG_FILTER = 'TAG_FILTER'
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 === 27) closeModal()
return
}
let { status, dispatch } = this.props
let { nav, top, list, detail } = this.refs
if (status.isTutorialOpen) {
dispatch(toggleTutorial())
e.preventDefault()
return
}
// Search inputがfocusされていたら大体のキー入力は無視される。
if (top.isInputFocused() && !e.metaKey) {
if (e.keyCode === 13 || e.keyCode === 27) top.escape()
return
}
switch (status.mode) {
case CREATE_MODE:
case EDIT_MODE:
if (e.keyCode === 27) {
detail.handleCancelButtonClick()
}
if ((e.keyCode === 13 && e.metaKey) || (e.keyCode === 83 && e.metaKey)) {
detail.handleSaveButtonClick()
}
break
case IDLE_MODE:
if (e.keyCode === 69) {
detail.handleEditButtonClick()
e.preventDefault()
}
if (e.keyCode === 68) {
detail.handleDeleteButtonClick()
}
// `detail`の`openDeleteConfirmMenu`の時。
if (detail.state.openDeleteConfirmMenu) {
if (e.keyCode === 27) {
detail.handleDeleteCancelButtonClick()
}
if (e.keyCode === 13 && e.metaKey) {
detail.handleDeleteConfirmButtonClick()
}
break
}
// `detail`の`openDeleteConfirmMenu`が`true`なら呼ばれない。
if (e.keyCode === 27 || (e.keyCode === 70 && e.metaKey)) {
top.focusInput()
}
if (e.keyCode === 38) {
list.selectPriorArticle()
}
if (e.keyCode === 40) {
list.selectNextArticle()
}
if (e.keyCode === 65 || e.keyCode === 13 && e.metaKey) {
nav.handleNewPostButtonClick()
e.preventDefault()
}
}
}
render () {
let { dispatch, status, articles, allArticles, activeArticle, folders, tags, filters } = this.props
return (
<div className='HomePage'>
<ArticleNavigator
ref='nav'
dispatch={dispatch}
folders={folders}
status={status}
allArticles={allArticles}
/>
<ArticleTopBar
ref='top'
dispatch={dispatch}
status={status}
/>
<ArticleList
ref='list'
dispatch={dispatch}
folders={folders}
articles={articles}
status={status}
activeArticle={activeArticle}
/>
<ArticleDetail
ref='detail'
dispatch={dispatch}
activeArticle={activeArticle}
folders={folders}
status={status}
tags={tags}
filters={filters}
/>
</div>
)
}
}
// 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 remap (state) {
let { folders, articles, status } = state
if (articles == null) articles = []
articles.sort((a, b) => {
return new Date(b.updatedAt) - new Date(a.updatedAt)
})
let allArticles = articles.slice()
let tags = _.uniq(allArticles.reduce((sum, article) => {
if (!_.isArray(article.tags)) return sum
return sum.concat(article.tags)
}, []))
// 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 => folder.name.match(new RegExp(`^${filter.value}$`)))
})
let fuzzyTargetFolders = folders.filter(folder => {
return _.find(folderFilters, filter => folder.name.match(new RegExp(`^${filter.value}`)))
})
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 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.match(new RegExp(tagFilter.value, 'i')))
})
}, articles)
}
}
// Grab active article
let activeArticle = _.findWhere(articles, {key: status.articleKey})
if (activeArticle == null) activeArticle = articles[0]
// remove Unsaved new article if user is not CREATE_MODE
if (status.mode !== CREATE_MODE) {
let targetIndex = _.findIndex(articles, article => article.status === NEW)
if (targetIndex >= 0) articles.splice(targetIndex, 1)
}
// switching CREATE_MODE
// restrict
// 1. team have one folder at least
// or Change IDLE MODE
if (status.mode === CREATE_MODE) {
let newArticle = _.findWhere(articles, {status: 'NEW'})
console.log('targetFolders')
let FolderKey = targetFolders.length > 0
? targetFolders[0].key
: folders[0].key
if (newArticle == null) {
newArticle = {
id: null,
key: keygen(),
title: '',
content: '',
mode: 'markdown',
tags: [],
FolderKey: FolderKey,
status: NEW
}
articles.unshift(newArticle)
}
activeArticle = newArticle
} else if (status.mode === CREATE_MODE) {
status.mode = IDLE_MODE
}
return {
folders,
status,
allArticles,
articles,
activeArticle,
tags,
filters: {
folder: folderFilters,
tag: tagFilters,
text: textFilters
}
}
}
HomePage.propTypes = {
params: PropTypes.shape({
userId: PropTypes.string
}),
status: PropTypes.shape({
userId: PropTypes.string
}),
articles: PropTypes.array,
allArticles: PropTypes.array,
activeArticle: PropTypes.shape(),
dispatch: PropTypes.func,
folders: PropTypes.array,
filters: PropTypes.shape({
folder: PropTypes.array,
tag: PropTypes.array,
text: PropTypes.array
})
}
export default connect(remap)(HomePage)

View File

@@ -0,0 +1,520 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import moment from 'moment'
import _ from 'lodash'
import ModeIcon from 'boost/components/ModeIcon'
import MarkdownPreview from 'boost/components/MarkdownPreview'
import CodeEditor from 'boost/components/CodeEditor'
import {
IDLE_MODE,
CREATE_MODE,
EDIT_MODE,
switchMode,
switchArticle,
switchFolder,
clearSearch,
lockStatus,
unlockStatus,
updateArticle,
destroyArticle,
NEW
} from 'boost/actions'
import linkState from 'boost/linkState'
import FolderMark from 'boost/components/FolderMark'
import TagLink from 'boost/components/TagLink'
import TagSelect from 'boost/components/TagSelect'
import ModeSelect from 'boost/components/ModeSelect'
import activityRecord from 'boost/activityRecord'
const BRAND_COLOR = '#18AF90'
const editDeleteTutorialElement = (
<svg width='300' height='500' className='tutorial'>
<text x='50' y='220' fill={BRAND_COLOR} fontSize='24'>Edit / Delete a post</text>
<text x='90' y='245' fill={BRAND_COLOR} fontSize='18'>press `e`/`d`</text>
<svg x='150' y='35'>
<path fill='white' d='M87.5,93.6c-16.3-5.7-30.6-16.7-39.9-31.4c-5.5-8.7-9-19.1-3.4-28.7c4.8-8.2,13.6-12.8,22.4-15.3
c15.7-4.5,34.4-6.2,49.7,0.4c17.3,7.4,25.6,26.3,25.7,44.4c0.1,10.4-3.4,20.9-13.1,26c-8.6,4.5-19,4.1-28.4,3.7
c-1.9-0.1-1.9,2.9,0,3c9.3,0.4,19.2,0.6,27.9-3.2c8.5-3.7,13.8-11.2,15.7-20.2c3.6-17.9-2.9-40.2-17.7-51.4
C110.8,9.1,89,9.9,70.8,14c-17.9,4-37.4,16.8-31.3,37.9C45.6,73,66.7,89.5,86.7,96.5C88.6,97.1,89.4,94.2,87.5,93.6L87.5,93.6z'/>
<path fill='white' d='M11.9,89.7c14.8-3.4,29.7-6,44.8-7.9c-0.5-0.6-1-1.3-1.4-1.9c-2.6,6.3-2.8,12.7-0.7,19.2
c0.6,1.8,3.5,1,2.9-0.8c-1.9-6-1.7-11.8,0.7-17.6c0.3-0.8-0.5-2-1.4-1.9c-15.3,1.9-30.6,4.5-45.6,8C9.3,87.3,10.1,90.2,11.9,89.7
L11.9,89.7z'/>
<path fill='white' d='M48.6,81.5c-9.4,10.4-17,22.3-22.2,35.3c-5.5,13.6-9.3,28.9-6,43.4c0.4,1.9,3.3,1.1,2.9-0.8
c-3.2-14,0.7-28.8,6-41.8c5.1-12.5,12.4-24,21.5-34C52,82.2,49.9,80,48.6,81.5L48.6,81.5z'/>
</svg>
</svg>
)
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>
)
function makeInstantArticle (article) {
return Object.assign({}, article)
}
export default class ArticleDetail extends React.Component {
constructor (props) {
super(props)
this.state = {
article: makeInstantArticle(props.activeArticle),
previewMode: false,
isArticleEdited: false,
isTagChanged: false,
isTitleChanged: false,
isContentChanged: false,
isModeChanged: false
}
}
componentDidMount () {
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
}
componentWillUnmount () {
clearInterval(this.refreshTimer)
}
componentDidUpdate (prevProps) {
let isModeChanged = prevProps.status.mode !== this.props.status.mode
if (isModeChanged && this.props.status.mode !== IDLE_MODE) {
ReactDOM.findDOMNode(this.refs.title).focus()
}
}
componentWillReceiveProps (nextProps) {
let nextState = {}
let isArticleChanged = nextProps.activeArticle != null && (nextProps.activeArticle.key !== this.state.article.key)
let isModeChanged = nextProps.status.mode !== this.props.status.mode
// Reset article input
if (isArticleChanged || (isModeChanged && nextProps.status.mode !== IDLE_MODE)) {
Object.assign(nextState, {
article: makeInstantArticle(nextProps.activeArticle)
})
}
// Clean state
if (isModeChanged) {
Object.assign(nextState, {
openDeleteConfirmMenu: false,
previewMode: false,
isArticleEdited: false,
isTagChanged: false,
isTitleChanged: false,
isContentChanged: false
})
}
this.setState(nextState)
}
renderEmpty () {
return (
<div className='ArticleDetail empty'>
Command() + Enter to create a new post
</div>
)
}
handleEditButtonClick (e) {
let { dispatch } = this.props
dispatch(switchMode(EDIT_MODE))
}
handleDeleteButtonClick (e) {
this.setState({openDeleteConfirmMenu: true})
}
handleDeleteConfirmButtonClick (e) {
let { dispatch, activeArticle } = this.props
dispatch(destroyArticle(activeArticle.key))
activityRecord.emit('ARTICLE_DESTROY')
this.setState({openDeleteConfirmMenu: false})
}
handleDeleteCancelButtonClick (e) {
this.setState({openDeleteConfirmMenu: false})
}
renderIdle () {
let { status, activeArticle, folders } = this.props
let tags = activeArticle.tags != null ? activeArticle.tags.length > 0
? activeArticle.tags.map(tag => {
return (<TagLink key={tag} tag={tag}/>)
})
: (
<span className='noTags'>Not tagged yet</span>
) : null
let folder = _.findWhere(folders, {key: activeArticle.FolderKey})
return (
<div className='ArticleDetail idle'>
{this.state.openDeleteConfirmMenu
? (
<div className='deleteConfirm'>
<div className='right'>
Are you sure to delete this article?
<button onClick={e => this.handleDeleteConfirmButtonClick(e)} className='primary'>
<i className='fa fa-fw fa-check'/> Sure
</button>
<button onClick={e => this.handleDeleteCancelButtonClick(e)}>
<i className='fa fa-fw fa-times'/> Cancel
</button>
</div>
</div>
)
: (
<div className='detailInfo'>
<div className='left'>
<div className='info'>
<FolderMark color={folder.color}/> <span className='folderName'>{folder.name}</span>&nbsp;
Created : {moment(activeArticle.createdAt).format('YYYY/MM/DD')}&nbsp;
Updated : {moment(activeArticle.updatedAt).format('YYYY/MM/DD')}
</div>
<div className='tags'><i className='fa fa-fw fa-tags'/>{tags}</div>
</div>
<div className='right'>
<button onClick={e => this.handleEditButtonClick(e)} className='editBtn'>
<i className='fa fa-fw fa-edit'/><span className='tooltip'>Edit (e)</span>
</button>
<button onClick={e => this.handleDeleteButtonClick(e)} className='deleteBtn'>
<i className='fa fa-fw fa-trash'/><span className='tooltip'>Delete (d)</span>
</button>
</div>
{status.isTutorialOpen ? editDeleteTutorialElement : null}
</div>
)
}
<div className='detailBody'>
<div className='detailPanel'>
<div className='header'>
<ModeIcon className='mode' mode={activeArticle.mode}/>
<div className='title'>{activeArticle.title}</div>
</div>
{activeArticle.mode === 'markdown'
? <MarkdownPreview content={activeArticle.content}/>
: <CodeEditor readOnly onChange={(e, value) => this.handleContentChange(e, value)} mode={activeArticle.mode} code={activeArticle.content}/>
}
</div>
</div>
</div>
)
}
handleCancelButtonClick (e) {
let { activeArticle, dispatch } = this.props
dispatch(unlockStatus())
if (activeArticle.status === NEW) dispatch(switchArticle(null))
dispatch(switchMode(IDLE_MODE))
}
handleSaveButtonClick (e) {
let { dispatch, folders, filters } = this.props
let article = this.state.article
let newArticle = Object.assign({}, article)
let folder = _.findWhere(folders, {key: article.FolderKey})
if (folder == null) return false
dispatch(unlockStatus())
delete newArticle.status
newArticle.updatedAt = new Date()
if (newArticle.createdAt == null) {
newArticle.createdAt = new Date()
activityRecord.emit('ARTICLE_CREATE')
} else {
activityRecord.emit('ARTICLE_UPDATE')
}
dispatch(updateArticle(newArticle))
dispatch(switchMode(IDLE_MODE))
// Folder filterがかかっている時に、
// Searchを初期化し、更新先のFolder filterをかける
// かかれていない時に
// Searchを初期化する
if (filters.folder.length !== 0) dispatch(switchFolder(folder.name))
else dispatch(clearSearch())
dispatch(switchArticle(newArticle.key))
}
handleFolderKeyChange (e) {
let article = this.state.article
article.FolderKey = e.target.value
this.setState({article: article})
}
handleTitleChange (e) {
let { article } = this.state
article.title = e.target.value
let _isTitleChanged = article.title !== this.props.activeArticle.title
let { isTagChanged, isContentChanged, isArticleEdited, isModeChanged } = this.state
let _isArticleEdited = _isTitleChanged || isTagChanged || isContentChanged || isModeChanged
this.setState({
article,
isTitleChanged: _isTitleChanged,
isArticleEdited: _isArticleEdited
}, () => {
if (isArticleEdited !== _isArticleEdited) {
let { dispatch } = this.props
if (_isArticleEdited) {
console.log('lockit')
dispatch(lockStatus())
} else {
console.log('unlockit')
dispatch(unlockStatus())
}
}
})
}
handleTagsChange (newTag, tags) {
let article = this.state.article
article.tags = tags
this.setState({article: article})
let _isTagChanged = _.difference(article.tags, this.props.activeArticle.tags).length > 0 || _.difference(this.props.activeArticle.tags, article.tags).length > 0
let { isTitleChanged, isContentChanged, isArticleEdited, isModeChanged } = this.state
let _isArticleEdited = _isTagChanged || isTitleChanged || isContentChanged || isModeChanged
this.setState({
article,
isTagChanged: _isTagChanged,
isArticleEdited: _isArticleEdited
}, () => {
if (isArticleEdited !== _isArticleEdited) {
let { dispatch } = this.props
if (_isArticleEdited) {
console.log('lockit')
dispatch(lockStatus())
} else {
console.log('unlockit')
dispatch(unlockStatus())
}
}
})
}
handleModeChange (value) {
let { article } = this.state
article.mode = value
let _isModeChanged = article.mode !== this.props.activeArticle.mode
let { isTagChanged, isContentChanged, isArticleEdited, isTitleChanged } = this.state
let _isArticleEdited = _isModeChanged || isTagChanged || isContentChanged || isTitleChanged
this.setState({
article,
previewMode: false,
isModeChanged: _isModeChanged,
isArticleEdited: _isArticleEdited
}, () => {
if (isArticleEdited !== _isArticleEdited) {
let { dispatch } = this.props
if (_isArticleEdited) {
console.log('lockit')
dispatch(lockStatus())
} else {
console.log('unlockit')
dispatch(unlockStatus())
}
}
})
}
handleModeSelectBlur () {
if (this.refs.code != null) {
this.refs.code.editor.focus()
}
}
handleContentChange (e, value) {
let { status } = this.props
if (status.mode === IDLE_MODE) {
return
}
let { article } = this.state
article.content = value
let _isContentChanged = article.content !== this.props.activeArticle.content
let { isTagChanged, isModeChanged, isArticleEdited, isTitleChanged } = this.state
let _isArticleEdited = _isContentChanged || isTagChanged || isModeChanged || isTitleChanged
this.setState({
article,
isContentChanged: _isContentChanged,
isArticleEdited: _isArticleEdited
}, () => {
if (isArticleEdited !== _isArticleEdited) {
let { dispatch } = this.props
if (_isArticleEdited) {
console.log('lockit')
dispatch(lockStatus())
} else {
console.log('unlockit')
dispatch(unlockStatus())
}
}
})
}
handleTogglePreviewButtonClick (e) {
this.setState({previewMode: !this.state.previewMode})
}
handleTitleKeyDown (e) {
if (e.keyCode === 9 && !e.shiftKey) {
e.preventDefault()
this.refs.mode.handleIdleSelectClick()
}
}
renderEdit () {
let { folders, status, tags } = this.props
let folderOptions = folders.map(folder => {
return (
<option key={folder.key} value={folder.key}>{folder.name}</option>
)
})
return (
<div className='ArticleDetail edit'>
<div className='detailInfo'>
<div className='left'>
<select
className='folder'
value={this.state.article.FolderKey}
onChange={e => this.handleFolderKeyChange(e)}
>
{folderOptions}
</select>
{this.state.isArticleEdited ? ' (edited)' : ''}
<TagSelect
tags={this.state.article.tags}
onChange={(tags, tag) => this.handleTagsChange(tags, tag)}
suggestTags={tags}
/>
{status.isTutorialOpen ? tagSelectTutorialElement : null}
</div>
<div className='right'>
{
this.state.article.mode === 'markdown'
? (<button className='preview' onClick={e => this.handleTogglePreviewButtonClick(e)}>{!this.state.previewMode ? 'Preview' : 'Edit'}</button>)
: null
}
<button onClick={e => this.handleCancelButtonClick(e)}>Cancel</button>
<button onClick={e => this.handleSaveButtonClick(e)} className='primary'>Save</button>
</div>
</div>
<div className='detailBody'>
<div className='detailPanel'>
<div className='header'>
<div className='title'>
<input onKeyDown={e => this.handleTitleKeyDown(e)} placeholder='Title' ref='title' value={this.state.article.title} onChange={e => this.handleTitleChange(e)}/>
</div>
<ModeSelect
ref='mode'
onChange={e => this.handleModeChange(e)}
value={this.state.article.mode}
className='mode'
onBlur={() => this.handleModeSelectBlur()}
/>
{status.isTutorialOpen ? modeSelectTutorialElement : null}
</div>
{this.state.previewMode
? <MarkdownPreview content={this.state.article.content}/>
: (<CodeEditor
ref='code'
onChange={(e, value) => this.handleContentChange(e, value)}
readOnly={false}
mode={this.state.article.mode}
code={this.state.article.content}
/>)
}
</div>
</div>
</div>
)
}
render () {
let { status, activeArticle } = this.props
if (activeArticle == null) return this.renderEmpty()
switch (status.mode) {
case CREATE_MODE:
case EDIT_MODE:
return this.renderEdit()
case IDLE_MODE:
default:
return this.renderIdle()
}
}
}
ArticleDetail.propTypes = {
status: PropTypes.shape(),
activeArticle: PropTypes.shape(),
activeUser: PropTypes.shape(),
dispatch: PropTypes.func
}
ArticleDetail.prototype.linkState = linkState

View File

@@ -0,0 +1,118 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ModeIcon from 'boost/components/ModeIcon'
import moment from 'moment'
import { switchArticle, NEW } from 'boost/actions'
import FolderMark from 'boost/components/FolderMark'
import TagLink from 'boost/components/TagLink'
import _ from 'lodash'
export default class ArticleList extends React.Component {
componentDidMount () {
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
}
componentWillUnmount () {
clearInterval(this.refreshTimer)
}
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
}
}
// 移動ができなかったら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) {
if (article.status === NEW) return null
dispatch(switchArticle(article.key))
}
}
render () {
let { articles, activeArticle, folders } = this.props
let articleElements = articles.map(article => {
let tagElements = Array.isArray(article.tags) && article.tags.length > 0
? article.tags.map(tag => {
return (<TagLink key={tag} tag={tag}/>)
})
: (<span>Not tagged yet</span>)
let folder = _.findWhere(folders, {key: article.FolderKey})
return (
<div key={'article-' + article.key}>
<div onClick={e => this.handleArticleClick(article)(e)} className={'articleItem' + (activeArticle.key === article.key ? ' active' : '')}>
<div className='top'>
{folder != null
? <span className='folderName'><FolderMark color={folder.color}/>{folder.name}</span>
: <span><FolderMark color={-1}/>Unknown</span>
}
<span className='updatedAt'>{article.status != null ? article.status : moment(article.updatedAt).fromNow()}</span>
</div>
<div className='middle'>
<ModeIcon className='mode' mode={article.mode}/> <div className='title'>{article.status !== NEW ? article.title : '(New article)'}</div>
</div>
<div className='bottom'>
<div className='tags'><i className='fa fa-fw fa-tags'/>{tagElements}</div>
</div>
</div>
<div className='divider'></div>
</div>
)
})
return (
<div className='ArticleList'>
{articleElements}
</div>
)
}
}
ArticleList.propTypes = {
folders: PropTypes.array,
articles: PropTypes.array,
activeArticle: PropTypes.shape(),
dispatch: PropTypes.func
}

View File

@@ -0,0 +1,160 @@
import React, { PropTypes } from 'react'
import { findWhere } from 'lodash'
import { setSearchFilter, switchFolder, switchMode, CREATE_MODE } from 'boost/actions'
import { openModal } from 'boost/modal'
import FolderMark from 'boost/components/FolderMark'
import Preferences from 'boost/components/modal/Preferences'
import CreateNewFolder from 'boost/components/modal/CreateNewFolder'
import remote from 'remote'
let userName = remote.getGlobal('process').env.USER
const BRAND_COLOR = '#18AF90'
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 newPostTutorialElement = (
<svg width='900' height='900' className='tutorial'>
<text x='290' y='155' fill={BRAND_COLOR} fontSize='24'>Create a new post!!</text>
<text x='300' y='180' fill={BRAND_COLOR} fontSize='16'>press `⌘ + Enter` or `a`</text>
<svg x='130' y='-20' width='400' height='400'>
<path fill='white' d='M56.2,132.5c11.7-2.9,23.9-6,36.1-4.1c8.7,1.4,16.6,5.5,23.7,10.5c13.3,9.4,24.5,21.5,40.2,27
c1.8,0.6,2.6-2.3,0.8-2.9c-17.1-6-28.9-20.3-44-29.7c-7-4.4-14.8-7.4-23-8.2c-11.7-1.1-23.3,1.7-34.5,4.5
C53.6,130.1,54.4,133,56.2,132.5L56.2,132.5 z'/>
</svg>
<svg x='130' y='-120' width='400' height='400'>
<path fill='white' d='M82.6,218c-7.7,4.5-15.3,9.3-22.7,14.3c-1,0.7-0.9,2.4,0.4,2.7c6.2,1.8,11.5,4.8,16.2,9.2
c1.4,1.3,3.5-0.8,2.1-2.1c-5.1-4.8-10.9-8.1-17.6-10c0.1,0.9,0.2,1.8,0.4,2.7c7.4-5,15-9.8,22.7-14.3
C85.7,219.7,84.2,217.1,82.6,218L82.6,218z'/>
</svg>
</svg>
)
const newFolderTutorialElement = (
<svg width='800' height='500' className='tutorial'>
<text x='145' y='110' fill={BRAND_COLOR} fontSize='24'>Create a new folder!!</text>
<svg x='115' y='-10' width='300' height='400'>
<path fill='white' d='M36.6,3.7C28.8,8.2,21.3,13,13.9,18c-1,0.7-0.9,2.4,0.4,2.7c6.2,1.8,11.5,4.8,16.2,9.2
c1.4,1.3,3.5-0.8,2.1-2.1c-5.1-4.8-10.9-8.1-17.6-10c0.1,0.9,0.2,1.8,0.4,2.7c7.4-5,15-9.8,22.7-14.3C39.7,5.3,38.2,2.7,36.6,3.7
L36.6,3.7z'/>
<path fill='white' d='M16.8,21.5c13.3-6.9,29.5-7,42.6,0.6c5.6,3.2,10.4,7.7,14.1,13c3.8,5.4,10.3,16.2,2.2,20.6
c-1.2,0.7-2.5,1.2-3.9,1.6c-1.1,0.4-2.3,0.5-3.4,0.5c-1.3-1.4-2.6-2.8-3.9-4.2c-0.2-4.6,7.5-6,10.5-5.8
c7.4,0.7,13.7,6.2,18.4,11.6c9.4,10.7,14.7,24.3,15.6,38.5c0.1,1.9,3.1,1.9,3,0c-0.9-15.5-6.9-30.4-17.5-41.8
c-6.8-7.3-25.8-19.1-32.3-4.8c-1.9,4.1,0.3,8.5,4.8,9.4c4.6,0.8,11.6-1.8,14.3-5.7c3.6-5.3-0.1-12.8-2.8-17.6
c-3.4-6.1-8.2-11.3-13.8-15.4C50.2,11.6,31,10.9,15.3,19C13.6,19.8,15.1,22.4,16.8,21.5L16.8,21.5z'/>
</svg>
</svg>
)
export default class ArticleNavigator extends React.Component {
handlePreferencesButtonClick (e) {
openModal(Preferences)
}
handleNewPostButtonClick (e) {
let { dispatch } = this.props
dispatch(switchMode(CREATE_MODE))
}
handleNewFolderButton (e) {
let { activeUser } = this.props
openModal(CreateNewFolder, {user: activeUser})
}
handleFolderButtonClick (name) {
return e => {
let { dispatch } = this.props
dispatch(switchFolder(name))
}
}
handleAllFoldersButtonClick (e) {
let { dispatch } = this.props
dispatch(setSearchFilter(''))
}
render () {
let { status, folders, allArticles } = this.props
let { targetFolders } = status
if (targetFolders == null) targetFolders = []
let folderElememts = folders.map((folder, index) => {
let isActive = findWhere(targetFolders, {key: folder.key})
let articleCount = allArticles.filter(article => article.FolderKey === folder.key).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 className='ArticleNavigator'>
<div className='userInfo'>
<div className='userProfileName'>{userName}</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='controlSection'>
<button onClick={e => this.handleNewPostButtonClick(e)} className='newPostBtn'>
New Post
<span className='tooltip'>Create a new Post ( + Enter or a)</span>
</button>
{status.isTutorialOpen ? newPostTutorialElement : null}
</div>
<div className='folders'>
<div className='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</span>
</button>
{status.isTutorialOpen ? newFolderTutorialElement : null}
</div>
<div className='folderList'>
<button onClick={e => this.handleAllFoldersButtonClick(e)} className={targetFolders.length === 0 ? 'active' : ''}>All folders</button>
{folderElememts}
</div>
</div>
</div>
)
}
}
ArticleNavigator.propTypes = {
activeUser: PropTypes.object,
folders: PropTypes.array,
allArticles: PropTypes.array,
status: PropTypes.shape({
folderId: PropTypes.number
}),
dispatch: PropTypes.func
}

View File

@@ -0,0 +1,164 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ExternalLink from 'boost/components/ExternalLink'
import { setSearchFilter, clearSearch, toggleTutorial } from 'boost/actions'
const BRAND_COLOR = '#18AF90'
const searchTutorialElement = (
<svg width='750' height='120' className='tutorial'>
<text x='450' y='33' fill={BRAND_COLOR} fontSize='24'>Search some posts!!</text>
<text x='450' y='60' fill={BRAND_COLOR} fontSize='18'>{'- Search by tag : #{string}'}</text>
<text x='450' y='85' fill={BRAND_COLOR} fontSize='18'>
{'- Search by folder : /{folder_name}\n'}</text>
<text x='465' y='105' fill={BRAND_COLOR} fontSize='14'>
{'exact match : //{folder_name}'}</text>
<svg width='500' height='300'>
<path fill='white' d='M54.5,51.5c-12.4,3.3-27.3-1.4-38.4-7C11.2,42,5,38.1,5.6,31.8c0.7-6.9,8.1-11.2,13.8-13.7
c12.3-5.4,26.4-6.8,39.7-7.7C72.4,9.6,85.7,9.7,99,9.8c55.2,0.3,110.4,2.2,165.5-1.5C291,6.5,317.7,3.8,344.1,7
c12.8,1.6,25.8,4.4,37.5,10c1.2,0.6,2.4,1.1,3.5,1.8c2.4,1.4,3.2,1.5,3.3,4.5c0.1,3.6-2.3,5.9-4.8,8.3c-3.9,3.8-8.6,6.8-13.5,9.2
c-12.6,6-26.5,7.2-40.3,7.7c-13.7,0.5-27.5,0.6-41.2,1.1c-27.7,0.9-55.3,2.2-82.9,4c-30.8,2-61.6,4.5-92.3,7.6
c-15.4,1.5-30.8,3.7-46.3,4.9c-13.6,1.1-30.7,1.5-41.8-7.8c-1.5-1.2-3.6,0.9-2.1,2.1c8.9,7.5,21.4,9.2,32.7,9.2
c15.3,0,30.6-2.6,45.8-4.2c31.3-3.3,62.7-6,94.2-8.1c30.9-2.1,61.8-3.7,92.8-4.7c15.7-0.5,31.4-0.5,47-1.3
c13.1-0.7,26.3-2.7,38.1-8.9c4.4-2.3,8.5-5.1,12-8.6c2.8-2.8,7.3-7.3,6.4-11.7c-0.8-4.3-6.4-6.3-9.8-7.9
c-5.6-2.6-11.4-4.6-17.3-6.2c-28.3-7.5-58.1-5.6-87-3.6c-62.3,4.4-124.5,2.6-187,2.4c-16.4,0-32.8,0-49,2.4
C29.9,11,13.4,13.8,5.5,24.6c-7.3,10,0.7,18.4,9.8,22.9c11.9,5.8,26.9,10.4,40,7C57.2,53.9,56.4,51,54.5,51.5L54.5,51.5z'/>
<path fill='white' d='M446.5,21.4c-9.1-1.6-18.1-3.5-27.4-3.5c-10.2,0-20.4,1.4-30.5,2.8c-1.9,0.3-1.9,3.3,0,3
c9.5-1.3,19.1-2.6,28.8-2.7c9.6-0.2,18.9,1.7,28.3,3.4C447.6,24.7,448.4,21.8,446.5,21.4L446.5,21.4z'/>
</svg>
</svg>
)
export default class ArticleTopBar extends React.Component {
constructor (props) {
super(props)
this.state = {
isTooltipHidden: true
}
}
componentDidMount () {
this.searchInput = ReactDOM.findDOMNode(this.refs.searchInput)
}
componentWillUnmount () {
this.searchInput.removeEventListener('keydown', this.showTooltip)
this.searchInput.removeEventListener('focus', this.showTooltip)
this.searchInput.removeEventListener('blur', this.showTooltip)
}
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
}
this.blurInput()
}
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()
}
handleTutorialButtonClick (e) {
let { dispatch } = this.props
dispatch(toggleTutorial())
}
render () {
let { status } = this.props
return (
<div className='ArticleTopBar'>
<div className='left'>
<div className='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='searchClearBtn'><i className='fa fa-times'/></button>
: null
}
<div className={'tooltip' + (this.state.isTooltipHidden ? ' hide' : '')}>
- Search by tag : #{'{string}'}<br/>
- Search by folder : /{'{folder_name}'}
</div>
</div>
{status.isTutorialOpen ? searchTutorialElement : null}
</div>
<div className='right'>
<button onClick={e => this.handleTutorialButtonClick(e)}>?<span className='tooltip'>How to use</span>
</button>
<ExternalLink className='logo' href='http://b00st.io'>
<img src='../../resources/favicon-230x230.png' width='44' height='44'/>
<span className='tooltip'>Boost official page</span>
</ExternalLink>
</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='120' y='55' fontSize='18' fill={BRAND_COLOR}>with pressing `Control` + `shift` + `tab`</text>
</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 = {
search: PropTypes.string,
dispatch: PropTypes.func,
status: PropTypes.shape({
search: PropTypes.string
})
}

View File

@@ -0,0 +1,52 @@
import React, { Component, PropTypes } from 'react'
import { Link } from 'react-router'
import ProfileImage from 'boost/components/ProfileImage'
import { openModal } from 'boost/modal'
import CreateNewTeam from 'boost/components/modal/CreateNewTeam'
export default class UserNavigator extends Component {
handleClick (e) {
openModal(CreateNewTeam)
}
// for dev
componentDidMount () {
// openModal(CreateNewTeam)
}
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' onClick={e => this.handleClick(e)}>
+
<div className='tooltip'>Create a new team</div>
</button>
</div>
)
}
}
UserNavigator.propTypes = {
users: PropTypes.array
}

View File

93
browser/main/LoginPage.js Normal file
View File

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

44
browser/main/MainPage.js Normal file
View File

@@ -0,0 +1,44 @@
import ipc from 'ipc'
import React, { PropTypes } from 'react'
var ContactModal = require('boost/components/modal/ContactModal')
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.')
}
openContactModal () {
this.openModal(ContactModal)
}
render () {
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> */}
{this.props.children}
</div>
)
}
}
MainContainer.propTypes = {
children: PropTypes.element
}

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
var shell = require('shell')
module.exports = {
openExternal: function (e) {
shell.openExternal(e.currentTarget.href)
e.preventDefault()
}
}

View File

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

View File

@@ -1,30 +0,0 @@
function deleteItemFromTargetArray (item, 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) {
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
}

View File

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

View File

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

View File

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

View File

@@ -1,166 +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)
},
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)
},
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)
}
}

104
browser/main/SignupPage.js Normal file
View File

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

View File

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

View File

@@ -1,159 +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)
// 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
})
}
})

View File

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

View File

@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<script>
var version = require('remote').getGlobal('version')
document.title = 'Boost ' + ((version == null || version.length === 0) ? 'DEV version' : 'v' + version)
</script>
<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">
<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;
}
});
}
require('electron-stylus')(__dirname + '/../styles/main/index.styl')
</script>
</head>
<body>
<div id="content"></div>
<script src="../ace/src-min/ace.js"></script>
<script>
require('node-jsx').install({ harmony: true, extension: '.jsx' })
require('./index.jsx')
</script>
</body>
</html>

View File

@@ -1,56 +1,68 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>CodeXen</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="../vendor/fontawesome/css/font-awesome.min.css" media="screen" title="no title" charset="utf-8"> <link rel="stylesheet" href="../../node_modules/font-awesome/css/font-awesome.min.css" media="screen" charset="utf-8">
<link rel="stylesheet" href="../../node_modules/devicon/devicon.min.css">
<link rel="shortcut icon" href="favicon.ico"> <link rel="shortcut icon" href="favicon.ico">
<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); <style>
for (var i = 1; i < arguments.length; i++) { @font-face {
var nextSource = arguments[i]; font-family: 'Lato';
if (nextSource === undefined || nextSource === null) { src: url('../../resources/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
continue; url('../../resources/Lato-Regular.woff') format('woff'), /* Modern Browsers */
} url('../../resources/Lato-Regular.ttf') format('truetype');
nextSource = Object(nextSource); font-style: normal;
font-weight: normal;
var keysArray = Object.keys(Object(nextSource)); text-rendering: optimizeLegibility;
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> #loadingCover{
<script src="../vendor/moment/min/moment.min.js"></script> position: absolute;
<script src="../vendor/markdown-it/dist/markdown-it.min.js"></script> top: 0;
<script src="../vendor/react/react-with-addons.js"></script> bottom: 0;
<script src="../vendor/react-router/build/umd/ReactRouter.js"></script> left: 0;
<script src="../vendor/reflux/dist/reflux.js"></script> right: 0;
<script src="../ace/src-min/ace.js"></script> 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> </head>
<body> <body>
<div id="loadingCover">
<img src="../../resources/favicon-230x230.png">
<div class='message'>Loading...</div>
</div>
<div id="content"></div> <div id="content"></div>
<script src="http://localhost:8090/webpack-dev-server.js"></script>
<script type="text/javascript" src="http://localhost:8090/assets/main.js"></script> <script src="../../submodules/ace/src-min/ace.js"></script>
<script type="text/javascript" src="http://localhost:8090/assets/main-style.js"></script> <script type='text/javascript'>
require('web-frame').setZoomLevelLimits(1, 1)
var version = require('remote').require('app').getVersion()
document.title = 'Boost' + ((version == null || version.length === 0) ? ' DEV' : '')
var scriptUrl = process.env.BOOST_ENV === 'development'
? 'http://localhost:8080/assets/main.js'
: '../../compiled/main.js'
var scriptEl=document.createElement('script')
scriptEl.setAttribute("type","text/javascript")
scriptEl.setAttribute("src", scriptUrl)
document.getElementsByTagName("head")[0].appendChild(scriptEl)
</script>
</body> </body>
</html> </html>

44
browser/main/index.js Normal file
View File

@@ -0,0 +1,44 @@
import React from 'react'
import { Provider } from 'react-redux'
// import { updateUser } from 'boost/actions'
import { Router, Route, IndexRoute } from 'react-router'
import MainPage from './MainPage'
import HomePage from './HomePage'
// import auth from 'boost/auth'
import store from 'boost/store'
import ReactDOM from 'react-dom'
require('../styles/main/index.styl')
import { openModal } from 'boost/modal'
import Tutorial from 'boost/components/modal/Tutorial'
import activityRecord from 'boost/activityRecord'
import ipc from 'ipc'
activityRecord.init()
window.addEventListener('online', function () {
ipc.send('check-update', 'check-update')
})
let routes = (
<Route path='/' component={MainPage}>
<IndexRoute name='home' component={HomePage}/>
</Route>
)
let el = document.getElementById('content')
ReactDOM.render((
<div>
<Provider store={store}>
<Router>{routes}</Router>
</Provider>
</div>
), el, function () {
let loadingCover = document.getElementById('loadingCover')
loadingCover.parentNode.removeChild(loadingCover)
let status = JSON.parse(localStorage.getItem('status'))
if (status == null) status = {}
if (!status.introWatched) {
openModal(Tutorial)
status.introWatched = true
localStorage.setItem('status', JSON.stringify(status))
}
})

View File

@@ -1,40 +0,0 @@
var React = require('react/addons')
var ReactRouter = require('react-router')
var Route = ReactRouter.Route
var DefaultRoute = ReactRouter.DefaultRoute
var MainContainer = require('./Containers/MainContainer')
var LoginContainer = require('./Containers/LoginContainer')
var SignupContainer = require('./Containers/SignupContainer')
var HomeContainer = require('./Containers/HomeContainer')
var UserContainer = require('./Containers/UserContainer')
var PlanetContainer = require('./Containers/PlanetContainer')
var routes = (
<Route path='/' handler={MainContainer}>
<DefaultRoute name='root'/>
<Route name='login' path='login' handler={LoginContainer}/>
<Route name='signup' path='signup' handler={SignupContainer}/>
<Route name='home' path='home' handler={HomeContainer}>
<DefaultRoute name='homeEmpty'/>
<Route name='user' path=':userName' handler={UserContainer}>
<DefaultRoute name='userHome'/>
<Route name='planet' path=':planetName' handler={PlanetContainer}>
<DefaultRoute name='planetHome'/>
<Route name='codes' path='codes/:localId'/>
<Route name='notes' path='notes/:localId'/>
</Route>
</Route>
</Route>
</Route>
)
ReactRouter.run(routes, ReactRouter.HashLocation, function (Root) {
React.render(<Root/>, document.getElementById('content'))
})

View File

@@ -1,2 +0,0 @@
require('../styles/main/index.styl')
require('react-select/dist/default.css')

View File

@@ -4,22 +4,27 @@
global-reset() global-reset()
@import '../shared/*' @import '../shared/*'
iptBgColor = #E6E6E6
iptFocusBorderColor = #369DCD
body body
font-family "Lato" font-family "Lato"
color textColor color textColor
font-size fontSize font-size fontSize
width 100%
height 100%
overflow hidden
.Finder .Finder
absolute top bottom left right absolute top bottom left right
.FinderInput .FinderInput
position absolute padding 11px
top 11px
left 11px
right 11px
margin 0 auto margin 0 auto
height 44px height 55px
box-sizing border-box box-sizing border-box
border-bottom solid 1px borderColor border-bottom solid 1px borderColor
background-color iptBgColor
z-index 200
input input
display block display block
width 100% width 100%
@@ -29,9 +34,9 @@ body
height 33px height 33px
border-radius 5px border-radius 5px
box-sizing border-box box-sizing border-box
border-radius 16.5px border-radius 5px
&:focus, &.focus &:focus, &.focus
border-color brandBorderColor border-color iptFocusBorderColor
outline none outline none
.FinderList .FinderList
absolute left bottom absolute left bottom
@@ -40,12 +45,16 @@ body
box-sizing border-box box-sizing border-box
width 250px width 250px
overflow-y auto overflow-y auto
z-index 0
&>ul>li &>ul>li
.articleItem .articleItem
padding 10px padding 10px
border solid 2px transparent border solid 2px transparent
box-sizing border-box box-sizing border-box
cursor pointer cursor pointer
white-space nowrap
overflow-x hidden
text-overflow ellipsis
.divider .divider
box-sizing border-box box-sizing border-box
border-bottom solid 1px borderColor border-bottom solid 1px borderColor
@@ -57,25 +66,54 @@ body
absolute right bottom absolute right bottom
top 55px top 55px
left 250px left 250px
box-shadow 0px 0px 10px 0 #CCC
z-index 100
.header .header
absolute top left right absolute top left right
height 44px height 55px
box-sizing border-box box-sizing border-box
padding 0 10px padding 0 10px
border-bottom solid 1px borderColor border-bottom solid 1px borderColor
line-height 44px line-height 55px
font-size 1.3em font-size 18px
white-space nowrap white-space nowrap
text-overflow ellipsis text-overflow ellipsis
overflow-x hidden overflow-x hidden
clearfix()
.left
float left
.right
float right
button
border-radius 16.5px
cursor pointer
height 33px
width 33px
border none
margin-right 5px
font-size 18px
color inactiveTextColor
background-color transparent
padding 0
.tooltip
tooltip()
&.clipboardBtn .tooltip
margin-left -160px
margin-top 25px
&:hover
color textColor
.tooltip
opacity 1
.content .content
.ace_editor, .marked position absolute
position absolute top 55px
top 49px padding 10px
left 5px bottom 0
right 5px left 0
bottom 5px right 0
box-sizing border-box box-sizing border-box
.marked overflow-y auto
.MarkdownPreview
marked() marked()
overflow-y auto .CodeEditor
absolute top bottom left right

View File

@@ -0,0 +1,329 @@
noTagsColor = #999
iptFocusBorderColor = #369DCD
.ArticleDetail
absolute right bottom
top 60px
left 450px
padding 10px
background-color #E6E6E6
border-top 1px solid borderColor
border-left 1px solid borderColor
*
-webkit-user-select all
.deleteConfirm
width 100%
height 70px
.right
float right
button
cursor pointer
height 33px
padding 0 10px
margin-left 5px
font-size 14px
color inactiveTextColor
background-color darken(white, 5%)
border solid 1px borderColor
border-radius 5px
&:hover
background-color white
&.primary
border none
background-color brandColor
color white
&:hover
color white
background-color lighten(brandColor, 10%)
.detailInfo
height 70px
width 100%
font-size 12px
position relative
.left
absolute top left bottom
right 120px
.folderName
display inline-block
max-width 100px
overflow ellipsis
height 10px
.right
absolute top right
.detailBody
absolute left right bottom
top 70px
overflow-x hidden
overflow-y auto
.detailPanel
absolute top
left 10px
right 10px
bottom 10px
background-color white
border-radius 5px
border solid 1px borderColor
&>.header
absolute top left right
height 60px
.MarkdownPreview
absolute left right bottom
top 60px
marked()
box-sizing border-box
padding 5px 15px
border-top solid 1px borderColor
overflow-y auto
.CodeEditor
absolute left right bottom
top 60px
border-top solid 1px borderColor
min-height 300px
border-bottom-left-radius 5px
border-bottom-right-radius 5px
&.edit
.detailInfo
.left
&>.tutorial
position fixed
z-index 35
font-style italic
.folder
border none
width 150px
height 27px
outline none
background-color darken(white, 5%)
&:hover
background-color white
.TagSelect
.tags
white-space nowrap
overflow-x auto
position relative
max-width 350px
margin-top 5px
noSelect()
z-index 30
background-color #E6E6E6
.tagItem
background-color brandColor
border-radius 2px
color white
margin 0 2px
padding 0
border 1px solid darken(brandColor, 10%)
button.tagRemoveBtn
color white
border-radius 2px
border none
background-color transparent
padding 4px 2px
border-right 1px solid #E6E6E6
font-size 8px
line-height 12px
transition 0.1s
&:hover
background-color lighten(brandColor, 10%)
.tagLabel
padding 4px 4px
font-size 12px
line-height 12px
input.tagInput
background-color transparent
outline none
margin 0 2px
border-radius 2px
border none
transition 0.1s
height 18px
.suggestTags
position fixed
width 150px
max-height 150px
background-color white
z-index 5
border 1px solid borderColor
border-radius 5px
button
width 100%
display block
padding 0 15px
height 33px
line-height 33px
background-color transparent
border none
text-align left
font-size 14px
&:hover
background-color darken(white, 10%)
.right
button
cursor pointer
height 33px
width 55px
margin-left 5px
font-size 14px
color inactiveTextColor
background-color darken(white, 5%)
border solid 1px borderColor
border-radius 5px
&.preview
width inherit
&:hover
background-color white
&.primary
border none
background-color brandColor
color white
&:hover
color white
background-color lighten(brandColor, 10%)
.detailBody
.detailPanel
&>.header
&>.tutorial
fixed right
z-index 35
font-style italic
.mode
position relative
z-index 30
absolute top bottom right
display block
height 33px
margin-top 12px
width 150px
margin-right 15px
border-radius 5px
border solid 1px borderColor
transition 0.1s
&.idle
background-color darken(white, 5%)
cursor pointer
&:hover
background-color white
.ModeIcon
float left
width 25px
line-height 33px
text-align center
.modeLabel
line-height 30px
&.edit
background-color white
input
width 150px
line-height 30px
padding 0 10px
border none
outline none
background-color transparent
font-size 14px
.modeOptions
position fixed
width 150px
z-index 10
margin-top 5px
border 1px solid borderColor
border-radius 5px
background-color white
max-height 250px
overflow-y auto
.option
height 33px
line-height 33px
cursor pointer
&.active, &:hover.active
background-color brandColor
color white
.ModeIcon
width 30px
text-align center
display inline-block
&:hover
background-color darken(white, 10%)
.title
absolute left top bottom
right 150px
padding 0 15px
input
width 100%
border none
background-color transparent
line-height 60px
font-size 24px
outline none
&.idle
.detailInfo
&>.tutorial
fixed top right
z-index 35
font-style italic
.left
right 99px
.info
padding 5px
overflow ellipsis
.tags
padding 10px 10px 5px
color articleItemColor
a
background-color brandColor
color white
border-radius 2px
padding 1.5px 5px
margin 2px
font-size 10px
opacity 0.8
cursor pointer
&:hover
opacity 1
span.noTags
color noTagsColor
.right
z-index 30
button
border-radius 16.5px
cursor pointer
height 33px
width 33px
border none
margin-right 5px
font-size 18px
color inactiveTextColor
background-color darken(white, 5%)
padding 0
.tooltip
tooltip()
&.editBtn .tooltip
margin-top 25px
margin-left -45px
&.deleteBtn .tooltip
margin-top 25px
margin-left -73px
&:hover
color textColor
.tooltip
opacity 1
.detailBody
.detailPanel
&>.header
.mode
display block
line-height 60px
width 45px
height 60px
font-size 18px
text-align center
.title
absolute top bottom
left 45px
right 15px
font-size 24px
line-height 60px
white-space nowrap
overflow-x auto
overflow-y hidden

View File

@@ -0,0 +1,74 @@
articleItemHoverBgColor = darken(white, 5%)
articleItemColor = #777
.ArticleList
absolute bottom
top 60px
left 200px
width 250px
border-top 1px solid borderColor
border-right 1px solid borderColor
overflow-y auto
noSelect()
&>div
.articleItem
border solid 2px transparent
position relative
height 88px
width 100%
cursor pointer
transition 0.1s
background-color white
padding 0 10px
font-size 12px
.top
clearfix()
line-height 20px
padding 5px 0
color articleItemColor
.folderName
overflow ellipsis
display inline-block
width 120px
.updatedAt
float right
line-height 20px
.middle
padding 3px 0 7px
font-size 16px
position relative
height 26px
.mode
position absolute
left 0
font-size 12px
line-height 16px
.title
position absolute
left 19px
right 0
overflow ellipsis
.bottom
padding 5px 0
overflow-x auto
white-space nowrap
.tags
color articleItemColor
a
background-color brandColor
color white
border-radius 2px
padding 1.5px 5px
margin 2px
font-size 10px
opacity 0.8
&:hover
opacity 1
&:hover, &.hover
background-color articleItemHoverBgColor
&:active, &.active
background-color white
&:active, &.active
border-color brandBorderColor
.divider
border-bottom solid 1px borderColor

View File

@@ -0,0 +1,170 @@
articleNavBgColor = #353535
articleCount = #999
.ArticleNavigator
background-color articleNavBgColor
absolute top bottom left
width 200px
border-right 1px solid borderColor
color white
.userInfo
height 60px
display block
border-bottom 1px solid borderColor
.userProfileName
color brandColor
font-size 28px
padding 6px 0 0 10px
white-space nowrap
text-overflow ellipsis
overflow hidden
.userName
color white
padding-left 20px
margin-top 3px
.tutorial
position fixed
z-index 35
top 0
left 0
pointer-event none
font-style italic
transition 0.1s
&.hide
opacity 0
.settingBtn
width 22px
height 22px
line-height 22px
border-radius 11px
position absolute
top 19px
right 14px
color white
padding 0
background-color transparent
border 1px solid white
z-index 31
.tooltip
tooltip()
margin-top -5px
margin-left 10px
&:hover
.tooltip
opacity 1
&:active
background-color brandColor
border-color brandColor
.controlSection
height 88px
padding 22px 15px
margin-bottom 44px
.tutorial
fixed top left
z-index 35
pointer-event none
font-style italic
transition 0.1s
&.hide
opacity 0
.newPostBtn
position relative
border none
background-color brandColor
color white
height 44px
width 170px
border-radius 5px
font-size 20px
transition 0.1s
z-index 30
.tooltip
tooltip()
margin-left 48px
margin-top -3px
&:hover
background-color lighten(brandColor, 7%)
.tooltip
opacity 1
.folders, .members
.header
border-bottom 1px solid borderColor
padding-bottom 5px
margin-bottom 10px
clearfix()
position relative
z-index 30
.title
float left
padding-left 10px
font-size 18px
line-height 22px
.addBtn
float right
margin-right 15px
width 22px
height 22px
font-size 10px
padding 0
line-height 22px
border 1px solid white
border-radius 11px
background-color transparent
color white
padding 0
font-weight bold
.tooltip
tooltip()
margin-top -6px
margin-left 11px
&:hover
.tooltip
opacity 1
&:active
background-color brandColor
border-color brandColor
.folders
absolute bottom
top 200px
width 100%
.header
.tutorial
position fixed
z-index 35px
top 200px
font-style italic
.folderList
absolute bottom
top 38px
overflow-y auto
.folderList button
height 33px
width 199px
border none
text-align left
font-size 14px
background-color transparent
color white
padding-left 15px
overflow ellipsis
&:hover
background-color transparentify(white, 5%)
&.active, &:active
background-color transparentify(lighten(brandColor, 25%), 70%)
.articleCount
color articleCount
font-size 12px
.members
.memberList>div
height 33px
width 200px
margin-bottom 5px
padding-left 15px
.memberImage
float left
margin-top 5.5px
border-radius 11px
.memberProfileName
float left
line-height 33px
margin-left 7px

View File

@@ -0,0 +1,156 @@
bgColor = #E6E6E6
inputBgColor = white
iptFocusBorderColor = #369DCD
topBarBtnColor = #B3B3B3
topBarBtnBgColor = #B3B3B3
topBarBtnBgActiveColor = #3A3A3A
infoBtnColor = bgColor
infoBtnBgColor = #B3B3B3
infoBtnActiveBgColor = #3A3A3A
.ArticleTopBar
absolute top right
left 200px
height 60px
background-color bgColor
&>.tutorial
.clickJammer
fixed top left bottom right
z-index 40
background transparent
.global
fixed bottom right
height 100px
z-index 35
font-style italic
.finder
fixed bottom right
height 250px
left 50%
margin-left -250px
z-index 35
font-style italic
.back
fixed top left bottom right
z-index 20
background-color transparentify(black, 80%)
&>.left
float left
&>.tutorial
fixed top
left 200px
z-index 36
font-style italic
&>.search
position relative
float left
height 33px
margin-top 13.5px
margin-left 15px
width 350px
padding 5px 15px
transition 0.1s
font-size 16px
border 1px solid transparent
z-index 30
.tooltip
tooltip()
margin-left -24px
margin-top 35px
opacity 1
&.hide
opacity 0
input
absolute top left
width 350px
border-radius 16.5px
background-color inputBgColor
border 1px solid transparent
padding-left 35px
outline none
font-size 14px
height 33px
line-height 33px
z-index 0
&:focus
border-color iptFocusBorderColor
i.fa.fa-search
position absolute
display block
top 0
left 10px
line-height 33px
z-index 1
pointer-events none
.searchClearBtn
position absolute
top 6px
right 10px
width 20px
height 20px
border-radius 10px
border none
background-color transparent
color topBarBtnColor
transition 0.1s
line-height 20px
text-align center
padding 0
&:hover
color white
background-color topBarBtnBgColor
&>.refreshBtn
float left
width 33px
height 33px
margin-top 13.5px
margin-left 15px
border none
color refreshBtColor
background transparent
font-size 18px
line-height 18px
transition 0.1s
&:hover
color refreshBtnActiveColor
&>.right
float right
&>button
display block
position absolute
right 74px
top 20px
width 20px
height 20px
font-size 14px
line-height 14px
background-color infoBtnBgColor
color bgColor
border-radius 11px
border none
transition 0.1s
.tooltip
tooltip()
margin-left -50px
margin-top 29px
&:hover
background-color infoBtnActiveBgColor
.tooltip
opacity 1
&>.logo
display block
position absolute
top 8px
right 15px
opacity 0.7
.tooltip
tooltip()
margin-top 44px
margin-left -120px
&:hover
opacity 1
.tooltip
opacity 1

View File

@@ -0,0 +1,86 @@
userNavigatorBgColor = #1B1C1C
userNavigatorColor = #DDD
userAnchorColor = #979797
userAnchorBgColor = #BEBEBE
userAnchorActiveColor = textColor
userAnchorActiveBgColor = white
.UserNavigator
noSelect()
background-color userNavigatorBgColor
absolute left top bottom
width 60px
text-align center
box-sizing border-box
ul.userList
position absolute
top 25px
left 0
right 0
bottom 70px
// overflow-y auto
&>li
a
display block
width 38px
height 64px
margin 0 auto 10px
text-align center
text-decoration none
color userAnchorColor
line-height 44px
font-size 1.1em
cursor pointer
transition 0.1s
img.ProfileImage
width 38px
height 38px
border-radius 22px
opacity 0.7
&:hover
img.ProfileImage
opacity 1
.userTooltip
opacity 1
&.active
img.ProfileImage
opacity 1
.userTooltip
tooltip()
position absolute
margin-top -52px
margin-left 44px
.keyLabel
margin-top -25px
font-size 0.8em
color userNavigatorColor
button.createTeamBtn
display block
margin 0 auto
width 30px
height 30px
border-radius 15px
border 2px solid darken(white, 5%)
color darken(white, 5%)
text-align center
background-image none
background-color transparent
box-sizing border-box
absolute left right
bottom 15px
font-size 22px
line-height 22px
transition 0.1s
.tooltip
tooltip()
margin-top -26px
margin-left 30px
&:hover, &.hover, &:focus, &.focus
color white
border-color white
.tooltip
opacity 1
&:active
background-color brandColor
border-color brandColor

View File

@@ -0,0 +1,12 @@
@require './components/UserNavigator'
@require './components/ArticleNavigator'
@require './components/ArticleTopBar'
@require './components/ArticleList'
@require './components/ArticleDetail'
@require './lib/modal'
@require './lib/CreateNewTeam'
@require './lib/CreateNewFolder'
@require './lib/Preferences'
@require './lib/Tutorial'
@require './lib/EditedAlert'

View File

@@ -0,0 +1,91 @@
tabNavColor = #999999
iptFocusBorderColor = #369DCD
.CreateNewFolder.modal
width 600px
height 450px
.closeBtn
position absolute
top 15px
right 15px
width 33px
height 33px
font-size 18px
line-height 33px
padding 0
text-align center
background-color transparent
border none
color stripBtnColor
&:hover
color stripHoverBtnColor
.title
font-size 32px
text-align center
font-weight bold
margin-top 25px
.ipt
display block
width 330px
font-size 14px
height 44px
line-height 44px
padding 0 15px
border-radius 5px
border solid 1px borderColor
outline none
margin 75px auto 20px
&:focus
border-color iptFocusBorderColor
.colorSelect
text-align center
.option
cursor pointer
font-size 22px
height 48px
width 48px
margin 0 2px
border 1px solid transparent
border-radius 5px
overflow hidden
line-height 45px
text-align center
transition 0.1s
display inline-block
&:hover
border-color borderColor
font-size 28px
&.active
font-size 28px
border-color iptFocusBorderColor
.alert
color infoTextColor
background-color infoBackgroundColor
font-size 14px
padding 15px 15px
width 330px
border-radius 5px
margin 15px auto 0
&.error
color errorTextColor
background-color errorBackgroundColor
.confirmBtn
display block
position absolute
left 180px
bottom 44px
width 240px
font-size 24px
height 44px
line-height 24px
font-weight bold
background-color brandColor
color white
border none
border-radius 5px
margin 0 auto
transition 0.1s
&:hover
transform scale(1.1)
&:disabled
opacity 0.7

View File

@@ -0,0 +1,199 @@
tabNavColor = #999999
iptFocusBorderColor = #369DCD
stripHoverBtnColor = #333
stripBtnColor = lighten(stripHoverBtnColor, 35%)
.CreateNewTeam.modal
width 600px
height 450px
.closeBtn
position absolute
top 15px
right 15px
width 33px
height 33px
font-size 18px
line-height 33px
padding 0
text-align center
background-color transparent
border none
color stripBtnColor
&:hover
color stripHoverBtnColor
.title
font-size 32px
text-align center
font-weight bold
margin-top 25px
.ipt
display block
width 330px
font-size 14px
height 44px
line-height 44px
padding 0 15px
border-radius 5px
border solid 1px borderColor
outline none
&:focus
border-color iptFocusBorderColor
.alert
padding 0 15px
height 44px
line-height 44px
width 300px
margin 0 auto
border-radius 5px
color infoTextColor
background-color infoBackgroundColor
white-space nowrap
overflow-x auto
&.error
color errorTextColor
background-color errorBackgroundColor
.confirmBtn
display block
position absolute
left 180px
bottom 44px
width 240px
font-size 24px
height 44px
line-height 24px
font-weight bold
background-color brandColor
color white
border none
border-radius 5px
margin 0 auto
transition 0.1s
&:hover
transform scale(1.1)
&:disabled
opacity 0.7
.tabNav
absolute left right
bottom 15px
height 33px
line-height 33px
width 150px
text-align center
font-size 12px
color tabNavColor
margin 0 auto
transition 0.1s
i.active
color brandColor
.createTab
.ipt
margin 105px auto 15px
.selectTab
.memberForm
display block
margin 25px auto 15px
width 330px
clearfix()
padding 0
font-size 14px
height 44px
line-height 44px
outline none
.Select.memberName
display block
margin 0
float left
width 280px
height 44px
font-size 14px
border none
line-height 44px
background-color transparent
outline none
&.is-focus
.Select-control
border-color iptFocusBorderColor
.Select-control
height 44px
line-height 44px
padding 0 0 0 15px
border-radius 5px 0 0 5px
border 1px solid borderColor
border-right none
.Select-placeholder
padding 0 0 0 15px
.Seleect-arrow
top 21px
.Select-clear
padding 0 10px
.Select-noresults, .Select-option
line-height 44px
padding 0 0 0 15px
&:focus, &.focus
border-color iptFocusBorderColor
button
font-weight 400
height 44px
cursor pointer
margin 0
padding 0
width 50px
float right
border none
background-color brandColor
border-top-right-radius 5px
border-bottom-right-radius 5px
color white
font-size 14px
.memberList
width 480px
margin 0 auto
height 190px
overflow scroll
border-bottom 1px solid borderColor
&>li
border-bottom 1px solid borderColor
height 44px
padding 0 25px
clearfix()
&:nth-last-child(1)
border-bottom-color transparent
.userPhoto
width 30px
height 30px
float left
margin-top 7px
margin-right 15px
border-radius 15px
.userInfo
float left
margin-top 7px
.userName
font-size 16px
margin-bottom 2px
.userEmail
font-size 12px
.userControl
float right
.userRole
float left
height 30px
background-color transparent
border 1px solid transparent
margin-top 7px
margin-right 35px
outline none
cursor pointer
&:hover
border-color borderColor
&:focus
border-color iptFocusBorderColor
button
border none
height 30px
margin-top 7px
background-color transparent
color stripBtnColor
&:hover
color stripHoverBtnColor

View File

@@ -0,0 +1,28 @@
.EditedAlert.modal
width 350px
top 100px
.title
font-size 24px
margin-bottom 15px
.message
font-size 14px
margin-bottom 15px
.control
text-align right
button
border-radius 5px
height 33px
padding 0 15px
font-size 14px
background-color white
border 1px solid borderColor
border-radius 5px
margin-left 5px
&:hover
background-color darken(white, 10%)
&.primary
border-color brandColor
background-color brandColor
color white
&:hover
background-color lighten(brandColor, 10%)

View File

@@ -0,0 +1,633 @@
menuColor = #808080
menuBgColor = #E6E6E6
closeBtnBgColor = #1790C6
iptFocusBorderColor = #369DCD
.Preferences.modal
padding 0
border-radius 5px
overflow hidden
width 720px
height 450px
&>.header
absolute top left right
height 50px
border-bottom 1px solid borderColor
background-color menuBgColor
&>.title
font-size 22px
font-weight bold
float left
padding-left 30px
line-height 50px
&>.closeBtn
float right
font-size 14px
background-color closeBtnBgColor
color white
padding 0 15px
height 33px
margin-top 9px
margin-right 15px
border none
border-radius 5px
&:hover
background-color lighten(closeBtnBgColor, 10%)
&>.nav
absolute left bottom
top 50px
width 180px
background-color menuBgColor
border-right 1px solid borderColor
&>button
width 100%
height 44px
font-size 18px
color menuColor
border none
background-color transparent
transition 0.1s
text-align left
padding-left 15px
&:hover
background-color darken(menuBgColor, 10%)
&.active, &:active
background-color brandColor
color white
&>.content
absolute right bottom
top 50px
left 180px
overflow-y auto
&>.section
padding 10px
border-bottom 1px solid borderColor
overflow-y auto
&:nth-last-child(1)
border-bottom none
&>.sectionTitle
font-size 18px
margin 10px 0 5px
color brandColor
&>.sectionInput
height 33px
margin-bottom 5px
clearfix()
label
width 180px
padding-left 15px
float left
line-height 33px
input
width 300px
float left
height 33px
border-radius 5px
border 1px solid borderColor
padding 0 10px
font-size 14px
outline none
&:focus
border-color iptFocusBorderColor
&>.sectionConfirm
clearfix()
padding 5px 15px
button
float right
background-color brandColor
color white
border none
border-radius 5px
height 33px
padding 0 15px
font-size 14px
&:hover
background-color lighten(brandColor, 10%)
.alert
float right
width 250px
padding 10px 15px
margin 0 10px 0
.alert
color infoTextColor
background-color infoBackgroundColor
font-size 14px
padding 15px 15px
width 330px
border-radius 5px
margin 10px auto
&.error
color errorTextColor
background-color errorBackgroundColor
&.ContactTab
&.done
.message
margin-top 75px
margin-bottom 15px
text-align center
font-size 22px
.checkIcon
margin-bottom 15px
font-size 144px
color brandColor
text-align center
.control
text-align center
button
border solid 1px borderColor
border-radius 5px
background-color white
padding 15px 15px
font-size 14px
&:hover
background-color darken(white, 10%)
&.form
padding 10px
.title
font-size 18px
color brandColor
margin-top 10px
margin-bottom 10px
.description
margin-bottom 15px
.iptGroup
margin-bottom 10px
input, textarea
border-radius 5px
border 1px solid borderColor
font-size 14px
outline none
padding 10px 15px
width 100%
&:focus
border-color iptFocusBorderColor
textarea
resize vertical
min-height 150px
.formControl
clearfix()
.alert
float right
padding 10px 15px
margin 0 5px 0
font-size 14px
line-height normal
button
padding 10px 15px
background-color brandColor
color white
font-size 14px
border-radius 5px
border none
float right
&:hover
background-color lighten(brandColor, 10%)
&.AppSettingTab
.description
marked()
&.TeamSettingTab
.header
border-bottom 1px solid borderColor
padding 10px
font-size 18px
color brandColor
line-height 33px
.teamSelect
border 1px solid borderColor
height 33px
width 200px
margin 0 10px
outline none
font-size 14px
&:focus
border-color iptFocusBorderColor
.teamDeleteConfirm
label
line-height 33px
font-size 14px
.teamDelete
label
line-height 33px
font-size 18px
color brandColor
.teamDelete, .teamDeleteConfirm
padding 15px 20px 15px 15px
button
background-color white
height 33px
font-size 14px
padding 0 15px
border 1px solid borderColor
float right
margin 0 5px
border-radius 5px
&:hover
background-color darken(white, 10%)
button.deleteBtn
background-color brandColor
border none
color white
&:hover
background-color lighten(brandColor, 10%)
&.MemberSettingTab
&>.header
border-bottom 1px solid borderColor
padding 10px
font-size 18px
color brandColor
line-height 33px
.teamSelect
border 1px solid borderColor
height 33px
width 200px
margin 0 10px
outline none
font-size 14px
&:focus
border-color iptFocusBorderColor
.membersTableSection
.addMember
clearfix()
padding 10px
.addMemberLabel
font-size 14px
line-height 33px
float left
.addMemberControl
width 330px
float left
margin-left 25px
.Select
display block
margin 0
float left
width 280px
height 33px
font-size 14px
border none
line-height 33px
background-color transparent
outline none
&.is-focus
.Select-control
border-color iptFocusBorderColor
.Select-control
height 33px
line-height 33px
padding 0 0 0 15px
border-radius 5px 0 0 5px
border 1px solid borderColor
border-right none
.Select-placeholder
padding 0 0 0 15px
.Seleect-arrow
top 21px
.Select-clear
padding 0 10px
.Select-noresults, .Select-option
line-height 33px
padding 0 0 0 15px
button
font-weight 400
height 33px
cursor pointer
margin 0
padding 0
width 50px
float right
border none
background-color brandColor
border-top-right-radius 5px
border-bottom-right-radius 5px
color white
font-size 14px
.memberList
&>.header
clearfix()
&>.userName
float left
&>.role
float left
&>.control
float right
&>li
&.edit
.colDescription
font-size 14px
line-height 33px
padding-left 15px
float left
strong
font-size 16px
color brandColor
.colDeleteConfirm
float right
margin-right 15px
button
border none
height 30px
width 60px
margin-top 1.5px
font-size 14px
background-color transparent
color stripBtnColor
&:hover
color stripHoverBtnColor
&:disabled
color lighten(stripBtnColor, 10%)
cursor not-allowed
&.primary
color brandColor
&:hover
color lighten(brandColor, 10%)
border-bottom 1px solid borderColor
height 44px
padding 0 25px
width 420px
margin 0 auto
clearfix()
&:nth-last-child(1)
border-bottom-color transparent
.colUserName
float left
width 250px
clearfix()
.userPhoto
width 30px
height 30px
float left
margin-top 7px
margin-right 15px
border-radius 15px
.userInfo
float left
margin-top 7px
width 205px
.userName
font-size 16px
margin-bottom 2px
overflow ellipsis
.userEmail
font-size 12px
overflow ellipsis
.colRole
float left
width 75px
.userRole
height 30px
background-color transparent
border 1px solid transparent
margin-top 7px
margin-right 35px
outline none
cursor pointer
&:hover
border-color borderColor
&:focus
border-color iptFocusBorderColor
&:disabled
border-color transparent
cursor not-allowed
.colDelete
width 45px
float right
text-align center
button.deleteButton
border none
height 30px
width 30px
margin-top 7px
background-color transparent
color stripBtnColor
&:hover
color stripHoverBtnColor
&:disabled
color lighten(stripBtnColor, 10%)
cursor not-allowed
&.header
.colRole, .colDelete
text-align center
.colUserName, .colRole, .colDelete
line-height 44px
&.FolderSettingTab
&>.header
border-bottom 1px solid borderColor
padding 10px
font-size 18px
color brandColor
line-height 33px
.teamSelect
border 1px solid borderColor
height 33px
width 200px
margin 0 10px
outline none
font-size 14px
&:focus
border-color iptFocusBorderColor
.section
.folderTable
width 420px
margin 15px auto
&>div
border-bottom 1px solid borderColor
clearfix()
height 43px
line-height 33px
padding 5px 0
&:last-child
border-color transparent
.folderColor
float left
margin-left 10px
text-align center
width 44px
.folderName
float left
width 175px
overflow ellipsis
.folderControl
float right
width 125px
text-align center
&.folderHeader
.folderName
padding-left 25px
&.newFolder
.alert
display block
color infoTextColor
background-color infoBackgroundColor
font-size 14px
padding 15px 15px
width 330px
border-radius 5px
margin 0 auto
&.error
color errorTextColor
background-color errorBackgroundColor
.folderName input
height 33px
border 1px solid transparent
border-radius 5px
padding 0 10px
font-size 14px
outline none
width 150px
overflow ellipsis
&:hover
border-color borderColor
&:focus
border-color iptFocusBorderColor
.folderPublic select
height 33px
border 1px solid transparent
background-color white
outline none
display block
margin 0 auto
font-size 14px
&:hover
border-color borderColor
&:focus
border-color iptFocusBorderColor
.folderControl
button
border none
height 30px
margin-top 1.5px
font-size 14px
background-color transparent
color brandColor
&:hover
color lighten(brandColor, 10%)
&.FolderRow
.sortBtns
float left
display block
height 30px
width 30px
margin-top 1.5px
position absolute
button
absolute left
background-color transparent
border none
height 15px
padding 0
margin 0
color stripBtnColor
&:first-child
top 0
&:last-child
top 15px
&:hover
color stripHoverBtnColor
&:disabled
color lighten(stripBtnColor, 10%)
cursor not-allowed
.folderName input
height 33px
border 1px solid borderColor
border-radius 5px
padding 0 10px
font-size 14px
outline none
width 150px
&:focus
border-color iptFocusBorderColor
.folderColor
.select
height 33px
width 33px
border 1px solid borderColor
background-color white
outline none
display block
margin 0 auto
font-size 14px
border-radius 5px
&:focus
border-color iptFocusBorderColor
.options
position absolute
background-color white
text-align left
border 1px solid borderColor
border-radius 5px
padding 0 5px 5px
margin-left 5px
margin-top -34px
clearfix()
.label
margin-left 5px
line-height 22px
font-size 12px
button
float left
border none
width 33px
height 33px
margin-right 5px
border 1px solid transparent
line-height 29px
overflow hidden
border-radius 5px
background-color transparent
outline none
transition 0.1s
&:hover
border-color borderColor
&.active
border-color iptFocusBorderColor
.FolderMark
transform scale(1.4)
.folderControl
button
border none
height 30px
width 30px
margin-top 1.5px
font-size 14px
background-color transparent
color stripBtnColor
&:hover
color stripHoverBtnColor
&:disabled
color lighten(stripBtnColor, 10%)
cursor not-allowed
&.edit
.folderControl
button
width 60px
&.primary
color brandColor
&:hover
color lighten(brandColor, 10%)
&.delete
.folderDeleteLabel
float left
height 33px
width 250px
padding-left 15px
overflow ellipsis
strong
font-size 16px
color brandColor
.folderControl
button
width 60px
&.primary
color brandColor
&:hover
color lighten(brandColor, 10%)

View File

@@ -0,0 +1,132 @@
slideBgColor0 = #2BAC8F
slideBgColor1 = #F68F92
slideBgColor2 = #D6AD56
slideBgColor3 = #26969B
slideBgColor4 = #00B493
.Tutorial.modal
background-color slideBgColor0
color white
width 720px
height 480px
margin-top 75px
border-radius 5px
overflow hidden
.priorBtn, .nextBtn
font-size 72px
position absolute
background-color transparent
color transparentify(white, 50%)
transition 0.1s
border none
line-height 72px
padding 0
width 93px
height 72px
z-index 2
top 189px
&:hover
color white
&.hide
opacity 0
.priorBtn
left 15px
.nextBtn
right 15px
.title
text-align center
font-size 54px
margin 40px 0
.content
text-align center
font-size 22px
line-height 1.8
.dots
position absolute
left 0
right 0
bottom 25px
margin 0 auto
color gray
text-align center
z-index 2
&>i
transition 0.3s
&.active
color white
.slide
absolute top bottom left right
z-index 1
.slide0
background-color slideBgColor0
.content
margin-top 100px
.slide1
background-color slideBgColor1
.content
.markdown
background-color white
color textColor
width 480px
height 140px
margin 45px auto 0
clearfix()
text-align left
border-radius 5px
overflow hidden
.left
float left
width 240px
height 140px
box-sizing border-box
font-size 0.5em
padding 30px
border-right 1px solid borderColor
.right
width 240px
height 140px
float right
box-sizing border-box
padding: 28px 0 0 10px
font-size 0.45em
marked()
ul
padding-left 20px
.slide2
background-color slideBgColor2
.code
border-radius 5px
overflow hidden
text-align left
width 480px
heght 140px
margin 45px auto 0
font-size 14px
.ace_editor
height 140px
.slide3
background-color slideBgColor3
.title
margin-bottom 15px
.content
font-size 18px
&>img
margin-top 25px
.slide4
background-color slideBgColor4
.content
&>button
background-color white
color brandColor
font-size 60px
width 250px
height 250px
border-radius 125px
border none
transition 0.1s
&:hover
transform scale(1.2)

View File

@@ -0,0 +1,21 @@
modalZIndex= 1000
modalBackColor = transparentify(black, 65%)
.ModalBase
fixed top left bottom right
z-index modalZIndex
&.hide
display none
.modalBack
absolute top left bottom right
background-color modalBackColor
z-index modalZIndex + 1
.modal
position relative
width 650px
margin 50px auto 0
z-index modalZIndex + 2
background-color white
padding 15px
color #666666
border-radius 5px

View File

@@ -23,7 +23,7 @@
transition: all 200ms ease; transition: all 200ms ease;
} }
.Select-control:hover { .Select-control:hover {
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); // box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
} }
.is-searchable.is-open > .Select-control { .is-searchable.is-open > .Select-control {
cursor: text; cursor: text;
@@ -42,8 +42,8 @@
cursor: text; cursor: text;
} }
.is-focused:not(.is-open) > .Select-control { .is-focused:not(.is-open) > .Select-control {
border-color: #0088cc #0099e6 #0099e6; // border-color: #0088cc #0099e6 #0099e6;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 5px -1px rgba(0, 136, 204, 0.5); // box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 5px -1px rgba(0, 136, 204, 0.5);
} }
.Select-placeholder { .Select-placeholder {
color: #aaaaaa; color: #aaaaaa;
@@ -141,8 +141,8 @@
cursor: pointer; cursor: pointer;
} }
.Select-menu-outer { .Select-menu-outer {
border-bottom-right-radius: 4px; border-bottom-right-radius: 5px;
border-bottom-left-radius: 4px; border-bottom-left-radius: 5px;
background-color: #ffffff; background-color: #ffffff;
border: 1px solid #cccccc; border: 1px solid #cccccc;
border-top-color: #e6e6e6; border-top-color: #e6e6e6;
@@ -168,8 +168,8 @@
padding: 8px 10px; padding: 8px 10px;
} }
.Select-option:last-child { .Select-option:last-child {
border-bottom-right-radius: 4px; border-bottom-right-radius: 5px;
border-bottom-left-radius: 4px; border-bottom-left-radius: 5px;
} }
.Select-option.is-focused { .Select-option.is-focused {
background-color: #f2f9fc; background-color: #f2f9fc;
@@ -196,10 +196,10 @@
padding: 3px 0; padding: 3px 0;
} }
.Select-item { .Select-item {
background-color: #f2f9fc; background-color: brandColor;
border-radius: 2px; border-radius: 2px;
border: 1px solid #c9e6f2; // border: 1px solid #c9e6f2;
color: #0088cc; color: white;
display: inline-block; display: inline-block;
font-size: 1em; font-size: 1em;
margin: 2px; margin: 2px;
@@ -216,20 +216,19 @@
padding: 3px 5px; padding: 3px 5px;
} }
.Select-item-label .Select-item-label__a { .Select-item-label .Select-item-label__a {
color: #0088cc; color: white;
cursor: pointer; cursor: white;
} }
.Select-item-icon { .Select-item-icon {
cursor: pointer; cursor: pointer;
border-bottom-left-radius: 2px; border-bottom-left-radius: 2px;
border-top-left-radius: 2px; border-top-left-radius: 2px;
border-right: 1px solid #c9e6f2; border-right: 1px solid darken(brandColor, 10%)
padding: 2px 5px 4px; padding: 2px 5px 4px;
} }
.Select-item-icon:hover, .Select-item-icon:hover,
.Select-item-icon:focus { .Select-item-icon:focus {
background-color: #ddeff7; background-color: lighten(brandColor, 10%)
color: #0077b3;
} }
.Select-item-icon:active { .Select-item-icon:active {
background-color: #c9e6f2; background-color: #c9e6f2;

View File

@@ -1,6 +1,6 @@
.LoginContainer, .SignupContainer .LoginContainer, .SignupContainer
margin 0 auto margin 0 auto
padding 25px 15px padding 105px 15px
box-sizing border-box box-sizing border-box
color inactiveTextColor color inactiveTextColor
.logo .logo
@@ -58,17 +58,24 @@
.alertInfo, .alertError .alertInfo, .alertError
margin-top 15px margin-top 15px
margin-bottom 15px margin-bottom 15px
height 44px padding 10px
padding 5px border-radius 5px
border-radius 10px line-height 1.6
line-height 44px
text-align center text-align center
.alertInfo .alertInfo
alertInfo() alertInfo()
.alertError .alertError
alertError() alertError()
div.form-group:last-child div.formField
margin-top 15px input
stripInput()
height 33px
width 100%
margin-bottom 10px
text-align center
font-size 1.1em
&:last-child
margin-top 15px
button.logInButton button.logInButton
btnPrimary() btnPrimary()
height 44px height 44px

View File

@@ -1,307 +1,123 @@
.HomeContainer userNavigatorWidth = 200px
.HomeNavigator userNavigatorBgColor = #333
noSelect() userNavigatorColor = #DDD
background-color planetNavBgColor userNavigatorProfileNameColor = brandColor
absolute left top bottom userNavigatorBorderColor = #666
width 55px
text-align center
box-sizing border-box
border-right solid 1px borderColor
.profileButton
display block
width 55px
height 55px
border-bottom solid 1px borderColor
overflow hidden
background-color black
margin 0
padding 0
cursor pointer
box-sizing border-box
border none
img
transition 0.1s
opacity 0.9
&.vivid.active, &.focus, &:focus, &.hover, &:hover
img
opacity 1
.profilePopup
position fixed
left 35px
top 35px
z-index popupZIndex
width 200px
background-color backgroundColor
box-shadow popupShadow
border-radius 10px
padding 10px 0 0px
&.close
display none
.profileGroup
margin-bottom 10px
.profileGroupLabel
text-align left
height 1em
padding 0 15px
span
position absolute
z-index 2
background-color backgroundColor
padding-right 5px
color inactiveTextColor
font-size 0.8em
&::before
content ''
position absolute
display block
z-index 1
height 0.5em
width 175px
border-bottom solid 1px borderColor
.profileGroupList
li
clearfix()
&:hover
background-color hoverBackgroundColor
.userName
width 155px
padding 10px 15px
text-align left
display block
text-decoration none
cursor pointer
.createNewTeam
btnStripDefault()
width 100%
padding 10px 20px
font-size 1em
cursor pointer
text-align left
.controlGroup
list-style none
border-top solid 1px borderColor
padding 10px 0
li
&:hover
background-color hoverBackgroundColor
button
btnStripDefault()
width 100%
padding 10px 20px
font-size 1em
cursor pointer
text-align left
ul.planetList>li userContentBgColor = #E6E6E6
margin 15px 0
.shortCut .UserContainer
margin-top 5px absolute top bottom right
color lighten(textColor, 5%) left 60px
font-size 0.8em .content
&.active absolute top bottom right
a left userNavigatorWidth
background-color planetAnchorActiveBgColor background-color userContentBgColor
color planetAnchorActiveColor .UserNavigator
a absolute left top bottom
display block width userNavigatorWidth
width 44px background-color userNavigatorBgColor
height 44px color userNavigatorColor
margin 0 auto noSelect()
text-align center &>.profile
background-color planetAnchorBgColor height 60px
text-decoration none padding 10px 15px 0
color planetAnchorColor
line-height 44px
font-size 1.1em
cursor pointer
circle()
transition 0.1s
&:hover, &:active
background-color white
.planetTooltip
position absolute
z-index popupZIndex
background-color transparentify(invBackgroundColor, 80%)
color invTextColor
padding 10px
line-height 1em
border-radius 5px
margin-top -41px
margin-left 52px
white-space nowrap
opacity 0
transition 0.1s
pointer-events none
&:hover .planetTooltip
opacity 1
img
circle()
width 55px
height 55px
button.newPlanet
display block
margin 0 auto
width 30px
height 30px
circle()
border solid 1px lightButtonColor
color lightButtonColor
text-align center
font-size 1
background-image none
background-color transparent
box-sizing border-box box-sizing border-box
absolute left bottom right position relative
bottom 15px border-bottom solid 1px userNavigatorBorderColor
&:hover, &.hover, &:focus, &.focus cursor pointer
border-color darken(lightButtonColor, 50%) &>.profileName
color darken(lightButtonColor, 50%) color userNavigatorProfileNameColor
&:active, &.active font-size 22px
border-color darken(brandBorderColor, 10%) cursor pointer
transition 0.1s
&>.name
padding 5px 10px
font-size 14px
color userNavigatorColor
cursor pointer
transition 0.1s
&>.dropdownIcon
position absolute
top 20px
right 25px
float right
width 20px
height 20px
line-height 20px
font-size 8px
border solid 1px userNavigatorColor
border-radius 12.5px
text-align center
transition 0.1s
&:hover
&>.profileName
color lighten(brandColor, 10%)
&>.name
color white
&>.dropdownIcon
border-color white
&:active
&>.dropdownIcon
background-color brandColor
border-color brandColor
&>.control
padding 15px 15px
&>.newPostButton
background-color brandColor background-color brandColor
color white color white
.tooltip height 44px
tooltip() width 100%
margin-top -22px border none
margin-left 33px
&:hover .tooltip
opacity 1
.UserContainer
absolute top bottom right
left 55px
.memberPopup
absolute left
top 235px
z-index 1
padding 0 15px 10px
width 200px
.label
padding 10px 0
font-size 0.9em
border-bottom solid 1px borderColor
margin-bottom 15px
.members
li
padding 0 10px
margin-bottom 15px
clearfix()
.memberImage
float left
margin-right 7px
circle()
.memberInfo
float left
.memberProfileName
margin-bottom 5px
font-size 1.05em
.memberName
margin-left 5px
font-size 0.9em
color inactiveTextColor
a:hover .memberProfileName, a:hover .memberName
text-decoration underline
.userProfile
absolute top left right
padding 15px
border-bottom solid 1px borderColor
height 125px
clearfix()
.userPhoto
circle()
float left
margin 5px 15px 15px
.userInfo
float left
margin-top 15px
.userProfileName
font-size 1.5em
color brandColor
margin-bottom 10px
.userName
font-size 1.1em
.editProfileButton
float right
btnDefault()
margin-top 25px
padding 10px 15px
border-radius 5px border-radius 5px
.teamList, .memberList font-size 16px
absolute left bottom font-weight 600
top 125px transition 0.1s
width 200px &:hover
padding 15px background-color lighten(brandColor, 10%)
border-right solid 1px borderColor &>.menu
overflow-y auto absolute left right bottom
.teamLabel, .memberLabel top 134px
font-size 1.2em padding 15px 0
margin-bottom 15px overflow auto
.teams &>.menuGruop
li &>.label
padding 0 10px border-bottom 1px solid userNavigatorBorderColor
margin-bottom 15px padding 10px 15px
clearfix() font-size 18px
.teamInfo
float left
.teamProfileName
margin-bottom 5px
font-size 1.05em
.teamName
margin-left 5px
font-size 0.9em
color inactiveTextColor
a:hover .teamProfileName, a:hover .teamName
text-decoration underline
margin-bottom 10px margin-bottom 10px
font-size 1.1em &>.plusButton
.createTeamButton, .addMemberButton float right
btnStripDefault() width 20px
.members height 20px
li margin-top -2.5px
padding 0 10px margin-right -5px
margin-bottom 15px line-height 15px
clearfix() font-size 8px
.memberImage border solid 1px userNavigatorColor
float left border-radius 10px
margin-right 7px background-color transparent
circle() text-align center
.memberInfo color userNavigatorColor
float left &:hover
.memberProfileName border-color white
margin-bottom 5px color white
font-size 1.05em &:active
.memberRole background-color brandColor
font-size 0.9em border-color brandColor
color inactiveTextColor &>.folders
.memberName .folderButton
margin-left 5px padding 10px 25px
font-size 0.9em width 100%
color inactiveTextColor background-color transparent
.createTeamButton, .addMemberButton border none
btnStripDefault() font-size 14px
a:hover .memberProfileName, a:hover .memberName color userNavigatorColor
text-decoration underline transition 0.1s
.planetList text-align left
absolute right bottom &:hover
top 125px background-color transparentify(white, 20%)
left 200px color white
padding 15px &.active
overflow-y auto background-color brandColor
.planetLabel color white
font-size 1.2em
margin-bottom 15px
.planetGroup
margin-left 15px
.planetGroupLabel
font-size 1.1em
margin-bottom 15px
.planets
margin-left 15px
li
a
font-size 1.1em
text-decoration none
&:hover
text-decoration underline
margin-bottom 10px
.createPlanetButton
btnStripDefault()

View File

@@ -5,35 +5,29 @@ global-reset()
@import '../shared/*' @import '../shared/*'
@import './components/*' @import './components/*'
@import './containers/*' @import './containers/*'
@import './HomeContainer'
*
-webkit-app-region no-drag
-webkit-user-select none
html, body html, body
width 100% width 100%
height 100% height 100%
overflow hidden overflow hidden
body body
font-family "Lato" font-family "Lato"
color textColor color textColor
font-size fontSize font-size fontSize
font-weight 400 font-weight 400
button
button, input, select
font-family "Lato" font-family "Lato"
div, span, a, button, input, textarea div, span, a, button, input, textarea
box-sizing border-box box-sizing border-box
h1
font-size 2em
h2
font-size 1.5em
h3
font-size 1.17em
h4
font-size 1em
h5
font-size 0.83em
h6
font-size 0.67em
a a
color brandColor color brandColor
&:hover &:hover
@@ -52,6 +46,9 @@ button
&:focus, &.focus &:focus, &.focus
outline none outline none
.noSelect
noSelect()
.text-center .text-center
text-align center text-align center
@@ -61,13 +58,6 @@ button
display block display block
margin-bottom 5px margin-bottom 5px
.stripInput
stripInput()
display block
width 100%
font-size 1em
height 33px
.block-input, .inline-input .block-input, .inline-input
border solid 1px borderColor border solid 1px borderColor
padding 0 10px padding 0 10px
@@ -107,22 +97,32 @@ textarea.block-input
z-index 2000 z-index 2000
bottom 5px bottom 5px
right 53px right 53px
btnPrimary()
padding 10px 15px padding 10px 15px
border none
border-radius 5px border-radius 5px
background-color backgroundColor background-color brandColor
color white
opacity 0.7
&:hover
opacity 1
background-color lighten(brandColor, 10%)
.contactButton .contactButton
position fixed position fixed
z-index 2000 z-index 2000
bottom 5px bottom 5px
right 5px right 5px
btnPrimary()
padding 10px 15px padding 10px 15px
border none
border-radius 5px border-radius 5px
background-color backgroundColor background-color brandColor
color white
opacity 0.7
&:hover
opacity 1
background-color lighten(brandColor, 10%)
.tooltip .tooltip
tooltip() tooltip()
margin-top -22px margin-top -22px
margin-left -97px margin-left -107px
&:hover .tooltip &:hover .tooltip
opacity 1 opacity 1

View File

@@ -3,6 +3,7 @@ stripInput()
border-bottom 1px solid borderColor border-bottom 1px solid borderColor
padding 5px 15px padding 5px 15px
transition 0.1s transition 0.1s
font-size 14px
&:focus, &.focus &:focus, &.focus
border-bottom 1px solid brandBorderColor border-bottom 1px solid brandBorderColor
outline none outline none
@@ -11,6 +12,7 @@ borderInput()
border solid 1px borderColor border solid 1px borderColor
padding 5px 15px padding 5px 15px
transition 0.1s transition 0.1s
font-size 14px
&:focus, &.focus &:focus, &.focus
border-color brandBorderColor border-color brandBorderColor
outline none outline none

View File

@@ -1,32 +1,36 @@
marked() marked()
h1, h2, h3, h4, h5, h6, p
&:first-child
margin-top 0
hr hr
border-top none border-top none
border-bottom solid 1px borderColor border-bottom solid 1px borderColor
margin 15px 0 margin 15px 0
h1 h1
font-size 2em font-size 2em
margin 0 auto 0.67em border-bottom solid 2px borderColor
margin 0.33em auto 0.67em
h2 h2
font-size 1.5em font-size 1.5em
margin 0 auto 0.83em margin 0.42em auto 0.83em
h3 h3
font-size 1.17em font-size 1.17em
margin 0 auto 1em margin 0.5em auto 1em
h4 h4
font-size 1em font-size 1em
margin 0 auto 1.33em margin 0.67em auto 1.33em
h5 h5
font-size 0.83em font-size 0.83em
margin 0 auto 1.67em margin 0.84em auto 1.67em
h6 h6
font-size 0.67em font-size 0.67em
margin 2.33em auto margin 1.16em auto 2.33em
h1, h2, h3, h4, h5, h6 h1, h2, h3, h4, h5, h6
font-weight 400 font-weight 700
line-height 1.4em line-height 1.8em
p p
line-height 1.4em line-height 1.8em
margin-bottom 15px margin 15px 0 25px
img img
max-width 100% max-width 100%
strong strong
@@ -37,14 +41,15 @@ marked()
text-decoration line-through text-decoration line-through
blockquote blockquote
border-left solid 4px brandBorderColor border-left solid 4px brandBorderColor
margin 15px 0 15px margin 15px 0 25px
padding 0 25px padding 0 25px
ul ul
list-style-type disc list-style-type disc
padding-left 35px padding-left 35px
margin-bottom 35px
li li
display list-item display list-item
margin 15px 0 line-height 1.8em
&>li>ul &>li>ul
list-style-type circle list-style-type circle
&>li>ul &>li>ul
@@ -52,9 +57,10 @@ marked()
ol ol
list-style-type decimal list-style-type decimal
padding-left 35px padding-left 35px
margin-bottom 35px
li li
display list-item display list-item
margin 15px 0 line-height 1.8em
code code
font-family monospace font-family monospace
padding 2px 4px padding 2px 4px
@@ -63,12 +69,14 @@ marked()
font-size 0.9em font-size 0.9em
color black color black
text-decoration none text-decoration none
background-color #F6F6F6
pre pre
padding 5px padding 5px
border solid 1px borderColor border solid 1px borderColor
border-radius 5px border-radius 5px
overflow-x auto overflow-x auto
margin-bottom 15px margin 15px 0 25px
background-color #F6F6F6
&>code &>code
padding 0 padding 0
border none border none

View File

@@ -3,11 +3,12 @@ tooltip()
z-index popupZIndex z-index popupZIndex
background-color transparentify(invBackgroundColor, 80%) background-color transparentify(invBackgroundColor, 80%)
color invTextColor color invTextColor
padding 10px padding 6px 15px
font-size 12px font-size 12px
line-height 12px font-weight normal
border-radius 5px line-height 20px
white-space nowrap white-space nowrap
opacity 0 opacity 0
transition 0.1s transition 0.1s
pointer-events none pointer-events none

View File

@@ -3,5 +3,4 @@ borderBox()
noSelect() noSelect()
-webkit-user-select none -webkit-user-select none
-webkit-app-region drag cursor default

Some files were not shown because too many files have changed in this diff Show More