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({ export default class ModeIcon extends React.Component {
propTypes: { getClassName () {
className: React.PropTypes.string,
mode: React.PropTypes.string
},
getClassName: function () {
var mode = this.props.mode var mode = this.props.mode
switch (mode) { switch (mode) {
// Script // Script
@@ -69,11 +65,17 @@ module.exports = React.createClass({
return 'fa fa-fw fa-file-text-o' return 'fa fa-fw fa-file-text-o'
} }
return 'fa fa-fw fa-code' return 'fa fa-fw fa-code'
}, }
render: function () {
render () {
var className = this.getClassName() var className = this.getClassName()
return ( return (
<i className={this.props.className + ' ' + className}/> <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 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 { export default class ArticleDetail extends React.Component {
render () { render () {
let { article, status, user } = this.props
let tags = article.Tags.length > 0 ? article.Tags.map(tag => {
return (
<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 ( return (
<div className='ArticleDetail'></div> <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 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'
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 (
<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>
)
})
class ArticleList extends React.Component {
render() {
return ( return (
<div className='ArticleList'></div> <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 () { render () {
let { user, status } = this.props let { user, status } = this.props
if (user == null) return (<div className='ArticleNavigator'/>) if (user == null) return (<div className='ArticleNavigator'/>)
console.log(user.Folders)
let activeFolder = findWhere(user.Folders, {id: status.folderId}) let activeFolder = findWhere(user.Folders, {id: status.folderId})
@@ -68,5 +67,8 @@ export default class ArticleNavigator extends React.Component {
} }
ArticleNavigator.propTypes = { 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 USER_UPDATE = 'USER_UPDATE'
export const ARTICLE_UPDATE = 'ARTICLE_UPDATE'
export const SWITCH_USER = 'SWITCH_USER' export const SWITCH_USER = 'SWITCH_USER'
export const SWITCH_FOLDER = 'SWITCH_FOLDER' 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) { export function updateUser (user) {
return { 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) { export function switchUser (userId) {
return { return {
type: SWITCH_USER, type: SWITCH_USER,
@@ -22,3 +35,10 @@ export function switchFolder (folderId) {
data: 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 () { render () {
const { users, user, status } = this.props const { users, user, status, articles, article } = this.props
return ( return (
<div className='HomeContainer'> <div className='HomeContainer'>
<UserNavigator users={users} /> <UserNavigator users={users} />
<ArticleNavigator user={user} status={status}/> <ArticleNavigator user={user} status={status}/>
<ArticleTopBar/> <ArticleTopBar/>
<ArticleList/> <ArticleList articles={articles} status={status}/>
<ArticleDetail/> <ArticleDetail user={user} article={article} status={status}/>
</div> </div>
) )
} }
@@ -49,11 +49,17 @@ function remap (state) {
let users = [currentUser, ...teams] let users = [currentUser, ...teams]
let user = findWhere(users, {id: parseInt(status.userId, 10)}) 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 { return {
users, users,
user, user,
status status,
articles,
article
} }
} }
@@ -67,6 +73,7 @@ HomeContainer.propTypes = {
userId: PropTypes.string, userId: PropTypes.string,
folderId: PropTypes.number folderId: PropTypes.number
}), }),
articles: PropTypes.array,
dispatch: PropTypes.func dispatch: PropTypes.func
} }

View File

@@ -1,6 +1,22 @@
var request = require('superagent-promise')(require('superagent'), Promise) var request = require('superagent-promise')(require('superagent'), Promise)
var apiUrl = require('../../../../config').apiUrl 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) { export function createTeam (input) {
return request return request
.post(apiUrl + 'teams') .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 { 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 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) { function currentUser (state, action) {
switch (action.type) { switch (action.type) {
@@ -26,13 +35,31 @@ function status (state, action) {
case SWITCH_FOLDER: case SWITCH_FOLDER:
state.folderId = action.data state.folderId = action.data
return state return state
case SWITCH_MODE:
state.mode = action.data
return state
default: 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 return state
} }
} }
export default combineReducers({ export default combineReducers({
currentUser, currentUser,
status status,
articles
}) })

View File

@@ -1,9 +1,9 @@
import React from 'react' import React from 'react'
import { createStore } from 'redux' import { createStore } from 'redux'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { updateUser } from './HomeContainer/actions' import { updateUser, updateArticles } from './HomeContainer/actions'
import reducer from './HomeContainer/reducer' 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 { Router, Route, IndexRoute } from 'react-router'
import MainContainer from './Containers/MainContainer' import MainContainer from './Containers/MainContainer'
import LoginContainer from './Containers/LoginContainer' import LoginContainer from './Containers/LoginContainer'
@@ -58,9 +58,22 @@ React.render((
loadingCover.parentNode.removeChild(loadingCover) loadingCover.parentNode.removeChild(loadingCover)
// Refresh user information // Refresh user information
Hq.getUser() fetchCurrentUser()
.then(function (res) { .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) { .catch(function (err) {
console.error(err.message) console.error(err.message)

View File

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

View File

@@ -6,65 +6,65 @@ articleItemColor = #777
top 60px top 60px
left 260px left 260px
width 250px width 250px
border-right solid 1px highlightenBorderColor border-top 1px solid borderColor
&>ul border-right 4px solid #E6E6E6
absolute top bottom left right overflow-y auto
overflow-y auto noSelect()
noSelect() &>div
li border-right 1px solid borderColor
.articleItem .articleItem
border solid 2px transparent border solid 2px transparent
position relative position relative
height 88px height 88px
width 100% width 100%
cursor pointer cursor pointer
transition 0.1s transition 0.1s
background-color white background-color white
padding 0 10px padding 0 10px
font-size 12px font-size 12px
.top .top
clearfix() clearfix()
line-height 20px
padding 5px 0
color articleItemColor
.profileImage
vertical-align middle
.updatedAt
float right
line-height 20px line-height 20px
padding 5px 0 .middle
clearfix()
padding 3px 0 7px
font-size 16px
.mode
float left
font-size 12px
line-height 16px
.title
float left
overflow ellipsis
padding 0 5px
.bottom
padding 5px 0
overflow-x auto
white-space nowrap
.tags
color articleItemColor color articleItemColor
.profileImage a
vertical-align middle background-color brandColor
.updatedAt color white
float right border-radius 2px
line-height 20px padding 1.5px 5px
.middle margin 2px
clearfix() font-size 10px
padding 3px 0 7px opacity 0.8
font-size 16px &:hover
.mode opacity 1
float left &:hover, &.hover
font-size 12px background-color articleItemHoverBgColor
line-height 16px
.title
float left
overflow ellipsis
padding 0 5px
.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 &:active, &.active
border-color brandBorderColor background-color white
.divider &:active, &.active
border-bottom solid 1px borderColor border-color brandBorderColor
.divider
border-bottom solid 1px borderColor

View File

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

View File

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