Skip to content

Commit

Permalink
ft(article-crud): enable users create and view articles
Browse files Browse the repository at this point in the history
- add routes for article creation and viewing
- add necessary pages for article creation and viewing
- write tests for all functionality

[Finishes #164798256]
  • Loading branch information
patrickf949 committed May 21, 2019
1 parent 69d4ddb commit c04ee4c
Show file tree
Hide file tree
Showing 53 changed files with 1,116 additions and 169 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/.idea/
coverage/
package-lock.json
dist/
src/tests/__snapshots__/*
dist/*
*/__snapshots__/*
.DS_Store
src/.DS_Store/*
39 changes: 0 additions & 39 deletions dist/index_bundle.js

This file was deleted.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@
"@material-ui/core": "^3.9.3",
"@material-ui/icons": "^3.0.2",
"axios": "^0.18.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^24.8.0",
"bootstrap": "^4.3.1",
"classnames": "^2.2.6",
"bootstrap": "^4.3.1",
"enzyme": "^3.9.0",
"enzyme-adapter-react-16": "^1.12.1",
"express": "^4.16.4",
Expand Down
3 changes: 3 additions & 0 deletions src/actions/LoginAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ export const userLoginRequest = userData => async dispatch => {

sessionStorage.setItem("token", response.data.user.token);
sessionStorage.setItem("username", response.data.user.username);
sessionStorage.setItem("email", response.data.user.email);

dispatch(successLogin(response));

toast.success(`Welcome ${response.data.user.username}. Login Successful`, {
position: toast.POSITION.TOP_RIGHT,
autoClose: 2000,
Expand Down
44 changes: 44 additions & 0 deletions src/actions/articleCreateEditAction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import axios from "axios";
import { ARTICLE_SUCCESS, ARTICLE_FAIL } from "./types";
import { toast } from "react-toastify";

export const successCreateArticle = data => {
return {
type: ARTICLE_SUCCESS,
payload: data.article.slug
};
};

export const articleCreateEditAction = (article, url, method, props) => {
return async dispatch => {
try {
const response = await axios({
method: method,
url: url,
data: article,
headers: {
Authorization: `Bearer ${sessionStorage.getItem("token")}`
}
});

toast.dismiss();
dispatch(successCreateArticle(response.data));
props.history.push("/article/" + response.data.article.slug);
} catch (error) {
if (error.response) {
dispatch({
type: ARTICLE_FAIL
});
toast.dismiss();
const errors = error.response.data.errors;
for (var key in errors) {
toast.error(`${key}: ${errors[key]}`, {
position: toast.POSITION.TOP_RIGHT,
autoClose: false,
hideProgressBar: false
});
}
}
}
};
};
19 changes: 19 additions & 0 deletions src/actions/getArticle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import axios from "axios";
import { FETCH_ARTICLE_SUCCESS } from "./types";

export const getArticleAction = slug => dispatch => {
return axios
.get(
"https://ah-backend-prime-staging.herokuapp.com/api/v1/articles/" + slug
)
.then(response => {
dispatch(fetchArticlesSuccess(response.data));
});
};

const fetchArticlesSuccess = payload => {
return {
type: FETCH_ARTICLE_SUCCESS,
payload: payload
};
};
18 changes: 12 additions & 6 deletions src/actions/getArticles.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import axios from "axios";
import { FETCH_ARTICLES_SUCCESS } from "./types";

export const getArticlesAction = () => dispatch => {
export const getArticlesAction = (kwargs = " ") => dispatch => {
return axios
.get("https://my-json-server.typicode.com/patrickf949/demo/posts")
.get(
"https://ah-backend-prime-staging.herokuapp.com/api/v1/articles/" + kwargs
)
.then(response => {
dispatch({
type: FETCH_ARTICLES_SUCCESS,
payload: response.data
});
dispatch(fetchArticlesSuccess(response.data.results));
});
};

const fetchArticlesSuccess = payload => {
return {
type: FETCH_ARTICLES_SUCCESS,
payload: payload
};
};
3 changes: 3 additions & 0 deletions src/actions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ export const PROFILE_FETCHING = "PROFILE_FETCHING";
export const PROFILE_FETCHED = "PROFILE_FETCHED";
export const PROFILE_FETCH_FAILED = "PROFILE_FETCH_FAILED";
export const PROFILE_EDIT_SUCCESS = "PROFILE_EDIT_SUCCESS";
export const ARTICLE_SUCCESS = "ARTICLE_SUCCESS";
export const ARTICLE_FAIL = "ARTICLE_FAIL";
export const FETCH_ARTICLE_SUCCESS = "FETCH_ARTICLE_SUCCESS";
76 changes: 76 additions & 0 deletions src/components/articles/createArticleComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from "react";
import "../../styles/createArticles.scss";
import "../../styles/singleArticle.scss";
const CreateArticleComponent = props => {
const { onChange, onSubmit, onUpload, image } = props;
return (
<div className="container-fluid mt-5">
<div className="row">
<div className="col col-lg-3" />
<div className="col col-lg-6">
<h1>Tell A story</h1>
<form>
<div className="form-group">
<input
onChange={onChange}
className="form-control"
placeholder="title"
name="title"
id="title"
/>
</div>
<div className="form-group">
<input
onChange={onChange}
className="form-control"
placeholder="description"
name="description"
id="description"
/>
</div>
<div className="form-group">
<textarea
onChange={onChange}
className="form-control"
placeholder="Body"
name="body"
id="body"
/>
</div>
<div className="form-group">
<input
onChange={onChange}
className="form-control"
placeholder="tags"
name="tags"
id="tags"
/>
<div>
<i>Separate your tags with a comma</i>
</div>
</div>
<div className="image-div">
<img src={image} className="image-fluid" />
<input
type="file"
name="imageUpload"
onChange={event => onUpload(event.target.files)}
id="fitz"
/>
</div>

<input name="image" />
<div className="form-group">
<button onClick={onSubmit} className="btn btn-primary">
Publish Story
</button>
</div>
</form>
</div>
<div className="col col-lg-3" />
</div>
</div>
);
};

export default CreateArticleComponent;
91 changes: 91 additions & 0 deletions src/components/articles/createArticlePage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import "../../styles/app.scss";
import { articleCreateEditAction } from "../../actions/articleCreateEditAction";
import CreateArticleComponent from "./createArticleComponent";
import firebase from "../../firebase/config";

export class CreateArticlePage extends Component {
constructor(props) {
super(props);
this.state = {
title: "",
body: "",
description: "",
tags: "",
image: "",
isUploaded: false
};
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.onUpload = this.onUpload.bind(this);
}

/* istanbul ignore next */
onUpload(files) {
const image = URL.createObjectURL(event.target.files[0]);
this.setState({ image });

const task = firebase
.storage()
.ref(`images/${files[0].name}`)
.put(files[0]);

task.then(res => {
firebase
.storage()
.ref(`images/${files[0].name}`)
.getDownloadURL()
.then(url => {
console.log(url);
this.setState({ isUploaded: true, image: url });
});
});

task.on("state_changed", snapshot => {
const isUploaded = true;
this.setState({ isUploaded: isUploaded });
});
}

onChange(e) {
this.setState({ [e.target.name]: e.target.value });
}
onSubmit(e) {
e.preventDefault();
const { title, body, description, tags, image } = this.state;
const messageObject = {
title: title,
body: body,
description: description,
tags: tags.split(","),
image: image
};

this.props.articleCreateEditAction(
messageObject,
"https://ah-backend-prime-staging.herokuapp.com/api/v1/articles/",
"post",
this.props
);
}

render() {
const { image } = this.state;
return (
<div>
<CreateArticleComponent
onChange={this.onChange}
onUpload={this.onUpload}
onSubmit={this.onSubmit}
image={image}
/>
</div>
);
}
}

