Commit 291f1522 authored by Oleksii Savran's avatar Oleksii Savran Committed by Matija Obreza

CMS: Activity post components

- added ActivityPostDisplayPage
- ActivityPostDisplayPage divided into components
parent f5d0bf16
......@@ -84,6 +84,7 @@
"itemEditorWarn": "Don't use the ItemsEditor for more than 100 items!",
"lastModified": "Last modified",
"lastUpdated": "Last updated",
"lastUpdatedBy": "Last updated by {{who}}",
"modified": "Modified",
"name": "Name",
"newVersionAvailable": "New version available",
......
......@@ -572,6 +572,11 @@
"c": {
"documentationSection": {
"toc": "Table of contents"
},
"activityPostSection": {
"shareArticle": "Share this article",
"tweet": "Tweet!",
"shareLinkedin": "Share on LinkedIn"
}
}
},
......
// constants
import { LOADING_SLUG, RECEIVE_ARTICLE, RECEIVE_DOCUMENTATION, RECEIVE_MENU } from 'cms/constants';
import { LOADING_SLUG, RECEIVE_ARTICLE, RECEIVE_DOCUMENTATION, RECEIVE_MENU, RECEIVE_LAST_NEWS, RECEIVE_POST } from 'cms/constants';
// model
import Article from 'model/cms/Article';
import Menu from 'model/cms/Menu';
import ADoc from 'model/cms/ADoc';
import ActivityPost from 'model/cms/ActivityPost';
// service
import CmsService from 'service/genesys/CmsService';
import AsciiDocService from 'service/genesys/AsciiDocService';
......@@ -18,6 +19,16 @@ const receiveArticle = (slug: string, article: Article) => ({
payload: {slug, article},
});
const receiveLastNews = (lastNews: ActivityPost[]) => ({
type: RECEIVE_LAST_NEWS,
payload: lastNews,
});
const receivePost = (post: ActivityPost, id: number) => ({
type: RECEIVE_POST,
payload: {post, id},
});
export const loadArticle = (locale: string, slug: string) => (dispatch) => {
dispatch(loadingSlug(slug));
return CmsService.getArticleBySlugAndLang(locale, slug)
......@@ -48,3 +59,15 @@ export const loadMenu = (menuKey: string) => (dispatch) => {
CmsService.getMenu(menuKey)
.then((menuItem) => dispatch(receiveMenu(menuKey, menuItem)));
};
export const loadLastNews = () => (dispatch) => {
CmsService.getLastNews().then((lastNews) => {
dispatch(receiveLastNews(lastNews));
});
};
export const loadActivityPostById = (id: number) => (dispatch) => {
CmsService.getActivityPost(id).then((post) => {
dispatch(receivePost(post, id));
});
};
......@@ -2,6 +2,8 @@ export const RECEIVE_ARTICLE = 'cms/public/RECEIVE_ARTICLE';
export const RECEIVE_DOCUMENTATION = 'cms/public/RECEIVE_DOCUMENTATION';
export const LOADING_SLUG = 'cms/public/LOADING_SLUG';
export const RECEIVE_MENU = 'cms/public/RECEIVE_MENU';
export const RECEIVE_LAST_NEWS = 'cms/public/RECEIVE_LAST_NEWS';
export const RECEIVE_POST = 'cms/public/RECEIVE_POST';
// admin
export const ADMIN_RECEIVE_ARTICLE = 'cms/admin/RECEIVE_ARTICLE';
......
import { RECEIVE_ARTICLE, RECEIVE_DOCUMENTATION, RECEIVE_MENU, LOADING_SLUG } from 'cms/constants';
import { RECEIVE_ARTICLE, RECEIVE_DOCUMENTATION, RECEIVE_MENU, LOADING_SLUG, RECEIVE_LAST_NEWS, RECEIVE_POST } from 'cms/constants';
import update from 'immutability-helper';
const INITIAL_STATE: {
......@@ -6,13 +6,19 @@ const INITIAL_STATE: {
documentations: any,
loadingSlug: string,
menus: any,
lastNews: any,
news: any,
} = {
articles: {},
documentations: {},
menus: {},
loadingSlug: null,
lastNews: null,
news: {},
};
/* state will be changes */
export default function listPublic(state = INITIAL_STATE, action: { type?: string, payload?: any } = {type: '', payload: {}}) {
switch (action.type) {
......@@ -47,6 +53,20 @@ export default function listPublic(state = INITIAL_STATE, action: { type?: strin
},
});
}
case RECEIVE_LAST_NEWS: {
const lastNews = action.payload;
return update(state, {
lastNews: {$set: lastNews},
});
}
case RECEIVE_POST: {
const {post, id} = action.payload;
return update(state, {
news: {
[id]: {$set: post},
},
});
}
default:
return state;
}
......
......@@ -3,6 +3,13 @@ import * as Loadable from 'react-loadable';
const publicRoutes = [
{
path: '/content/news/:id/:slug',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "cms" */'cms/ui/ActivityPostDisplayPage'),
loading: Loading,
}),
},
{
path: '/content/:menuKey/:slug',
component: Loadable({
......
......@@ -59,6 +59,11 @@
"c": {
"documentationSection": {
"toc": "Table of contents"
},
"activityPostSection": {
"shareArticle": "Share this article",
"tweet": "Tweet!",
"shareLinkedin": "Share on LinkedIn"
}
}
},
......
import * as React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
// Actions
import { loadLastNews, loadActivityPostById } from 'cms/actions/public';
// Models
import ActivityPost from 'model/cms/ActivityPost';
import MenuItem from 'model/cms/MenuItem';
import Menu from 'model/cms/Menu';
// UI
import PageLayout from 'ui/layout/PageLayout';
import ContentHeader from 'ui/common/heading/ContentHeader';
import Loading from 'ui/common/Loading';
import ArticleSection from './c/ArticleSection';
import Grid from '@material-ui/core/Grid';
import MenuStepper from 'cms/ui/c/MenuStepper';
interface IActivityPostDisplayPage {
activityPost: ActivityPost;
lastNews: ActivityPost[];
loadLastNews: () => ActivityPost[];
loadActivityPostById: (id: number) => void;
slug: string;
id: number;
}
class ActivityPostDisplayPage extends React.Component<IActivityPostDisplayPage> {
protected static needs = [
({ params }) => loadActivityPostById(params.id),
({ params }) => params.lastNews ? loadLastNews() : null,
];
public componentWillReceiveProps(nextProps) {
const { loadLastNews, lastNews, activityPost, id, loadActivityPostById } = nextProps;
if (!lastNews) {
loadLastNews();
}
if (!activityPost || activityPost.id !== id) {
loadActivityPostById(id);
}
}
public componentWillMount() {
const { loadLastNews, lastNews, activityPost, id, loadActivityPostById } = this.props;
if (!lastNews) {
loadLastNews();
}
if (!activityPost) {
loadActivityPostById(id);
}
}
public render() {
const { lastNews, activityPost } = this.props;
const title = activityPost && activityPost.title.match(/>(.+)</)[1];
const lastNewsMenu = new Menu();
if (lastNews) {
lastNewsMenu.items = lastNews.map((lastNewsItem) => {
const newsMenuItem = new MenuItem();
newsMenuItem.id = lastNewsItem.id;
newsMenuItem.text = lastNewsItem.title.match(/>(.+)</)[1];
newsMenuItem.url = `/content/news/${newsMenuItem.id}/${
lastNewsItem.title
.match(/>(.+)</)[1]
.toLowerCase()
.replace(/\s/g, '-')
}`;
return newsMenuItem;
});
}
return (
<PageLayout withFooter>
{ !lastNews || !activityPost ? (
<Loading/>
) : (
<Grid container style={ {height: '100%'} }>
<ContentHeader title={ title }/>
<ArticleSection title={ title } body={ activityPost.body }/>
<MenuStepper menu={ lastNewsMenu }/>
</Grid>
) }
</PageLayout>
);
}
}
const mapStateToProps = (state, ownProps) => ({
slug: ownProps.match.params.slug,
id: +ownProps.match.params.id,
lastNews: state.cms.public.lastNews,
activityPost: state.cms.public.news[ownProps.match.params.id],
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
loadLastNews,
loadActivityPostById,
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(ActivityPostDisplayPage);
......@@ -66,9 +66,9 @@ class ContentPage extends React.Component<IContentPageProps> {
return (
<PageLayout withFooter>
{ article &&
<Grid container>
<Grid container style={ {height: '100%'} }>
<ContentHeader title={ article.title } />
<ArticleSection article={ article }/>
<ArticleSection body={ article.body }/>
{ menuKey && <MenuStepper menu={ menu } t={ t }/> }
</Grid>
}
......
......@@ -149,7 +149,7 @@ class ArticleEditPage extends React.Component<IArticleEditPageProps, any> {
t={ t }
/>
) : (
<ArticleSection article={ article } className={ classes.article }/>
<ArticleSection body={ article.body } className={ classes.article }/>
) }
</Grid>
</PageContents>
......
import * as React from 'react';
import { Link } from 'react-router-dom';
import { translate } from 'react-i18next';
import PrettyDate from 'ui/common/time/PrettyDate';
// Models
import ActivityPost from 'model/cms/ActivityPost';
// UI
import Card, { CardHeader, CardContent } from 'ui/common/Card';
import Grid from '@material-ui/core/Grid';
import withStyles from '@material-ui/core/styles/withStyles';
import Avatar from '@material-ui/core/Avatar';
const style = (theme) => ({
root: {
},
header: {
paddingBottom: '0 !important',
borderBottom: '0 !important',
},
/*tslint:disable*/
headerWrapper: {
display: 'flex' as 'flex',
flexDirection: 'row' as 'row',
},
titleText: {
color: 'black',
fontSize: '20px',
fontWeight: 500,
'& > p': {
marginBottom: 0,
},
},
avatar: {
margin: '1.143rem 0 0 1.429rem',
}
/*tslint:enable*/
});
const ActivityPostCard = ({classes, post, compact, t}: { post: ActivityPost, compact?: false, classes?: any, t: any }) => {
const urlTitle = post.title.match(/>(.+)</)[1].toLowerCase().replace(/\s/g, '-');
return compact ? (
<Grid item xs={ 12 }>
<Card className="p-20">
<Link to={ `/content/news/${post.id}/${urlTitle}` }>
{ <div dangerouslySetInnerHTML={ {__html: post.title} } /> }
</Link>
<div dangerouslySetInnerHTML={ {__html: post.body} } />
</Card>
</Grid>
)
:
(
<Grid item xs={ 12 }>
<Card className={ classes.root }>
<div className={ classes.headerWrapper }>
<Avatar className={ classes.avatar }>{ post.lastModifiedBy[0].toUpperCase() }</Avatar>
<CardHeader
className={ classes.header }
title={
<Link to={ `/content/news/${post.id}/${urlTitle}` }>
{ <div className={ classes.titleText } dangerouslySetInnerHTML={ {__html: post.title} } /> }
</Link>
}
subheader={
<span>
{ `${t('common:label.lastUpdatedBy', {who: post.lastModifiedBy})} ` }
<PrettyDate value={ new Date(post.lastModifiedDate) }/>
</span>
}
/>
</div>
<CardContent className="pb-10">
<div dangerouslySetInnerHTML={ {__html: post.body} } />
</CardContent>
</Card>
</Grid>
);
};
export default translate()(withStyles(style)(ActivityPostCard));
import * as React from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
import Article from 'model/cms/Article';
import { translate } from 'react-i18next';
import Grid from '@material-ui/core/Grid';
import withStyles from '@material-ui/core/styles/withStyles';
import { ExternalLink } from 'ui/common/Links';
const styles = () => ({
const styles = (theme) => ({
/*tslint:disable*/
root: {
minHeight: 'calc(100vh - 365px)',
height: 'calc(100% - 51px)',
},
wrapper: {
backgroundColor: 'white',
padding: '33px 40px 49px 40px',
height: '100%',
boxSizing: 'border-box' as 'border-box',
},
text: {
'& > p': {
fontFamily: 'Roboto-Light',
fontSize: '24px',
......@@ -34,26 +40,105 @@ const styles = () => ({
lineHeight: '30px',
},
},
social: {
borderTop: '1px solid #d5d4d3',
borderBottom: '1px solid #d5d4d3',
marginTop: '57px',
paddingTop: '20px',
paddingBottom: '19px',
fontSize: '1.3rem',
},
socialLink: {
backgroundColor: 'white', //
display: 'inline-block',
paddingLeft: '18px',
[theme.breakpoints.down('xs')]: {
paddingLeft: '0',
width: '50%',
textAlign: 'center' as 'center',
},
'&:hover > a > *': {
filter: 'brightness(0.5)',
transition: 'all, 0.1s',
}
},
socialText: {
[theme.breakpoints.down('sm')]: {
display: 'none',
},
},
/*tslint:enable*/
share: {
fontSize: '1.3rem',
display: 'inline-block',
color: '#4d4c46',
[theme.breakpoints.down('xs')]: {
width: '100%',
textAlign: 'center' as 'center',
marginBottom: '20px',
},
},
twitter: {
color: '#1da1f2',
verticalAlign: 'middle',
},
linkedin: {
color: '#0077b5',
verticalAlign: 'middle',
},
icon: {
fontSize: '3.6rem',
marginRight: '10px',
[theme.breakpoints.down('xs')]: {
marginRight: '0',
},
},
});
interface IArticleSectionProps extends React.ClassAttributes<any> {
article: Article;
classes: any;
className?: string;
title?: string;
t: any;
body: string;
}
class ArticleSection extends React.Component<IArticleSectionProps> {
public render() {
const {article, classes, className} = this.props;
const {classes, className, title, t, body} = this.props;
return(
<Grid item xs={ 9 } className={ className }>
<div className={ classes.root } dangerouslySetInnerHTML={ {__html: article.body} }/>
<Grid item xs={ 9 } className={ `${className} ${classes.root}` }>
<div className={ classes.wrapper }>
<div className={ classes.text } dangerouslySetInnerHTML={ {__html: body} }/>
{ title && (
<div className={ classes.social }>
<div className={ classes.share }>
{ t('cms.public.c.activityPostSection.shareArticle') }
</div>
<div className={ classes.socialLink }>
<ExternalLink link={ `https://twitter.com/intent/tweet?url=${window.location}&GenesysPGR` }>
<i className={ `fa fa-twitter-square ${classes.twitter} ${classes.icon}` }/>
<span className={ `${classes.twitter} ${classes.socialText}` }>
{ t('cms.public.c.activityPostSection.tweet') }
</span>
</ExternalLink>
</div>
<div className={ classes.socialLink }>
<ExternalLink link={ `https://www.linkedin.com/shareArticle?mini=true&url=${window.location}&title=${title}&summary=${body}&source=GenesysPGR` }>
<i className={ `fa fa-linkedin-square ${classes.linkedin} ${classes.icon}` }/>
<span className={ `${classes.linkedin} ${classes.socialText}` }>
{ t('cms.public.c.activityPostSection.shareLinkedin') }
</span>
</ExternalLink>
</div>
</div>
) }
</div>
</Grid>
);
}
}
export default withStyles(styles)(ArticleSection);
export default translate()(withStyles(styles)(ArticleSection));
......@@ -40,7 +40,7 @@ class MenuStepper extends React.Component<IMenuStepperProps> {
{ menu.items.map((menuItem: MenuItem) => (
<Link to={ menuItem.url }>
<div className={ classes.menuItem }>
{ t(`cms.${menuItem.text}`) }
{ t ? t(`cms.${menuItem.text}`) : menuItem.text }
</div>
</Link>
))
......
......@@ -22,6 +22,9 @@ const styles = (theme) => ({
minHeight: 'calc(100vh - 83px)',
flexDirection: 'column' as 'column',
maxWidth: '100%',
[theme.breakpoints.down('md')]: {
minHeight: 'calc(100vh - 3.5rem)',
},
},
children: {
flexGrow: 1,
......
......@@ -5,6 +5,11 @@ import {withStyles} from '@material-ui/core/styles';
import { translate } from 'react-i18next';
// import { navigateTo } from 'actions/navigation';
// Actions
import { loadLastNews } from 'cms/actions/public';
// Models
import ActivityPost from 'model/cms/ActivityPost';
import ContentHeader from 'ui/common/heading/ContentHeader';
import Grid from '@material-ui/core/Grid';
......@@ -21,6 +26,7 @@ import PageLayout, { PageContents } from 'ui/layout/PageLayout';
import Number from 'ui/common/Number';
import GridContainer from 'ui/layout/GridContainer';
import CropCard from 'crop/ui/c/CropCard';
import ActivityPostCard from 'cms/ui/c/ActivityPostCard';
const styles = (theme) => ({
/* tslint:disable */
......@@ -234,7 +240,8 @@ interface IWelcomeProps {
classes?: any;
serverInfo: any;
crops: any;
lastNews: ActivityPost[];
loadLastNews: () => ActivityPost[];
}
class WelcomePage extends React.Component<IWelcomeProps, any> {
......@@ -245,8 +252,15 @@ class WelcomePage extends React.Component<IWelcomeProps, any> {
this.state = { query: '', queryPlaceholder: t('public.p.welcome.search.placeholder') };
}
public componentWillMount() {
const { loadLastNews, lastNews } = this.props;
if (!lastNews) {
loadLastNews();
}
}
public render() {
const { classes, t, serverInfo, crops } = this.props;
const { classes, t, serverInfo, crops, lastNews } = this.props;
const Stats = ({children}) => (
<div className={ classes.stats }>
......@@ -443,6 +457,15 @@ share caracterization and evaluation information on material held in their colle
</Grid>
</Grid>
</Grid>
<Grid item xs={ 12 }>
{ lastNews &&
<PageContents className="pt-1rem">
<GridContainer>
{ lastNews.map((post) => <ActivityPostCard key={ post.id } post={ post } />) }
</GridContainer>
</PageContents>
}
</Grid>
<Grid item xs={ 12 }>
{ crops &&
<PageContents className="pt-1rem">
......@@ -460,10 +483,12 @@ share caracterization and evaluation information on material held in their colle
const mapStateToProps = (state) => ({
serverInfo: state.serverInfo.data,
crops: state.crop.public.list || undefined,
lastNews: state.cms.public.lastNews,
// accessToken: state.login.access_token,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
loadLastNews,
// navigateTo,
}, dispatch);
......
Markdown is supported
0% or .