diff --git a/client/src/actions/articles-actions.js b/client/src/actions/articles-actions.js new file mode 100644 index 00000000..9e144e4c --- /dev/null +++ b/client/src/actions/articles-actions.js @@ -0,0 +1,21 @@ +import API from 'lib-app/api-call'; + +export default { + + initArticles() { + return { + type: 'INIT_ARTICLES', + payload: {} + }; + }, + + retrieveArticles() { + return { + type: 'GET_ARTICLES', + payload: API.call({ + path: '/article/get-all', + data: {} + }) + }; + } +}; \ No newline at end of file diff --git a/client/src/app-components/article-add-modal.js b/client/src/app-components/article-add-modal.js new file mode 100644 index 00000000..613abbc7 --- /dev/null +++ b/client/src/app-components/article-add-modal.js @@ -0,0 +1,56 @@ +import React from 'react'; + +import i18n from 'lib-app/i18n'; +import API from 'lib-app/api-call'; +import ModalContainer from 'app-components/modal-container'; + +import Header from 'core-components/header'; +import Form from 'core-components/form'; +import FormField from 'core-components/form-field'; +import SubmitButton from 'core-components/submit-button'; +import Button from 'core-components/button'; + +class ArticleAddModal extends React.Component { + static propTypes = { + topicId: React.PropTypes.number.isRequired, + topicName: React.PropTypes.string.isRequired, + position: React.PropTypes.number.isRequired + }; + + render() { + return ( +
+
+
+ + + {i18n('ADD_ARTICLE')} + + +
+ ); + } + + onAddNewArticleFormSubmit(form) { + API.call({ + path: '/article/add', + data: { + title: form.title, + content: form.content, + topicId: this.props.topicId, + position: this.props.position + } + }).then(() => { + ModalContainer.closeModal(); + + if(this.props.onChange) { + this.props.onChange(); + } + }); + } +} + +export default ArticleAddModal; \ No newline at end of file diff --git a/client/src/app-components/article-add-modal.scss b/client/src/app-components/article-add-modal.scss new file mode 100644 index 00000000..50ba5b88 --- /dev/null +++ b/client/src/app-components/article-add-modal.scss @@ -0,0 +1,7 @@ +.article-add-article { + + &__cancel-button { + float: right; + margin-top: 15px; + } +} \ No newline at end of file diff --git a/client/src/app-components/articles-list.js b/client/src/app-components/articles-list.js new file mode 100644 index 00000000..2944b8c0 --- /dev/null +++ b/client/src/app-components/articles-list.js @@ -0,0 +1,88 @@ +import React from 'react'; +import {connect} from 'react-redux'; + +import i18n from 'lib-app/i18n'; +import ArticlesActions from 'actions/articles-actions'; + +import TopicViewer from 'app-components/topic-viewer'; +import ModalContainer from 'app-components/modal-container'; +import TopicEditModal from 'app-components/topic-edit-modal'; + +import Loading from 'core-components/loading'; +import Button from 'core-components/button'; +import Icon from 'core-components/icon'; + +class ArticlesList extends React.Component { + + static propTypes = { + editable: React.PropTypes.bool, + articlePath: React.PropTypes.string, + loading: React.PropTypes.bool, + topics: React.PropTypes.array, + retrieveOnMount: React.PropTypes.bool + }; + + static defaultProps = { + editable: true, + retrieveOnMount: true + }; + + componentDidMount() { + if(this.props.retrieveOnMount) { + this.retrieveArticles(); + } + } + + render() { + return (this.props.loading) ? : this.renderContent(); + } + + renderContent() { + return ( +
+ {this.renderTopics()} + {(this.props.editable) ? this.renderAddTopicButton() : null} +
+ ); + } + + renderTopics() { + return ( +
+ {this.props.topics.map((topic, index) => { + return ( +
+ + +
+ ); + })} +
+ ); + } + + renderAddTopicButton() { + return ( +
+ +
+ ); + } + + retrieveArticles() { + this.props.dispatch(ArticlesActions.retrieveArticles()); + } +} + +export default connect((store) => { + return { + topics: store.articles.topics, + loading: store.articles.loading + }; +})(ArticlesList); diff --git a/client/src/app-components/articles-list.scss b/client/src/app-components/articles-list.scss new file mode 100644 index 00000000..df4325bd --- /dev/null +++ b/client/src/app-components/articles-list.scss @@ -0,0 +1,22 @@ +@import "../scss/vars"; + +.articles-list { + + &__add { + position: relative; + } + + &__add-icon { + position: absolute; + left: 10px; + margin-top: -4px; + } + + &__topic-separator { + background-color: $grey; + display: block; + height: 1px; + margin: 30px 0; + width: 100%; + } +} \ No newline at end of file diff --git a/client/src/app-components/modal-container.js b/client/src/app-components/modal-container.js index c9591c31..4cd8e03e 100644 --- a/client/src/app-components/modal-container.js +++ b/client/src/app-components/modal-container.js @@ -16,6 +16,10 @@ class ModalContainer extends React.Component { ); } + static closeModal() { + store.dispatch(ModalActions.closeModal()); + } + static childContextTypes = { closeModal: React.PropTypes.func }; diff --git a/client/src/app-components/topic-edit-modal.js b/client/src/app-components/topic-edit-modal.js new file mode 100644 index 00000000..f092699c --- /dev/null +++ b/client/src/app-components/topic-edit-modal.js @@ -0,0 +1,74 @@ +import React from 'react'; + +import i18n from 'lib-app/i18n'; +import API from 'lib-app/api-call'; + +import Header from 'core-components/header'; +import Button from 'core-components/button'; +import Form from 'core-components/form'; +import FormField from 'core-components/form-field'; +import SubmitButton from 'core-components/submit-button'; +import IconSelector from 'core-components/icon-selector'; +import ColorSelector from 'core-components/color-selector'; + +class TopicEditModal extends React.Component { + + static contextTypes = { + closeModal: React.PropTypes.func + }; + + static propTypes = { + defaultValues: React.PropTypes.object, + addForm: React.PropTypes.bool, + topicId: React.PropTypes.number + }; + + state = { + values: this.props.defaultValues || {title: ''} + }; + + render() { + return ( +
+
+
+ + + + + + {i18n('SAVE')} + + + +
+ ); + } + + onSubmit() { + API.call({ + path: (this.props.addForm) ? '/article/add-topic' : '/article/edit-topic', + data: { + topicId: this.props.topicId, + name: this.state.values['title'], + icon: this.state.values['icon'], + iconColor: this.state.values['color'] + } + }).then(this.context.closeModal); + } + + onFormChange(form) { + this.setState({ + values: form + }); + } + + onDiscardClick(event) { + event.preventDefault(); + this.context.closeModal(); + } +} + +export default TopicEditModal; \ No newline at end of file diff --git a/client/src/app-components/topic-edit-modal.scss b/client/src/app-components/topic-edit-modal.scss new file mode 100644 index 00000000..470f01ae --- /dev/null +++ b/client/src/app-components/topic-edit-modal.scss @@ -0,0 +1,19 @@ +.topic-edit-modal { + + &__icon { + display: inline-block; + float: left; + } + + &__color { + margin-left: 60px; + } + + &__save-button { + + } + + &__discard-button { + float: right; + } +} \ No newline at end of file diff --git a/client/src/app-components/topic-viewer.js b/client/src/app-components/topic-viewer.js new file mode 100644 index 00000000..51273272 --- /dev/null +++ b/client/src/app-components/topic-viewer.js @@ -0,0 +1,248 @@ +import React from 'react'; +import _ from 'lodash'; +import classNames from 'classnames'; +import {Link} from 'react-router'; + +import i18n from 'lib-app/i18n'; +import API from 'lib-app/api-call'; +import ModalContainer from 'app-components/modal-container'; +import TopicEditModal from 'app-components/topic-edit-modal'; +import AreYouSure from 'app-components/are-you-sure'; +import ArticleAddModal from 'app-components/article-add-modal'; + +import Icon from 'core-components/icon'; +import Button from 'core-components/button'; + +class TopicViewer extends React.Component { + static propTypes = { + id: React.PropTypes.number.isRequired, + name: React.PropTypes.string.isRequired, + icon: React.PropTypes.string.isRequired, + iconColor: React.PropTypes.string.isRequired, + articles: React.PropTypes.array.isRequired, + articlePath: React.PropTypes.string, + editable: React.PropTypes.bool + }; + + static defaultProps = { + articlePath: '/admin/panel/articles/view-article/', + editable: true + }; + + state = { + articles: this.props.articles, + currentDraggedId: 0 + }; + + render() { + return ( +
+
+ + {this.props.name} + {(this.props.editable) ? this.renderEditButton() : null} + {(this.props.editable) ? this.renderDeleteButton() : null} +
+ +
+ ); + } + + renderEditButton() { + return ( + {ModalContainer.openModal(this.renderEditModal());}}> + + + ); + } + + renderDeleteButton() { + return ( + + + + ); + } + + renderArticleItem(article, index) { + return ( +
  • + + {article.title} + +
  • + ); + } + + renderAddNewArticle() { + return ( +
  • + +
  • + ); + } + + renderEditModal() { + let props = { + topicId: this.props.id, + defaultValues: { + title: this.props.name, + icon: this.props.icon, + iconColor: this.props.iconColor + } + }; + + return ( + + ); + } + + renderAddNewArticleModal() { + let props = { + topicId: this.props.id, + position: this.props.articles.length, + topicName: this.props.name + }; + + return ( + + ); + } + + getArticleLinkProps(article, index) { + let classes = { + 'topic-viewer__list-item-button': true, + 'topic-viewer__list-item-hidden': article.hidden + }; + + let props = { + className: classNames(classes), + to: this.props.articlePath + article.id + }; + + if(this.props.editable) { + _.extend(props, { + onDragOver: this.onItemDragOver.bind(this, article, index), + onDrop: this.onItemDrop.bind(this, article, index), + onDragStart: () => this.setState({currentDraggedId: article.id}), + onDragEnd: () => { + if(this.state.currentDraggedId) { + this.setState({articles: this.props.articles}); + } + } + }); + } + + return props; + } + + onDeleteClick() { + API.call({ + path: '/article/delete-topic', + data: { + topicId: this.props.id + } + }).then(this.onChange.bind(this)); + } + + onItemDragOver(article, index, event) { + event.preventDefault(); + + if(!article.hidden) { + let articles = []; + let draggedId = this.state.currentDraggedId; + let draggedIndex = _.findIndex(this.props.articles, {id: draggedId}); + + _.forEach(this.props.articles, (current, currentIndex) => { + if(draggedIndex < index) { + if(current.id !== draggedId) { + articles.push(current); + } + + if(index === currentIndex) { + articles.push({ + id: article.id, + title: 'X', + hidden: true + }); + } + } else { + if(index === currentIndex) { + articles.push({ + id: article.id, + title: 'X', + hidden: true + }); + } + + if(current.id !== draggedId) { + articles.push(current); + } + } + }); + + this.setState({articles}); + } + } + + onItemDrop(article, index, event) { + event.stopPropagation(); + event.preventDefault(); + let articles = []; + let draggedId = this.state.currentDraggedId; + let dragged = _.find(this.props.articles, {id: draggedId}); + let draggedIndex = _.findIndex(this.props.articles, {id: draggedId}); + + _.forEach(this.props.articles, (current) => { + if(current.id !== draggedId) { + if(draggedIndex < index) { + articles.push(current); + + if(current.id === article.id) { + articles.push(dragged); + } + } else { + if(current.id === article.id) { + articles.push(dragged); + } + + articles.push(current); + } + } + }); + + if(draggedIndex === index) { + this.setState({articles: this.props.articles, currentDraggedId: 0}); + } else { + this.updatePositions(articles.map((article) => article.id)); + this.setState({articles, currentDraggedId: 0}, this.onChange.bind(this)); + } + } + + updatePositions(positions) { + _.forEach(positions, (id, index) => { + if(this.props.articles[index].id !== id) { + API.call({ + path: '/article/edit', + data: { + articleId: id, + position: index + } + }); + } + }); + } + + onChange() { + if(this.props.onChange) { + this.props.onChange(); + } + } +} + +export default TopicViewer; \ No newline at end of file diff --git a/client/src/app-components/topic-viewer.scss b/client/src/app-components/topic-viewer.scss new file mode 100644 index 00000000..0f489be2 --- /dev/null +++ b/client/src/app-components/topic-viewer.scss @@ -0,0 +1,62 @@ +@import "../scss/vars"; + +.topic-viewer { + text-align: left; + + &__header { + cursor: default; + margin-bottom: 15px; + font-size: $font-size--bg; + + &:hover { + .topic-viewer__edit-icon { + display: inline-block; + } + } + } + + &__icon { + color: $primary-green; + } + + &__title { + font-size: $font-size--md; + margin-left: 15px; + } + + &__edit-icon { + color: $grey; + cursor: pointer; + margin-left: 10px; + display: none; + } + + &__list { + + &-item { + display: inline-block; + width: 50%; + color: $secondary-blue; + margin-bottom: 10px; + + &-hidden { + width: 80%; + display: inline-block; + opacity: 0; + } + } + + &-item:before { + content: "• "; + color: $grey; + } + + &-item-button { + color: $secondary-blue; + } + } + + &__add-item { + color: $dark-grey; + } +} \ No newline at end of file diff --git a/client/src/app/Routes.js b/client/src/app/Routes.js index 534f9cda..86e8a7c3 100644 --- a/client/src/app/Routes.js +++ b/client/src/app/Routes.js @@ -66,7 +66,7 @@ export default ( - + @@ -97,7 +97,7 @@ export default ( - + diff --git a/client/src/app/admin/panel/articles/admin-panel-list-articles.js b/client/src/app/admin/panel/articles/admin-panel-list-articles.js index cb607d94..223fb157 100644 --- a/client/src/app/admin/panel/articles/admin-panel-list-articles.js +++ b/client/src/app/admin/panel/articles/admin-panel-list-articles.js @@ -1,11 +1,18 @@ import React from 'react'; +import i18n from 'lib-app/i18n'; +import ArticlesList from 'app-components/articles-list'; +import Header from 'core-components/header'; + class AdminPanelListArticles extends React.Component { render() { return ( -
    - /admin/panel/articles/list-articles +
    +
    +
    + +
    ); } diff --git a/client/src/app/admin/panel/articles/admin-panel-list-articles.scss b/client/src/app/admin/panel/articles/admin-panel-list-articles.scss new file mode 100644 index 00000000..1807e990 --- /dev/null +++ b/client/src/app/admin/panel/articles/admin-panel-list-articles.scss @@ -0,0 +1,6 @@ +.admin-panel-list-articles { + + &__list { + padding: 0 50px; + } +} \ No newline at end of file diff --git a/client/src/app/admin/panel/articles/admin-panel-view-article.js b/client/src/app/admin/panel/articles/admin-panel-view-article.js index fa026870..a023e8e5 100644 --- a/client/src/app/admin/panel/articles/admin-panel-view-article.js +++ b/client/src/app/admin/panel/articles/admin-panel-view-article.js @@ -1,14 +1,148 @@ import React from 'react'; +import _ from 'lodash'; +import {connect} from 'react-redux'; +import RichTextEditor from 'react-rte-browserify'; + +import ArticlesActions from 'actions/articles-actions'; +import SessionStore from 'lib-app/session-store'; +import i18n from 'lib-app/i18n'; +import API from 'lib-app/api-call'; +import DateTransformer from 'lib-core/date-transformer'; + +import Header from 'core-components/header'; +import Loading from 'core-components/loading'; +import Button from 'core-components/button'; +import Form from 'core-components/form'; +import FormField from 'core-components/form-field'; +import SubmitButton from 'core-components/submit-button'; class AdminPanelViewArticle extends React.Component { + static propTypes = { + topics: React.PropTypes.array, + loading: React.PropTypes.bool + }; + + static defaultProps = { + topics: [], + loading: true + }; + + state = { + editable: false + }; + + componentDidMount() { + if(SessionStore.getItem('topics')) { + this.props.dispatch(ArticlesActions.initArticles()); + } else { + this.props.dispatch(ArticlesActions.retrieveArticles()); + } + } + render() { return ( -
    - /admin/panel/articles/view-article +
    + {(this.props.loading) ? : this.renderContent()}
    ); } + + renderContent() { + let article = this.findArticle(); + + return (article) ? this.renderArticle(article) : i18n('ARTICLE_NOT_FOUND'); + } + + renderArticle(article) { + return (this.state.editable) ? this.renderArticleEdit(article) : this.renderArticlePreview(article); + } + + renderArticlePreview(article) { + return ( +
    +
    + +
    + +
    +
    + +
    +
    +
    +
    + {i18n('LAST_EDITED_IN', {date: DateTransformer.transformToString(article.lastEdited)})} +
    +
    +
    + ); + } + + renderArticleEdit() { + return ( +
    this.setState({form})} onSubmit={this.onFormSubmit.bind(this)}> +
    + {i18n('SAVE')} + +
    + + + + ); + } + + findArticle() { + let article = null; + + _.forEach(this.props.topics, (topic) => { + if(!article) { + article = _.find(topic.articles, {id: this.props.params.articleId * 1}); + } + }); + + return article; + } + + onEditClick(article) { + this.setState({ + editable: true, + form: { + title: article.title, + content: RichTextEditor.createValueFromString(article.content, 'html') + } + }); + } + + onFormSubmit(form) { + API.call({ + path: '/article/edit', + data: { + title: form.title, + content: form.content + } + }).then(() => { + this.props.dispatch(ArticlesActions.retrieveArticles()); + this.setState({ + editable: false + }); + }); + } + + onFormCancel(event) { + event.preventDefault(); + + this.setState({ + editable: false + }); + } } -export default AdminPanelViewArticle; \ No newline at end of file +export default connect((store) => { + return { + topics: store.articles.topics, + loading: store.articles.loading + }; +})(AdminPanelViewArticle); diff --git a/client/src/app/admin/panel/articles/admin-panel-view-article.scss b/client/src/app/admin/panel/articles/admin-panel-view-article.scss new file mode 100644 index 00000000..300041c9 --- /dev/null +++ b/client/src/app/admin/panel/articles/admin-panel-view-article.scss @@ -0,0 +1,22 @@ +.admin-panel-view-article { + + &__edit-button { + text-align: left; + margin-bottom: 20px; + } + + &__last-edited { + font-style: italic; + text-align: right; + margin-top: 20px; + } + + &__buttons { + text-align: left; + margin-bottom: 20px; + } + + &__button { + margin-right: 20px; + } +} \ No newline at end of file diff --git a/client/src/app/admin/panel/users/admin-panel-ban-users.js b/client/src/app/admin/panel/users/admin-panel-ban-users.js index a368eeaf..aac27f29 100644 --- a/client/src/app/admin/panel/users/admin-panel-ban-users.js +++ b/client/src/app/admin/panel/users/admin-panel-ban-users.js @@ -1,4 +1,5 @@ import React from 'react'; +import _ from 'lodash'; import i18n from 'lib-app/i18n'; import API from 'lib-app/api-call'; @@ -79,7 +80,7 @@ class AdminPanelBanUsers extends React.Component { onSearch(query) { this.setState({ - filteredEmails: SearchBox.searchQueryInList(this.state.emails, query) + filteredEmails: SearchBox.searchQueryInList(this.state.emails, query, _.startsWith, _.includes) }); } diff --git a/client/src/app/demo/components-demo-page.js b/client/src/app/demo/components-demo-page.js index ef679cbd..e3f68b67 100644 --- a/client/src/app/demo/components-demo-page.js +++ b/client/src/app/demo/components-demo-page.js @@ -107,7 +107,7 @@ let DemoPage = React.createClass({ title: 'Tooltip', render: (
    - + hola
    diff --git a/client/src/app/main/dashboard/dashboard-article/dashboard-article-page.js b/client/src/app/main/dashboard/dashboard-article/dashboard-article-page.js index 15d5a134..8aeca485 100644 --- a/client/src/app/main/dashboard/dashboard-article/dashboard-article-page.js +++ b/client/src/app/main/dashboard/dashboard-article/dashboard-article-page.js @@ -1,14 +1,114 @@ import React from 'react'; +import _ from 'lodash'; +import {connect} from 'react-redux'; + +import ArticlesActions from 'actions/articles-actions'; +import SessionStore from 'lib-app/session-store'; +import i18n from 'lib-app/i18n'; +import DateTransformer from 'lib-core/date-transformer'; + +import Header from 'core-components/header'; +import Loading from 'core-components/loading'; +import BreadCrumb from 'core-components/breadcrumb'; class DashboardArticlePage extends React.Component { + static propTypes = { + topics: React.PropTypes.array, + loading: React.PropTypes.bool + }; + + static defaultProps = { + topics: [], + loading: true + }; + + componentDidMount() { + if(SessionStore.getItem('topics')) { + this.props.dispatch(ArticlesActions.initArticles()); + } else { + this.props.dispatch(ArticlesActions.retrieveArticles()); + } + } + render() { return ( -
    - DASHBOARD ARTICLE +
    +
    + +
    + {(this.props.loading) ? : this.renderContent()}
    ); } + + renderContent() { + let article = this.findArticle(); + + return (article) ? this.renderArticlePreview(article) : i18n('ARTICLE_NOT_FOUND'); + } + + renderArticlePreview(article) { + return ( +
    +
    + +
    +
    +
    +
    + {i18n('LAST_EDITED_IN', {date: DateTransformer.transformToString(article.lastEdited)})} +
    +
    + ); + } + + findArticle() { + let article = null; + + _.forEach(this.props.topics, (topic) => { + if(!article) { + article = _.find(topic.articles, {id: this.props.params.articleId * 1}); + } + }); + + return article; + } + + findTopic() { + let topicFound = {}; + + _.forEach(this.props.topics, (topic) => { + if(_.find(topic.articles, {id: this.props.params.articleId * 1})) { + topicFound = topic; + } + }); + + return topicFound; + } + + getBreadCrumbItems() { + let article = this.findArticle(); + let topic = this.findTopic(); + let items = [ + {content: i18n('ARTICLES'), url: '/dashboard/articles'} + ]; + + if(topic && topic.name) { + items.push({content: topic.name, url: '/dashboard/articles'}); + } + + if(article && article.title) { + items.push({content: article.title}); + } + + return items; + } } -export default DashboardArticlePage; +export default connect((store) => { + return { + topics: store.articles.topics, + loading: store.articles.loading + }; +})(DashboardArticlePage); diff --git a/client/src/app/main/dashboard/dashboard-article/dashboard-article-page.scss b/client/src/app/main/dashboard/dashboard-article/dashboard-article-page.scss new file mode 100644 index 00000000..b165a2cd --- /dev/null +++ b/client/src/app/main/dashboard/dashboard-article/dashboard-article-page.scss @@ -0,0 +1,8 @@ +.dashboard-article-page { + + &__last-edited { + font-style: italic; + text-align: right; + margin-top: 20px; + } +} \ No newline at end of file diff --git a/client/src/app/main/dashboard/dashboard-list-articles/dashboard-list-articles-page.js b/client/src/app/main/dashboard/dashboard-list-articles/dashboard-list-articles-page.js index 552b8198..36837ab9 100644 --- a/client/src/app/main/dashboard/dashboard-list-articles/dashboard-list-articles-page.js +++ b/client/src/app/main/dashboard/dashboard-list-articles/dashboard-list-articles-page.js @@ -1,14 +1,109 @@ import React from 'react'; +import {connect} from 'react-redux'; +import _ from 'lodash'; +import {Link} from 'react-router'; + +import i18n from 'lib-app/i18n'; +import ArticlesList from 'app-components/articles-list'; +import ArticlesActions from 'actions/articles-actions'; + +import Header from 'core-components/header'; +import SearchBox from 'core-components/search-box'; class DashboardListArticlesPage extends React.Component { + state = { + results: [], + showSearchResults: false + }; + + componentDidMount() { + this.props.dispatch(ArticlesActions.retrieveArticles()); + } + render() { return ( -
    - DASHBOARD ARTICLES LIST +
    +
    + + {(!this.state.showSearchResults) ? this.renderArticleList() : this.renderSearchResults()}
    ); } + + renderArticleList() { + return ( + + ); + } + + renderSearchResults() { + return ( +
    + {(_.isEmpty(this.state.results)) ? i18n('NO_RESULTS') : this.state.results.map(this.renderSearchResultsItem.bind(this))} +
    + ); + } + + renderSearchResultsItem(item) { + let content = this.stripHTML(item.content); + content = content.substring(0, 100); + content += '...'; + + return ( +
    +
    + {item.title} +
    +
    {content}
    +
    {item.topic}
    +
    + ); + } + + onSearch(query) { + this.setState({ + results: SearchBox.searchQueryInList(this.getArticles(), query, this.isQueryInTitle.bind(this), this.isQueryInContent.bind(this)), + showSearchResults: query.length + }); + } + + getArticles() { + let articles = []; + + _.forEach(this.props.topics, (topic) => { + _.forEach(topic.articles, (article) => { + articles.push({ + id: article.id, + title: article.title, + content: article.content, + topic: topic.name + }); + }); + }); + + return articles; + } + + isQueryInTitle(article, query) { + return _.includes(article.title.toLowerCase(), query.toLowerCase()); + } + + isQueryInContent(article, query) { + return _.includes(article.content.toLowerCase(), query.toLowerCase()); + } + + stripHTML(html){ + let tmp = document.createElement('DIV'); + tmp.innerHTML = html; + return tmp.textContent || tmp.innerText || ""; + } } -export default DashboardListArticlesPage; + +export default connect((store) => { + return { + topics: store.articles.topics, + loading: store.articles.loading + }; +})(DashboardListArticlesPage); diff --git a/client/src/app/main/dashboard/dashboard-list-articles/dashboard-list-articles-page.scss b/client/src/app/main/dashboard/dashboard-list-articles/dashboard-list-articles-page.scss new file mode 100644 index 00000000..418797af --- /dev/null +++ b/client/src/app/main/dashboard/dashboard-list-articles/dashboard-list-articles-page.scss @@ -0,0 +1,32 @@ +@import "../../../../scss/vars"; + +.dashboard-list-articles-page { + + &__search-results { + + } + + &__search-box { + margin-bottom: 30px; + } + + &__search-result { + margin-bottom: 20px; + text-align: left; + + &-title { + + } + + &-description { + font-size: $font-size--xs; + margin: 5px 0; + } + + &-topic { + color: $grey; + font-size: $font-size--sm; + text-transform: uppercase; + } + } +} \ No newline at end of file diff --git a/client/src/core-components/breadcrumb.js b/client/src/core-components/breadcrumb.js new file mode 100644 index 00000000..8f1382e7 --- /dev/null +++ b/client/src/core-components/breadcrumb.js @@ -0,0 +1,42 @@ +import React from 'react'; +import {Link} from 'react-router'; + +class BreadCrumb extends React.Component { + static propTypes = { + items: React.PropTypes.arrayOf(React.PropTypes.shape({ + content: React.PropTypes.string.isRequired, + url: React.PropTypes.string + })) + }; + + render() { + return ( +
      + {this.props.items.map(this.renderItem.bind(this))} +
    + ); + } + + renderItem(item, index) { + return ( +
  • + {(item.url) ? this.renderItemLink(item) : item.content} + {(index < this.props.items.length - 1) ? this.renderArrow() : null} +
  • + ); + } + + renderItemLink(item) { + return ( + {item.content} + ); + } + + renderArrow() { + return ( + {'>'} + ); + } +} + +export default BreadCrumb; \ No newline at end of file diff --git a/client/src/core-components/breadcrumb.scss b/client/src/core-components/breadcrumb.scss new file mode 100644 index 00000000..df30d414 --- /dev/null +++ b/client/src/core-components/breadcrumb.scss @@ -0,0 +1,18 @@ +@import "../scss/vars"; + +.breadcrumb { + padding: 0; + text-align: left; + margin-bottom: 20px; + + &__item { + color: $grey; + display: inline-block; + text-decoration: none; + } + + &__arrow { + color: $grey; + margin: 0 5px; + } +} \ No newline at end of file diff --git a/client/src/core-components/color-selector.js b/client/src/core-components/color-selector.js new file mode 100644 index 00000000..64e2c920 --- /dev/null +++ b/client/src/core-components/color-selector.js @@ -0,0 +1,57 @@ +import React from 'react'; + +import Tooltip from 'core-components/tooltip'; + +const colors = ['#ff6900', '#fcb900', '#7bdcb5', '#00d084', '#8ed1fc', '#0693e3', '#abb8c3', '#eb144c', '#f78da7', '#9900ef']; + +class ColorSelector extends React.Component { + static propTypes = { + value: React.PropTypes.string, + onChange: React.PropTypes.func + }; + + static defaultProps = { + value: '#ff6900' + }; + + state = { + show: false + }; + + render() { + return ( +
    + this.setState({show})}> + + +
    + ) + } + + renderTooltipContent() { + return ( +
    + {colors.map(this.renderTooltipColor.bind(this))} +
    + ); + } + + renderTooltipColor(color) { + return ( + + ); + } + + onColorClick(color) { + this.setState({ + show: false + }); + + if(this.props.onChange) { + this.props.onChange({target: {value: color}}); + } + } +} + + +export default ColorSelector; \ No newline at end of file diff --git a/client/src/core-components/color-selector.scss b/client/src/core-components/color-selector.scss new file mode 100644 index 00000000..5976e6fc --- /dev/null +++ b/client/src/core-components/color-selector.scss @@ -0,0 +1,21 @@ +.color-selector { + + &__current { + cursor: pointer; + display: inline-block; + width: 30px; + height: 30px; + } + + &__tooltip { + width: 200px; + } + + &__tooltip-color { + cursor: pointer; + display: inline-block; + width: 30px; + height: 30px; + margin: 2px 5px; + } +} \ No newline at end of file diff --git a/client/src/core-components/icon-selector.js b/client/src/core-components/icon-selector.js new file mode 100644 index 00000000..0ed08563 --- /dev/null +++ b/client/src/core-components/icon-selector.js @@ -0,0 +1,61 @@ +import React from 'react'; + +import Icon from 'core-components/icon'; +import Tooltip from 'core-components/tooltip'; + +const icons = ['address-card', 'address-card-o', 'adjust', 'american-sign-language-interpreting', 'anchor', 'archive', 'area-chart', 'arrows', 'arrows-h', 'arrows-v', 'asl-interpreting (alias)', 'assistive-listening-systems', 'asterisk', 'audio-description', 'automobile (alias)', 'balance-scale', 'ban', 'bank (alias)', 'bar-chart', 'bar-chart-o (alias)', 'barcode', 'bars', 'bath', 'bathtub (alias)', 'battery (alias)', 'battery-0 (alias)', 'battery-1 (alias)', 'battery-2 (alias)', 'battery-3 (alias)', 'battery-4 (alias)', 'battery-empty', 'battery-full', 'battery-half', 'battery-quarter', 'battery-three-quarters', 'bed', 'beer', 'bell', 'bell-o', 'bell-slash', 'bell-slash-o', 'bicycle', 'binoculars', 'birthday-cake', 'blind', 'bluetooth', 'bluetooth-b', 'bolt', 'bomb', 'book', 'bookmark', 'bookmark-o', 'braille', 'briefcase', 'bug', 'building', 'building-o', 'bullhorn', 'bullseye', 'bus', 'cab (alias)', 'calculator', 'calendar', 'calendar-check-o', 'calendar-minus-o', 'calendar-o', 'calendar-plus-o', 'calendar-times-o', 'camera', 'camera-retro', 'car', 'caret-square-o-down', 'caret-square-o-left', 'caret-square-o-right', 'caret-square-o-up', 'cart-arrow-down', 'cart-plus', 'certificate', 'check', 'check-circle', 'check-circle-o', 'check-square', 'check-square-o', 'child', 'circle', 'circle-o', 'circle-o-notch', 'circle-thin', 'clock-o', 'clone', 'close (alias)', 'cloud', 'cloud-download', 'cloud-upload', 'code', 'code-fork', 'coffee', 'cog', 'cogs', 'comment', 'comment-o', 'commenting', 'commenting-o', 'comments', 'comments-o', 'compass', 'copyright', 'creative-commons', 'credit-card', 'credit-card-alt', 'crop', 'crosshairs', 'cube', 'cubes', 'cutlery', 'dashboard (alias)', 'database', 'deaf', 'deafness (alias)', 'desktop', 'diamond', 'dot-circle-o', 'download', 'drivers-license (alias)', 'drivers-license-o (alias)', 'edit (alias)', 'ellipsis-h', 'ellipsis-v', 'envelope', 'envelope-o', 'envelope-open', 'envelope-open-o', 'envelope-square', 'eraser', 'exchange', 'exclamation', 'exclamation-circle', 'exclamation-triangle', 'external-link', 'external-link-square', 'eye', 'eye-slash', 'eyedropper', 'fax', 'feed (alias)', 'female', 'fighter-jet', 'file-archive-o', 'file-audio-o', 'file-code-o', 'file-excel-o', 'file-image-o', 'file-movie-o (alias)', 'file-pdf-o', 'file-photo-o (alias)', 'file-picture-o (alias)', 'file-powerpoint-o', 'file-sound-o (alias)', 'file-video-o', 'file-word-o', 'file-zip-o (alias)', 'film', 'filter', 'fire', 'fire-extinguisher', 'flag', 'flag-checkered', 'flag-o', 'flash (alias)', 'flask', 'folder', 'folder-o', 'folder-open', 'folder-open-o', 'frown-o', 'futbol-o', 'gamepad', 'gavel', 'gear (alias)', 'gears (alias)', 'gift', 'glass', 'globe', 'graduation-cap', 'group (alias)', 'hand-grab-o (alias)', 'hand-lizard-o', 'hand-paper-o', 'hand-peace-o', 'hand-pointer-o', 'hand-rock-o', 'hand-scissors-o', 'hand-spock-o', 'hand-stop-o (alias)', 'handshake-o', 'hard-of-hearing (alias)', 'hashtag', 'hdd-o', 'headphones', 'heart', 'heart-o', 'heartbeat', 'history', 'home', 'hotel (alias)', 'hourglass', 'hourglass-1 (alias)', 'hourglass-2 (alias)', 'hourglass-3 (alias)', 'hourglass-end', 'hourglass-half', 'hourglass-o', 'hourglass-start', 'i-cursor', 'id-badge', 'id-card', 'id-card-o', 'image (alias)', 'inbox', 'industry', 'info', 'info-circle', 'institution (alias)', 'key', 'keyboard-o', 'language', 'laptop', 'leaf', 'legal (alias)', 'lemon-o', 'level-down', 'level-up', 'life-bouy (alias)', 'life-buoy (alias)', 'life-ring', 'life-saver (alias)', 'lightbulb-o', 'line-chart', 'location-arrow', 'lock', 'low-vision', 'magic', 'magnet', 'mail-forward (alias)', 'mail-reply (alias)', 'mail-reply-all (alias)', 'male', 'map', 'map-marker', 'map-o', 'map-pin', 'map-signs', 'meh-o', 'microchip', 'microphone', 'microphone-slash', 'minus', 'minus-circle', 'minus-square', 'minus-square-o', 'mobile', 'mobile-phone (alias)', 'money', 'moon-o', 'mortar-board (alias)', 'motorcycle', 'mouse-pointer', 'music', 'navicon (alias)', 'newspaper-o', 'object-group', 'object-ungroup', 'paint-brush', 'paper-plane', 'paper-plane-o', 'paw', 'pencil', 'pencil-square', 'pencil-square-o', 'percent', 'phone', 'phone-square', 'photo (alias)', 'picture-o', 'pie-chart', 'plane', 'plug', 'plus', 'plus-circle', 'plus-square', 'plus-square-o', 'podcast', 'power-off', 'print', 'puzzle-piece', 'qrcode', 'question', 'question-circle', 'question-circle-o', 'quote-left', 'quote-right', 'random', 'recycle', 'refresh', 'registered', 'remove (alias)', 'reorder (alias)', 'reply', 'reply-all', 'retweet', 'road', 'rocket', 'rss', 'rss-square', 's15 (alias)', 'search', 'search-minus', 'search-plus', 'send (alias)', 'send-o (alias)', 'server', 'share', 'share-alt', 'share-alt-square', 'share-square', 'share-square-o', 'shield', 'ship', 'shopping-bag', 'shopping-basket', 'shopping-cart', 'shower', 'sign-in', 'sign-language', 'sign-out', 'signal', 'signing (alias)', 'sitemap', 'sliders', 'smile-o', 'snowflake-o', 'soccer-ball-o (alias)', 'sort', 'sort-alpha-asc', 'sort-alpha-desc', 'sort-amount-asc', 'sort-amount-desc', 'sort-asc', 'sort-desc', 'sort-down (alias)', 'sort-numeric-asc', 'sort-numeric-desc', 'sort-up (alias)', 'space-shuttle', 'spinner', 'spoon', 'square', 'square-o', 'star', 'star-half', 'star-half-empty (alias)', 'star-half-full (alias)', 'star-half-o', 'star-o', 'sticky-note', 'sticky-note-o', 'street-view', 'suitcase', 'sun-o', 'support (alias)', 'tablet', 'tachometer', 'tag', 'tags', 'tasks', 'taxi', 'television', 'terminal', 'thermometer (alias)', 'thermometer-0 (alias)', 'thermometer-1 (alias)', 'thermometer-2 (alias)', 'thermometer-3 (alias)', 'thermometer-4 (alias)', 'thermometer-empty', 'thermometer-full', 'thermometer-half', 'thermometer-quarter', 'thermometer-three-quarters', 'thumb-tack', 'thumbs-down', 'thumbs-o-down', 'thumbs-o-up', 'thumbs-up', 'ticket', 'times', 'times-circle', 'times-circle-o', 'times-rectangle (alias)', 'times-rectangle-o (alias)', 'tint', 'toggle-down (alias)', 'toggle-left (alias)', 'toggle-off', 'toggle-on', 'toggle-right (alias)', 'toggle-up (alias)', 'trademark', 'trash', 'trash-o', 'tree', 'trophy', 'truck', 'tty', 'tv (alias)', 'umbrella', 'universal-access', 'university', 'unlock', 'unlock-alt', 'unsorted (alias)', 'upload', 'user', 'user-circle', 'user-circle-o', 'user-o', 'user-plus', 'user-secret', 'user-times', 'users', 'vcard (alias)', 'vcard-o (alias)', 'video-camera', 'volume-control-phone', 'volume-down', 'volume-off', 'volume-up', 'warning (alias)', 'wheelchair', 'wheelchair-alt', 'wifi', 'window-close', 'window-close-o', 'window-maximize', 'window-minimize', 'window-restore', 'wrench']; + +class IconSelector extends React.Component { + static propTypes = { + value: React.PropTypes.string, + onChange: React.PropTypes.func + }; + + static defaultProps = { + value: 'adjust' + }; + + state = { + show: false + }; + + render() { + return ( +
    + this.setState({show})}> + + + + +
    + ); + } + + renderTooltipContent() { + return ( +
    + {icons.map(this.renderTooltipIcon.bind(this))} +
    + ); + } + + renderTooltipIcon(name) { + return ( +
    + +
    + ); + } + + onIconClick(name) { + this.setState({ + show: false + }); + + if(this.props.onChange) { + this.props.onChange({target: {value: name}}); + } + } +} + +export default IconSelector; \ No newline at end of file diff --git a/client/src/core-components/icon-selector.scss b/client/src/core-components/icon-selector.scss new file mode 100644 index 00000000..562535eb --- /dev/null +++ b/client/src/core-components/icon-selector.scss @@ -0,0 +1,37 @@ +@import "../scss/vars"; + +.icon-selector { + + &__tooltip { + width: 400px; + height: 200px; + overflow-y: scroll; + } + + &__tooltip-icon { + display: inline-block; + cursor: pointer; + background-color: $light-grey; + padding: 6px 3px; + border-radius: 5px; + margin: 5px; + width: 32px; + height: 32px; + text-align: center; + + &:hover { + background-color: $medium-grey; + } + } + + &__current-icon { + cursor: pointer; + display: inline-block; + text-align: center; + padding: 6px 3px; + border-radius: 5px; + width: 32px; + height: 32px; + background-color: $light-grey; + } +} \ No newline at end of file diff --git a/client/src/core-components/icon.js b/client/src/core-components/icon.js index 6570bd90..d26123d1 100644 --- a/client/src/core-components/icon.js +++ b/client/src/core-components/icon.js @@ -5,6 +5,7 @@ class Icon extends React.Component { static propTypes = { name: React.PropTypes.string.isRequired, + color: React.PropTypes.string, size: React.PropTypes.string }; @@ -18,7 +19,7 @@ class Icon extends React.Component { renderFontIcon() { return ( -