export default connect(
null,
{ articleCreateEditAction }
)(CreateArticlePage);
97 changes: 97 additions & 0 deletions src/components/articles/singleArticle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import moment from "moment";
import { Link } from "react-router-dom";

import "../../styles/singleArticle.scss";
import { getArticleAction } from "../../actions/getArticle";

export class SingleArticleComponent extends Component {
componentDidMount() {
this.props.getArticleAction(this.props.match.params.slug);
}

render() {
const { article } = this.props;
const oneArticle = article ? article.article : null;
const singleArticle = oneArticle ? (
<div className="container">
<div className="single-article-div">
<div className="inner">
<h2 className="article-title">{oneArticle.title}</h2>
<div>
<div>
<div className="">
<div className="article-profile">
<img
src={
oneArticle.author.image
? oneArticle.author.image
: "https://static.wixstatic.com/media/2cd43b_4ef9dc9638cc431b956e1c36862b519b~mv2.png?dn="
}
alt="profile pic"
/>
</div>
<div className="article-author">
{oneArticle.author.username}
<button className="btn btn-primary follow">follow</button>

<div className="article-time-stamps">
<div>
Created at{" "}
{moment(oneArticle.createdAt).format(
"h:mm:ss a [on] MMMM Do YYYY."
)}
</div>
<div>
modified at{" "}
{moment(oneArticle.updatedAt).format(
"h:mm:ss a [on] MMMM Do YYYY."
)}
.
</div>
<div>{oneArticle.reading_time}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="article-single">
<div className="article-description-body">
<p>{oneArticle.description}</p>
</div>
<div className="image-div">
<img
src={
oneArticle.image
? oneArticle.image
: "https://previews.123rf.com/images/twindesigner/twindesigner1708/twin" +
"designer170800135/84202763-ah-brush-letter-logo-design-with-black-" +
"circle-creative-brushed-letters-icon-logo-.jpg"
}
className="article-image"
/>
</div>

<div className="article-description-body">
<p style={{ fontWeight: "light" }}>{oneArticle.body}</p>
</div>
</div>
</div>
) : (
<div>The article you have requested does not exist</div>
);
return <div>{singleArticle}</div>;
}
}

export const mapStateToProps = state => ({
article: state.getArticleReducer.article
});

export default connect(
mapStateToProps,
{ getArticleAction }
)(SingleArticleComponent);
Loading

0 comments on commit c04ee4c

Please sign in to comment.