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

revive articledetail

This commit is contained in:
Rokt33r
2015-10-13 16:09:37 +09:00
parent 5356e68b51
commit e5e8032ba1
16 changed files with 354 additions and 135 deletions

View File

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

View File

@@ -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,11 +1,7 @@
var React = require('react')
import React, { PropTypes } from 'react'
module.exports = React.createClass({
propTypes: {
className: React.PropTypes.string,
mode: React.PropTypes.string
},
getClassName: function () {
export default class ModeIcon extends React.Component {
getClassName () {
var mode = this.props.mode
switch (mode) {
// Script
@@ -69,11 +65,17 @@ module.exports = React.createClass({
return 'fa fa-fw fa-file-text-o'
}
return 'fa fa-fw fa-code'
},
render: function () {
}
render () {
var className = this.getClassName()
return (
<i className={this.props.className + ' ' + className}/>
)
}
})
}
ModeIcon.propTypes = {
className: PropTypes.string,
mode: PropTypes.string
}

View File

@@ -1,9 +1,59 @@
import React, { PropTypes } from 'react'
import moment from 'moment'
import { findWhere } from 'lodash'
import ModeIcon from '../../Components/ModeIcon'
import MarkdownPreview from '../../Components/MarkdownPreview'
import CodeEditor from '../../Components/CodeEditor'
export default class ArticleDetail extends React.Component {
render () {
let { article, status, user } = this.props
let tags = article.Tags.length > 0 ? article.Tags.map(tag => {
return (
<div className='ArticleDetail'></div>
<a key={tag.id}>{tag.name}</a>
)
}) : (
<span className='noTags'>Not tagged yet</span>
)
let folder = findWhere(user.Folders, {id: article.FolderId})
let folderName = folder != null ? folder.name : '(unknown)'
return (
<div className='ArticleDetail show'>
<div className='detailInfo'>
<div className='left'>
<div className='info'>
<i className='fa fa-fw fa-square'/> {folderName}&nbsp;
by {article.User.profileName}&nbsp;
Created {moment(article.createdAt).format('YYYY/MM/DD')}&nbsp;
Updated {moment(article.updatedAt).format('YYYY/MM/DD')}
</div>
<div className='tags'><i className='fa fa-fw fa-tags'/>{tags}</div>
</div>
<div className='right'>
<button><i className='fa fa-fw fa-edit'/></button>
<button><i className='fa fa-fw fa-trash'/></button>
<button><i className='fa fa-fw fa-share-alt'/></button>
</div>
</div>
<div className='detailBody'>
<div className='detailPanel'>
<div className='header'>
<ModeIcon className='mode' mode={article.mode}/>
<div className='title'>{article.title}</div>
</div>
{article.mode === 'markdown' ? <MarkdownPreview content={article.content}/> : <CodeEditor readOnly={true} onChange={this.handleContentChange} mode={article.mode} code={article.content}/>}
</div>
</div>
</div>
)
}
}
ArticleDetail.propTypes = {
article: PropTypes.shape(),
status: PropTypes.shape(),
user: PropTypes.shape()
}

View File

@@ -1,11 +1,66 @@
import React, { PropTypes } from 'react'
import ProfileImage from '../../components/ProfileImage'
import ModeIcon from '../../Components/ModeIcon'
import moment from 'moment'
import { IDLE_MODE, CREATE_MODE, EDIT_MODE } from '../actions'
class ArticleList extends React.Component {
export default class ArticleList extends React.Component {
render () {
let { articles, status } = this.props
let articlesEl = articles.map(article => {
let tags = Array.isArray(article.Tags) && article.Tags.length > 0 ? article.Tags.map(tag => {
return (
<div className='ArticleList'></div>
<a key={tag.id}>#{tag.name}</a>
)
}) : (
<span>Not tagged yet</span>
)
return (
<div key={'article-' + article.id}>
<div className={'articleItem' + (false ? ' active' : '')}>
<div className='top'>
<i className='fa fa-fw fa-square'/>
by <ProfileImage className='profileImage' size='20' email={article.User.email}/> {article.User.profileName}
<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'/>{tags}</div>
</div>
</div>
<div className='divider'></div>
</div>
)
})
return (
<div className='ArticleList'>
{ status.mode === 'CREATE_MODE' ? (
<div key={'article-' + article.id}>
<div className={'articleItem'}>
<div className='top'>
<span className='updatedAt'>{}</span>
</div>
<div className='middle'>
<ModeIcon className='mode' mode={article.mode}/> <div className='title'>'(New article)'</div>
</div>
<div className='bottom'>
<div className='tags'><i className='fa fa-fw fa-tags'/></div>
</div>
</div>
<div className='divider'></div>
</div>
) : null}
{articlesEl}
</div>
)
}
}
export default ArticleList
ArticleList.propTypes = {
articles: PropTypes.array
}

View File

@@ -6,7 +6,6 @@ export default class ArticleNavigator extends React.Component {
render () {
let { user, status } = this.props
if (user == null) return (<div className='ArticleNavigator'/>)
console.log(user.Folders)
let activeFolder = findWhere(user.Folders, {id: status.folderId})
@@ -68,5 +67,8 @@ export default class ArticleNavigator extends React.Component {
}
ArticleNavigator.propTypes = {
user: PropTypes.object
user: PropTypes.object,
state: PropTypes.shape({
folderId: PropTypes.number
})
}

View File

@@ -1,6 +1,12 @@
export const USER_UPDATE = 'USER_UPDATE'
export const ARTICLE_UPDATE = 'ARTICLE_UPDATE'
export const SWITCH_USER = 'SWITCH_USER'
export const SWITCH_FOLDER = 'SWITCH_FOLDER'
export const SWITCH_MODE = 'SWITCH_MODE'
export const IDLE_MODE = 'IDLE_MODE'
export const CREATE_MODE = 'CREATE_MODE'
export const EDIT_MODE = 'EDIT_MODE'
export function updateUser (user) {
return {
@@ -9,6 +15,13 @@ export function updateUser (user) {
}
}
export function updateArticles (userId, articles) {
return {
type: ARTICLE_UPDATE,
data: {userId, articles}
}
}
export function switchUser (userId) {
return {
type: SWITCH_USER,
@@ -22,3 +35,10 @@ export function switchFolder (folderId) {
data: folderId
}
}
export function switchMode (mode) {
return {
type: SWITCH_MODE,
data: mode
}
}

View File

@@ -27,15 +27,15 @@ class HomeContainer extends React.Component {
}
render () {
const { users, user, status } = this.props
const { users, user, status, articles, article } = this.props
return (
<div className='HomeContainer'>
<UserNavigator users={users} />
<ArticleNavigator user={user} status={status}/>
<ArticleTopBar/>
<ArticleList/>
<ArticleDetail/>
<ArticleList articles={articles} status={status}/>
<ArticleDetail user={user} article={article} status={status}/>
</div>
)
}
@@ -49,11 +49,17 @@ function remap (state) {
let users = [currentUser, ...teams]
let user = findWhere(users, {id: parseInt(status.userId, 10)})
if (user == null) user = users[0]
let articles = state.articles['team-' + user.id]
let article = findWhere(users, {id: status.articleId})
if (article == null) article = articles[0]
return {
users,
user,
status
status,
articles,
article
}
}
@@ -67,6 +73,7 @@ HomeContainer.propTypes = {
userId: PropTypes.string,
folderId: PropTypes.number
}),
articles: PropTypes.array,
dispatch: PropTypes.func
}

View File

@@ -1,6 +1,22 @@
var request = require('superagent-promise')(require('superagent'), Promise)
var apiUrl = require('../../../../config').apiUrl
export function fetchCurrentUser () {
return request
.get(apiUrl + 'auth/user')
.set({
Authorization: 'Bearer ' + localStorage.getItem('token')
})
}
export function fetchArticles (userId) {
return request
.get(apiUrl + 'teams/' + userId + '/articles')
.set({
Authorization: 'Bearer ' + localStorage.getItem('token')
})
}
export function createTeam (input) {
return request
.post(apiUrl + 'teams')

View File

@@ -0,0 +1,11 @@
import markdownit from 'markdown-it'
var md = markdownit({
typographer: true,
linkify: true
})
export default function markdown (content) {
if (content == null) content = ''
return md.render(content.toString())
}

View File

@@ -1,8 +1,17 @@
import { combineReducers } from 'redux'
import { SWITCH_USER, SWITCH_FOLDER, USER_UPDATE } from './actions'
import { SWITCH_USER, SWITCH_FOLDER, SWITCH_MODE, USER_UPDATE, ARTICLE_UPDATE, IDLE_MODE, CREATE_MODE, EDIT_MODE } from './actions'
const initialCurrentUser = JSON.parse(localStorage.getItem('currentUser'))
const initialParams = {}
const initialStatus = {
mode: IDLE_MODE
}
// init articles
let teams = Array.isArray(initialCurrentUser.Teams) ? initialCurrentUser.Teams : []
let users = [initialCurrentUser, ...teams]
const initialArticles = users.reduce((res, user) => {
res['team-' + user.id] = JSON.parse(localStorage.getItem('team-' + user.id))
return res
}, {})
function currentUser (state, action) {
switch (action.type) {
@@ -26,13 +35,31 @@ function status (state, action) {
case SWITCH_FOLDER:
state.folderId = action.data
return state
case SWITCH_MODE:
state.mode = action.data
return state
default:
if (state == null) return initialParams
if (state == null) return initialStatus
return state
}
}
function articles (state, action) {
switch (action.type) {
case ARTICLE_UPDATE:
let { userId, articles } = action.data
let teamKey = 'team-' + userId
localStorage.setItem(teamKey, JSON.stringify(articles))
state[teamKey] = articles
return state
default:
if (state == null) return initialArticles
return state
}
}
export default combineReducers({
currentUser,
status
status,
articles
})

View File

@@ -1,9 +1,9 @@
import React from 'react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { updateUser } from './HomeContainer/actions'
import { updateUser, updateArticles } from './HomeContainer/actions'
import reducer from './HomeContainer/reducer'
import Hq from './Services/Hq'
import { fetchCurrentUser, fetchArticles } from './HomeContainer/lib/api'
import { Router, Route, IndexRoute } from 'react-router'
import MainContainer from './Containers/MainContainer'
import LoginContainer from './Containers/LoginContainer'
@@ -58,9 +58,22 @@ React.render((
loadingCover.parentNode.removeChild(loadingCover)
// Refresh user information
Hq.getUser()
fetchCurrentUser()
.then(function (res) {
store.dispatch(updateUser(res.body))
let user = res.body
store.dispatch(updateUser(user))
let users = [user].concat(user.Teams)
users.forEach(user => {
fetchArticles(user.id)
.then(res => {
store.dispatch(updateArticles(user.id, res.body))
})
.catch(err => {
if (err.status == null) throw err
console.error(err)
})
})
})
.catch(function (err) {
console.error(err.message)

View File

@@ -5,6 +5,9 @@ noTagsColor = #999
top 60px
left 510px
padding 10px
background-color #E6E6E6
border-top 1px solid borderColor
border-left 1px solid borderColor
*
-webkit-user-select all
.detailInfo

View File

@@ -6,12 +6,12 @@ articleItemColor = #777
top 60px
left 260px
width 250px
border-right solid 1px highlightenBorderColor
&>ul
absolute top bottom left right
border-top 1px solid borderColor
border-right 4px solid #E6E6E6
overflow-y auto
noSelect()
li
&>div
border-right 1px solid borderColor
.articleItem
border solid 2px transparent
position relative

View File

@@ -5,6 +5,7 @@ articleNavBgColor = #353535
absolute top bottom
left 60px
width 200px
border-right 1px solid borderColor
color white
.userInfo
height 60px
@@ -48,7 +49,7 @@ articleNavBgColor = #353535
.header
border-bottom 1px solid borderColor
padding-bottom 5px
margin-bottom 5px
margin-bottom 10px
clearfix()
.title
float left
@@ -75,8 +76,8 @@ articleNavBgColor = #353535
.folders
margin-bottom 15px
.folderList button
height 44px
width 200px
height 33px
width 199px
border none
text-align left
font-size 14px

View File

@@ -43,7 +43,9 @@ module.exports = {
'react-transform-catch-errors',
'redux-devtools',
'redux-devtools/lib/react',
'react-select'
'react-select',
'markdown-it',
'moment'
],
resolve: {
extensions: ['', '.js', '.jsx', 'styl']