Skip to content
Snippets Groups Projects
Commit d1ddee25 authored by Terry Mac-Tay's avatar Terry Mac-Tay
Browse files

Update repo

parent a4193ffe
No related branches found
No related tags found
No related merge requests found
Showing
with 1044 additions and 1 deletion
stages:
- test
- build_app
- scan
- deploy_app_testing
variables:
IMAGE_NAME: ${CI_REGISTRY_IMAGE}/app:latest
build_app:
stage: build_app
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
services:
- docker:dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build --cache-from $CI_REGISTRY/wimma-lab-2019/mysticons/cayac/demofrontend/app:latest --tag $CI_REGISTRY/wimma-lab-201$
- docker push $CI_REGISTRY/wimma-lab-2019/mysticons/cayac/demofrontend/app:latest
only:
- Development
container_scan_service:
stage: scan
variables:
ANCHORE_CLI_URL: "http://anchore-engine:8228/v1"
GIT_STRATEGY: none
image: docker.io/anchore/inline-scan:v0.3.3
services:
- name: docker.io/anchore/inline-scan:v0.3.3
alias: anchore-engine
command: ["start"]
- name: anchore/engine-db-preload:v0.3.3
alias: anchore-db
script:
- anchore-cli system wait
- anchore-cli registry add "$CI_REGISTRY" gitlab-ci-token "$CI_JOB_TOKEN" --skip-validate
- anchore_ci_tools.py -a -r --timeout 500 --image $IMAGE_NAME
artifacts:
name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}
paths:
- anchore-reports/*
only:
- Development
FROM node:12.2.0-alpine
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./
RUN npm install
RUN npm install --silent
# If you are building your code for production
# RUN npm ci --only=production
# Bundle app source
COPY . .
# default values
ENV REACT_APP_API_ROOT=http://localhost:8080/api
# expose to 4100 port
EXPOSE 4100
CMD [ "npm", "start" ]
# source-demo-front
# ![React + Redux Example App](project-logo.png)
[![RealWorld Frontend](https://img.shields.io/badge/realworld-frontend-%23783578.svg)](http://realworld.io)
> ### React + Redux codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld-example-apps) spec and API.
<a href="https://stackblitz.com/edit/react-redux-realworld" target="_blank"><img width="187" src="https://github.com/gothinkster/realworld/blob/master/media/edit_on_blitz.png?raw=true" /></a>&nbsp;&nbsp;<a href="https://thinkster.io/tutorials/build-a-real-world-react-redux-application" target="_blank"><img width="384" src="https://raw.githubusercontent.com/gothinkster/realworld/master/media/learn-btn-hr.png" /></a>
### [Demo](https://react-redux.realworld.io)&nbsp;&nbsp;&nbsp;&nbsp;[RealWorld](https://github.com/gothinkster/realworld)
Originally created for this [GH issue](https://github.com/reactjs/redux/issues/1353). The codebase is now feature complete; please submit bug fixes via pull requests & feedback via issues.
We also have notes in [**our wiki**](https://github.com/gothinkster/react-redux-realworld-example-app/wiki) about how the various patterns used in this codebase and how they work (thanks [@thejmazz](https://github.com/thejmazz)!)
## Getting started
You can view a live demo over at https://react-redux.realworld.io/
To get the frontend running locally:
- Clone this repo
- `npm install` to install all req'd dependencies
- `npm start` to start the local server (this project uses create-react-app)
Local web server will use port 4100 instead of standard React's port 3000 to prevent conflicts with some backends like Node or Rails. You can configure port in scripts section of `package.json`: we use [cross-env](https://github.com/kentcdodds/cross-env) to set environment variable PORT for React scripts, this is Windows-compatible way of setting environment variables.
Alternatively, you can add `.env` file in the root folder of project to set environment variables (use PORT to change webserver's port). This file will be ignored by git, so it is suitable for API keys and other sensitive stuff. Refer to [dotenv](https://github.com/motdotla/dotenv) and [React](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#adding-development-environment-variables-in-env) documentation for more details. Also, please remove setting variable via script section of `package.json` - `dotenv` never override variables if they are already set.
### Making requests to the backend API
For convenience, we have a live API server running at https://conduit.productionready.io/api for the application to make requests against. You can view [the API spec here](https://github.com/GoThinkster/productionready/blob/master/api) which contains all routes & responses for the server.
The source code for the backend server (available for Node, Rails and Django) can be found in the [main RealWorld repo](https://github.com/gothinkster/realworld).
If you want to change the API URL to a local server, simply edit `src/agent.js` and change `API_ROOT` to the local server's URL (i.e. `http://localhost:3000/api`)
## Functionality overview
The example application is a social blogging site (i.e. a Medium.com clone) called "Conduit". It uses a custom API for all requests, including authentication. You can view a live demo over at https://redux.productionready.io/
**General functionality:**
- Authenticate users via JWT (login/signup pages + logout button on settings page)
- CRU* users (sign up & settings page - no deleting required)
- CRUD Articles
- CR*D Comments on articles (no updating required)
- GET and display paginated lists of articles
- Favorite articles
- Follow other users
**The general page breakdown looks like this:**
- Home page (URL: /#/ )
- List of tags
- List of articles pulled from either Feed, Global, or by Tag
- Pagination for list of articles
- Sign in/Sign up pages (URL: /#/login, /#/register )
- Use JWT (store the token in localStorage)
- Settings page (URL: /#/settings )
- Editor page to create/edit articles (URL: /#/editor, /#/editor/article-slug-here )
- Article page (URL: /#/article/article-slug-here )
- Delete article button (only shown to article's author)
- Render markdown from server client side
- Comments section at bottom of page
- Delete comment button (only shown to comment's author)
- Profile page (URL: /#/@username, /#/@username/favorites )
- Show basic user info
- List of articles populated from author's created articles or author's favorited articles
<br />
[![Brought to you by Thinkster](https://raw.githubusercontent.com/gothinkster/realworld/master/media/end.png)](https://thinkster.io)
{
"name": "react-redux-realworld-example-app",
"version": "0.1.0",
"private": true,
"devDependencies": {
"cross-env": "^5.1.4",
"react-scripts": "1.1.1"
},
"dependencies": {
"history": "^4.6.3",
"marked": "^0.3.6",
"prop-types": "^15.5.10",
"react": "^16.3.0",
"react-dom": "^16.3.0",
"react-redux": "^5.0.7",
"react-router": "^4.1.2",
"react-router-dom": "^4.1.2",
"react-router-redux": "^5.0.0-alpha.6",
"redux": "^3.6.0",
"redux-devtools-extension": "^2.13.2",
"redux-logger": "^3.0.1",
"superagent": "^3.8.2",
"superagent-promise": "^1.1.0"
},
"scripts": {
"start": "cross-env PORT=4100 react-scripts start",
"build": "react-scripts build",
"test": "cross-env PORT=4100 react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
project-logo.png

70.5 KiB

public/favicon.ico

24.3 KiB

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="stylesheet" href="//demo.productionready.io/main.css">
<link href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic" rel="stylesheet" type="text/css">
<!--
Notice the use of %PUBLIC_URL% in the tag above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favico.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Conduit</title>
</head>
<body>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start`.
To create a production bundle, use `npm run build`.
-->
</body>
</html>
import superagentPromise from 'superagent-promise';
import _superagent from 'superagent';
const superagent = superagentPromise(_superagent, global.Promise);
const API_ROOT = 'https://conduit.productionready.io/api';
const encode = encodeURIComponent;
const responseBody = res => res.body;
let token = null;
const tokenPlugin = req => {
if (token) {
req.set('authorization', `Token ${token}`);
}
}
const requests = {
del: url =>
superagent.del(`${API_ROOT}${url}`).use(tokenPlugin).then(responseBody),
get: url =>
superagent.get(`${API_ROOT}${url}`).use(tokenPlugin).then(responseBody),
put: (url, body) =>
superagent.put(`${API_ROOT}${url}`, body).use(tokenPlugin).then(responseBody),
post: (url, body) =>
superagent.post(`${API_ROOT}${url}`, body).use(tokenPlugin).then(responseBody)
};
const Auth = {
current: () =>
requests.get('/user'),
login: (email, password) =>
requests.post('/users/login', { user: { email, password } }),
register: (username, email, password) =>
requests.post('/users', { user: { username, email, password } }),
save: user =>
requests.put('/user', { user })
};
const Tags = {
getAll: () => requests.get('/tags')
};
const limit = (count, p) => `limit=${count}&offset=${p ? p * count : 0}`;
const omitSlug = article => Object.assign({}, article, { slug: undefined })
const Articles = {
all: page =>
requests.get(`/articles?${limit(10, page)}`),
byAuthor: (author, page) =>
requests.get(`/articles?author=${encode(author)}&${limit(5, page)}`),
byTag: (tag, page) =>
requests.get(`/articles?tag=${encode(tag)}&${limit(10, page)}`),
del: slug =>
requests.del(`/articles/${slug}`),
favorite: slug =>
requests.post(`/articles/${slug}/favorite`),
favoritedBy: (author, page) =>
requests.get(`/articles?favorited=${encode(author)}&${limit(5, page)}`),
feed: () =>
requests.get('/articles/feed?limit=10&offset=0'),
get: slug =>
requests.get(`/articles/${slug}`),
unfavorite: slug =>
requests.del(`/articles/${slug}/favorite`),
update: article =>
requests.put(`/articles/${article.slug}`, { article: omitSlug(article) }),
create: article =>
requests.post('/articles', { article })
};
const Comments = {
create: (slug, comment) =>
requests.post(`/articles/${slug}/comments`, { comment }),
delete: (slug, commentId) =>
requests.del(`/articles/${slug}/comments/${commentId}`),
forArticle: slug =>
requests.get(`/articles/${slug}/comments`)
};
const Profile = {
follow: username =>
requests.post(`/profiles/${username}/follow`),
get: username =>
requests.get(`/profiles/${username}`),
unfollow: username =>
requests.del(`/profiles/${username}/follow`)
};
export default {
Articles,
Auth,
Comments,
Profile,
Tags,
setToken: _token => { token = _token; }
};
import agent from '../agent';
import Header from './Header';
import React from 'react';
import { connect } from 'react-redux';
import { APP_LOAD, REDIRECT } from '../constants/actionTypes';
import { Route, Switch } from 'react-router-dom';
import Article from '../components/Article';
import Editor from '../components/Editor';
import Home from '../components/Home';
import Login from '../components/Login';
import Profile from '../components/Profile';
import ProfileFavorites from '../components/ProfileFavorites';
import Register from '../components/Register';
import Settings from '../components/Settings';
import { store } from '../store';
import { push } from 'react-router-redux';
const mapStateToProps = state => {
return {
appLoaded: state.common.appLoaded,
appName: state.common.appName,
currentUser: state.common.currentUser,
redirectTo: state.common.redirectTo
}};
const mapDispatchToProps = dispatch => ({
onLoad: (payload, token) =>
dispatch({ type: APP_LOAD, payload, token, skipTracking: true }),
onRedirect: () =>
dispatch({ type: REDIRECT })
});
class App extends React.Component {
componentWillReceiveProps(nextProps) {
if (nextProps.redirectTo) {
// this.context.router.replace(nextProps.redirectTo);
store.dispatch(push(nextProps.redirectTo));
this.props.onRedirect();
}
}
componentWillMount() {
const token = window.localStorage.getItem('jwt');
if (token) {
agent.setToken(token);
}
this.props.onLoad(token ? agent.Auth.current() : null, token);
}
render() {
if (this.props.appLoaded) {
return (
<div>
<Header
appName={this.props.appName}
currentUser={this.props.currentUser} />
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/login" component={Login} />
<Route path="/register" component={Register} />
<Route path="/editor/:slug" component={Editor} />
<Route path="/editor" component={Editor} />
<Route path="/article/:id" component={Article} />
<Route path="/settings" component={Settings} />
<Route path="/@:username/favorites" component={ProfileFavorites} />
<Route path="/@:username" component={Profile} />
</Switch>
</div>
);
}
return (
<div>
<Header
appName={this.props.appName}
currentUser={this.props.currentUser} />
</div>
);
}
}
// App.contextTypes = {
// router: PropTypes.object.isRequired
// };
export default connect(mapStateToProps, mapDispatchToProps)(App);
import { Link } from 'react-router-dom';
import React from 'react';
import agent from '../../agent';
import { connect } from 'react-redux';
import { DELETE_ARTICLE } from '../../constants/actionTypes';
const mapDispatchToProps = dispatch => ({
onClickDelete: payload =>
dispatch({ type: DELETE_ARTICLE, payload })
});
const ArticleActions = props => {
const article = props.article;
const del = () => {
props.onClickDelete(agent.Articles.del(article.slug))
};
if (props.canModify) {
return (
<span>
<Link
to={`/editor/${article.slug}`}
className="btn btn-outline-secondary btn-sm">
<i className="ion-edit"></i> Edit Article
</Link>
<button className="btn btn-outline-danger btn-sm" onClick={del}>
<i className="ion-trash-a"></i> Delete Article
</button>
</span>
);
}
return (
<span>
</span>
);
};
export default connect(() => ({}), mapDispatchToProps)(ArticleActions);
import ArticleActions from './ArticleActions';
import { Link } from 'react-router-dom';
import React from 'react';
const ArticleMeta = props => {
const article = props.article;
return (
<div className="article-meta">
<Link to={`/@${article.author.username}`}>
<img src={article.author.image} alt={article.author.username} />
</Link>
<div className="info">
<Link to={`/@${article.author.username}`} className="author">
{article.author.username}
</Link>
<span className="date">
{new Date(article.createdAt).toDateString()}
</span>
</div>
<ArticleActions canModify={props.canModify} article={article} />
</div>
);
};
export default ArticleMeta;
import DeleteButton from './DeleteButton';
import { Link } from 'react-router-dom';
import React from 'react';
const Comment = props => {
const comment = props.comment;
const show = props.currentUser &&
props.currentUser.username === comment.author.username;
return (
<div className="card">
<div className="card-block">
<p className="card-text">{comment.body}</p>
</div>
<div className="card-footer">
<Link
to={`/@${comment.author.username}`}
className="comment-author">
<img src={comment.author.image} className="comment-author-img" alt={comment.author.username} />
</Link>
&nbsp;
<Link
to={`/@${comment.author.username}`}
className="comment-author">
{comment.author.username}
</Link>
<span className="date-posted">
{new Date(comment.createdAt).toDateString()}
</span>
<DeleteButton show={show} slug={props.slug} commentId={comment.id} />
</div>
</div>
);
};
export default Comment;
import CommentInput from './CommentInput';
import CommentList from './CommentList';
import { Link } from 'react-router-dom';
import React from 'react';
const CommentContainer = props => {
if (props.currentUser) {
return (
<div className="col-xs-12 col-md-8 offset-md-2">
<div>
<list-errors errors={props.errors}></list-errors>
<CommentInput slug={props.slug} currentUser={props.currentUser} />
</div>
<CommentList
comments={props.comments}
slug={props.slug}
currentUser={props.currentUser} />
</div>
);
} else {
return (
<div className="col-xs-12 col-md-8 offset-md-2">
<p>
<Link to="/login">Sign in</Link>
&nbsp;or&nbsp;
<Link to="/register">sign up</Link>
&nbsp;to add comments on this article.
</p>
<CommentList
comments={props.comments}
slug={props.slug}
currentUser={props.currentUser} />
</div>
);
}
};
export default CommentContainer;
import React from 'react';
import agent from '../../agent';
import { connect } from 'react-redux';
import { ADD_COMMENT } from '../../constants/actionTypes';
const mapDispatchToProps = dispatch => ({
onSubmit: payload =>
dispatch({ type: ADD_COMMENT, payload })
});
class CommentInput extends React.Component {
constructor() {
super();
this.state = {
body: ''
};
this.setBody = ev => {
this.setState({ body: ev.target.value });
};
this.createComment = ev => {
ev.preventDefault();
const payload = agent.Comments.create(this.props.slug,
{ body: this.state.body });
this.setState({ body: '' });
this.props.onSubmit(payload);
};
}
render() {
return (
<form className="card comment-form" onSubmit={this.createComment}>
<div className="card-block">
<textarea className="form-control"
placeholder="Write a comment..."
value={this.state.body}
onChange={this.setBody}
rows="3">
</textarea>
</div>
<div className="card-footer">
<img
src={this.props.currentUser.image}
className="comment-author-img"
alt={this.props.currentUser.username} />
<button
className="btn btn-sm btn-primary"
type="submit">
Post Comment
</button>
</div>
</form>
);
}
}
export default connect(() => ({}), mapDispatchToProps)(CommentInput);
import Comment from './Comment';
import React from 'react';
const CommentList = props => {
return (
<div>
{
props.comments.map(comment => {
return (
<Comment
comment={comment}
currentUser={props.currentUser}
slug={props.slug}
key={comment.id} />
);
})
}
</div>
);
};
export default CommentList;
import React from 'react';
import agent from '../../agent';
import { connect } from 'react-redux';
import { DELETE_COMMENT } from '../../constants/actionTypes';
const mapDispatchToProps = dispatch => ({
onClick: (payload, commentId) =>
dispatch({ type: DELETE_COMMENT, payload, commentId })
});
const DeleteButton = props => {
const del = () => {
const payload = agent.Comments.delete(props.slug, props.commentId);
props.onClick(payload, props.commentId);
};
if (props.show) {
return (
<span className="mod-options">
<i className="ion-trash-a" onClick={del}></i>
</span>
);
}
return null;
};
export default connect(() => ({}), mapDispatchToProps)(DeleteButton);
import ArticleMeta from './ArticleMeta';
import CommentContainer from './CommentContainer';
import React from 'react';
import agent from '../../agent';
import { connect } from 'react-redux';
import marked from 'marked';
import { ARTICLE_PAGE_LOADED, ARTICLE_PAGE_UNLOADED } from '../../constants/actionTypes';
const mapStateToProps = state => ({
...state.article,
currentUser: state.common.currentUser
});
const mapDispatchToProps = dispatch => ({
onLoad: payload =>
dispatch({ type: ARTICLE_PAGE_LOADED, payload }),
onUnload: () =>
dispatch({ type: ARTICLE_PAGE_UNLOADED })
});
class Article extends React.Component {
componentWillMount() {
this.props.onLoad(Promise.all([
agent.Articles.get(this.props.match.params.id),
agent.Comments.forArticle(this.props.match.params.id)
]));
}
componentWillUnmount() {
this.props.onUnload();
}
render() {
if (!this.props.article) {
return null;
}
const markup = { __html: marked(this.props.article.body, { sanitize: true }) };
const canModify = this.props.currentUser &&
this.props.currentUser.username === this.props.article.author.username;
return (
<div className="article-page">
<div className="banner">
<div className="container">
<h1>{this.props.article.title}</h1>
<ArticleMeta
article={this.props.article}
canModify={canModify} />
</div>
</div>
<div className="container page">
<div className="row article-content">
<div className="col-xs-12">
<div dangerouslySetInnerHTML={markup}></div>
<ul className="tag-list">
{
this.props.article.tagList.map(tag => {
return (
<li
className="tag-default tag-pill tag-outline"
key={tag}>
{tag}
</li>
);
})
}
</ul>
</div>
</div>
<hr />
<div className="article-actions">
</div>
<div className="row">
<CommentContainer
comments={this.props.comments || []}
errors={this.props.commentErrors}
slug={this.props.match.params.id}
currentUser={this.props.currentUser} />
</div>
</div>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Article);
import ArticlePreview from './ArticlePreview';
import ListPagination from './ListPagination';
import React from 'react';
const ArticleList = props => {
if (!props.articles) {
return (
<div className="article-preview">Loading...</div>
);
}
if (props.articles.length === 0) {
return (
<div className="article-preview">
No articles are here... yet.
</div>
);
}
return (
<div>
{
props.articles.map(article => {
return (
<ArticlePreview article={article} key={article.slug} />
);
})
}
<ListPagination
pager={props.pager}
articlesCount={props.articlesCount}
currentPage={props.currentPage} />
</div>
);
};
export default ArticleList;
import React from 'react';
import { Link } from 'react-router-dom';
import agent from '../agent';
import { connect } from 'react-redux';
import { ARTICLE_FAVORITED, ARTICLE_UNFAVORITED } from '../constants/actionTypes';
const FAVORITED_CLASS = 'btn btn-sm btn-primary';
const NOT_FAVORITED_CLASS = 'btn btn-sm btn-outline-primary';
const mapDispatchToProps = dispatch => ({
favorite: slug => dispatch({
type: ARTICLE_FAVORITED,
payload: agent.Articles.favorite(slug)
}),
unfavorite: slug => dispatch({
type: ARTICLE_UNFAVORITED,
payload: agent.Articles.unfavorite(slug)
})
});
const ArticlePreview = props => {
const article = props.article;
const favoriteButtonClass = article.favorited ?
FAVORITED_CLASS :
NOT_FAVORITED_CLASS;
const handleClick = ev => {
ev.preventDefault();
if (article.favorited) {
props.unfavorite(article.slug);
} else {
props.favorite(article.slug);
}
};
return (
<div className="article-preview">
<div className="article-meta">
<Link to={`/@${article.author.username}`}>
<img src={article.author.image} alt={article.author.username} />
</Link>
<div className="info">
<Link className="author" to={`/@${article.author.username}`}>
{article.author.username}
</Link>
<span className="date">
{new Date(article.createdAt).toDateString()}
</span>
</div>
<div className="pull-xs-right">
<button className={favoriteButtonClass} onClick={handleClick}>
<i className="ion-heart"></i> {article.favoritesCount}
</button>
</div>
</div>
<Link to={`/article/${article.slug}`} className="preview-link">
<h1>{article.title}</h1>
<p>{article.description}</p>
<span>Read more...</span>
<ul className="tag-list">
{
article.tagList.map(tag => {
return (
<li className="tag-default tag-pill tag-outline" key={tag}>
{tag}
</li>
)
})
}
</ul>
</Link>
</div>
);
}
export default connect(() => ({}), mapDispatchToProps)(ArticlePreview);
import ListErrors from './ListErrors';
import React from 'react';
import agent from '../agent';
import { connect } from 'react-redux';
import {
ADD_TAG,
EDITOR_PAGE_LOADED,
REMOVE_TAG,
ARTICLE_SUBMITTED,
EDITOR_PAGE_UNLOADED,
UPDATE_FIELD_EDITOR
} from '../constants/actionTypes';
const mapStateToProps = state => ({
...state.editor
});
const mapDispatchToProps = dispatch => ({
onAddTag: () =>
dispatch({ type: ADD_TAG }),
onLoad: payload =>
dispatch({ type: EDITOR_PAGE_LOADED, payload }),
onRemoveTag: tag =>
dispatch({ type: REMOVE_TAG, tag }),
onSubmit: payload =>
dispatch({ type: ARTICLE_SUBMITTED, payload }),
onUnload: payload =>
dispatch({ type: EDITOR_PAGE_UNLOADED }),
onUpdateField: (key, value) =>
dispatch({ type: UPDATE_FIELD_EDITOR, key, value })
});
class Editor extends React.Component {
constructor() {
super();
const updateFieldEvent =
key => ev => this.props.onUpdateField(key, ev.target.value);
this.changeTitle = updateFieldEvent('title');
this.changeDescription = updateFieldEvent('description');
this.changeBody = updateFieldEvent('body');
this.changeTagInput = updateFieldEvent('tagInput');
this.watchForEnter = ev => {
if (ev.keyCode === 13) {
ev.preventDefault();
this.props.onAddTag();
}
};
this.removeTagHandler = tag => () => {
this.props.onRemoveTag(tag);
};
this.submitForm = ev => {
ev.preventDefault();
const article = {
title: this.props.title,
description: this.props.description,
body: this.props.body,
tagList: this.props.tagList
};
const slug = { slug: this.props.articleSlug };
const promise = this.props.articleSlug ?
agent.Articles.update(Object.assign(article, slug)) :
agent.Articles.create(article);
this.props.onSubmit(promise);
};
}
componentWillReceiveProps(nextProps) {
if (this.props.match.params.slug !== nextProps.match.params.slug) {
if (nextProps.match.params.slug) {
this.props.onUnload();
return this.props.onLoad(agent.Articles.get(this.props.match.params.slug));
}
this.props.onLoad(null);
}
}
componentWillMount() {
if (this.props.match.params.slug) {
return this.props.onLoad(agent.Articles.get(this.props.match.params.slug));
}
this.props.onLoad(null);
}
componentWillUnmount() {
this.props.onUnload();
}
render() {
return (
<div className="editor-page">
<div className="container page">
<div className="row">
<div className="col-md-10 offset-md-1 col-xs-12">
<ListErrors errors={this.props.errors}></ListErrors>
<form>
<fieldset>
<fieldset className="form-group">
<input
className="form-control form-control-lg"
type="text"
placeholder="Article Title"
value={this.props.title}
onChange={this.changeTitle} />
</fieldset>
<fieldset className="form-group">
<input
className="form-control"
type="text"
placeholder="What's this article about?"
value={this.props.description}
onChange={this.changeDescription} />
</fieldset>
<fieldset className="form-group">
<textarea
className="form-control"
rows="8"
placeholder="Write your article (in markdown)"
value={this.props.body}
onChange={this.changeBody}>
</textarea>
</fieldset>
<fieldset className="form-group">
<input
className="form-control"
type="text"
placeholder="Enter tags"
value={this.props.tagInput}
onChange={this.changeTagInput}
onKeyUp={this.watchForEnter} />
<div className="tag-list">
{
(this.props.tagList || []).map(tag => {
return (
<span className="tag-default tag-pill" key={tag}>
<i className="ion-close-round"
onClick={this.removeTagHandler(tag)}>
</i>
{tag}
</span>
);
})
}
</div>
</fieldset>
<button
className="btn btn-lg pull-xs-right btn-primary"
type="button"
disabled={this.props.inProgress}
onClick={this.submitForm}>
Publish Article
</button>
</fieldset>
</form>
</div>
</div>
</div>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Editor);
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment