diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..93225ea --- /dev/null +++ b/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": "current" + } + } + ], + "@babel/preset-react" + ], + "plugins": ["syntax-class-properties", "transform-class-properties"] +} diff --git a/client/components/App.js b/client/App.js similarity index 70% rename from client/components/App.js rename to client/App.js index cd41208..a2ad359 100644 --- a/client/components/App.js +++ b/client/App.js @@ -1,14 +1,12 @@ import React, { Component, Fragment } from 'react'; -import { - Link, Route, Switch, withRouter -} from 'react-router-dom'; +import { Route, Switch, withRouter } from 'react-router-dom'; import io from 'socket.io-client'; -import Header1 from './Header1'; -import Header2 from './Header2'; -import Header3 from './Header3'; -import LeftContainer from './LeftContainer'; -import RightContainer from './RightContainer'; -import { createMuiTheme, makeStyles, ThemeProvider } from '@material-ui/core/styles' +import { createMuiTheme } from '@material-ui/core/styles'; +import HeaderMainPage from './components/HeaderMainPage'; +import RegisterPage from './components/RegisterPage'; +import LeftContainer from './containers/LeftContainer'; +import RightContainer from './containers/RightContainer'; +import LoginPage from './components/LoginPage'; const theme = createMuiTheme({ palette: { @@ -21,11 +19,13 @@ const theme = createMuiTheme({ } }); +const socket = io('http://localhost:3000/'); + + class App extends Component { constructor(props) { super(props); this.state = { - socket: io('http://localhost:3000/'), user_id: null, username: null, password: null, @@ -42,17 +42,18 @@ class App extends Component { location: null, category: null, minrating: 1, - friends: [] + friends: [], + source: 1 }, categories: ['Attraction', 'Food', 'Accomodation', 'Activity'], locations: [], - postData: { - location: null, - category: null, - rating: null, - recommendation: null, - review_text: null - }, + // postData: { + // location: null, + // category: null, + // rating: null, + // recommendation: null, + // review_text: null + // }, follow_user: null, // {user_id: #, username: # }, likedPosts: [], numberLikes: null // drilling this down to rerender @@ -61,10 +62,8 @@ class App extends Component { this.signup = this.signup.bind(this); this.login = this.login.bind(this); this.logout = this.logout.bind(this); - this.handleChangeFirstname = this.handleChangeFirstname.bind(this); - this.handleChangeLastname = this.handleChangeLastname.bind(this); - this.handleChangeUsername = this.handleChangeUsername.bind(this); - this.handleChangePassword = this.handleChangePassword.bind(this); + this.handleChangeItem = this.handleChangeItem.bind(this); + // methods to handle filtering reviews this.handleChangeLocation = this.handleChangeLocation.bind(this); this.handleChangeCategory = this.handleChangeCategory.bind(this); this.handleChangeRating = this.handleChangeRating.bind(this); @@ -73,12 +72,12 @@ class App extends Component { this.fetchPosts = this.fetchPosts.bind(this); this.filterPosts = this.filterPosts.bind(this); // methods for posting - this.handlePostForm = this.handlePostForm.bind(this); - this.handleChangeRecommendation = this.handleChangeRecommendation.bind(this); - this.handleChangeReview = this.handleChangeReview.bind(this); - this.handleChangePostRating = this.handleChangePostRating.bind(this); - this.handleChangePostLocation = this.handleChangePostLocation.bind(this); - this.handleChangePostCategory = this.handleChangePostCategory.bind(this); + // this.handlePostForm = this.handlePostForm.bind(this); + // this.handleChangeRecommendation = this.handleChangeRecommendation.bind(this); + // this.handleChangeReview = this.handleChangeReview.bind(this); + // this.handleChangePostRating = this.handleChangePostRating.bind(this); + // this.handleChangePostLocation = this.handleChangePostLocation.bind(this); + // this.handleChangePostCategory = this.handleChangePostCategory.bind(this); this.parseLocation = this.parseLocation.bind(this); // methods for following this.handleChangeFollow = this.handleChangeFollow.bind(this); @@ -86,6 +85,16 @@ class App extends Component { // like or unlike a post this.handleLikeReview = this.handleLikeReview.bind(this); //this.likeReview = this.handleLikeReview.bind(this); + // delete review + this.handleDeleteReview = this.handleDeleteReview.bind(this); + + socket.on('post', (post) => { + post = this.parseLocation(post); + this.setState({ + posts: [post, ...this.state.posts] + }); + this.filterPosts(); + }); } handleChangeFollow(e, value) { @@ -116,7 +125,7 @@ class App extends Component { // Attempt to connect to room (catches refreshes during session) this.fetchUser() .then(({ username }) => { - if (username) this.state.socket.emit('room', username); + if (username) socket.emit('room', username); }); // fetch posts only once @@ -126,14 +135,7 @@ class App extends Component { //this.fetchLikes(); // Handle recieved posts - this.state.socket.on('post', (post) => { - post = this.parseLocation(post); - this.setState({ - posts: [post, ...this.state.posts] - }); - this.filterPosts(); - }); - + } @@ -216,20 +218,8 @@ class App extends Component { .catch((err) => console.error(err)); } - handleChangeFirstname(event) { - this.setState({ firstname: event.target.value }); - } - - handleChangeLastname(event) { - this.setState({ lastname: event.target.value }); - } - - handleChangeUsername(event) { - this.setState({ username: event.target.value }); - } - - handleChangePassword(event) { - this.setState({ password: event.target.value }); + handleChangeItem(event) { + this.setState({ [event.target.id]: event.target.value }); } handleChangeLocation(event) { @@ -244,67 +234,67 @@ class App extends Component { this.setState({ postFilter: { ...this.state.postFilter, minrating: event.target.value } }); } - handleChangeFriendsFilter(event, value) { - this.setState({ postFilter: { ...this.state.postFilter, friends: value.map((a) => a.user_id) } }); + handleChangeFriendsFilter(event) { + this.setState({ postFilter: { ...this.state.postFilter, source: event.target.value } }); // value.map(a => String(a.user_id)) } // Post form Handles - handleChangeRecommendation(event) { - this.setState({ postData: { ...this.state.postData, recommendation: event.target.value } }); - } - - handleChangeReview(event) { - this.setState({ postData: { ...this.state.postData, reviewText: event.target.value } }); - } - - handleChangePostLocation(event, value) { - if (!value.structured_formatting.hasOwnProperty('secondary_text')) { - this.setState({ postLocationMessage: 'Please specify a more specific location' }); - } - this.setState({ postLocationMessage: null }); - this.setState({ postData: { ...this.state.postData, location: `${value.structured_formatting.main_text} ${value.structured_formatting.secondary_text}` } }); - } - - handleChangePostCategory(event) { - this.setState({ postData: { ...this.state.postData, category: event.target.value } }); - } - - handleChangePostRating(event) { - this.setState({ postData: { ...this.state.postData, rating: event.target.value } }); - } + // handleChangeRecommendation(event) { + // this.setState({ postData: { ...this.state.postData, recommendation: event.target.value } }); + // } + + // handleChangeReview(event) { + // this.setState({ postData: { ...this.state.postData, reviewText: event.target.value } }); + // } + + // handleChangePostLocation(event, value) { + // if (!value.structured_formatting.hasOwnProperty('secondary_text')) { + // this.setState({ postLocationMessage: 'Please specify a more specific location' }); + // } + // this.setState({ postLocationMessage: null }); + // this.setState({ postData: { ...this.state.postData, location: `${value.structured_formatting.main_text} ${value.structured_formatting.secondary_text}` } }); + // } + + // handleChangePostCategory(event) { + // this.setState({ postData: { ...this.state.postData, category: event.target.value } }); + // } + + // handleChangePostRating(event) { + // this.setState({ postData: { ...this.state.postData, rating: event.target.value } }); + // } // post form data to server - handlePostForm() { - console.log('location to go with post request', this.state.postData.location); - const data = { - username: this.state.username, - location: this.state.postData.location, - category: this.state.postData.category, - rating: this.state.postData.rating, - recommendation: this.state.postData.recommendation, - reviewText: this.state.postData.reviewText - }; - - fetch('/users/submitreview', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }) - .then(() => { - console.log('Success'); - }) - .catch(() => { - console.error('Error'); - }); - } + // handlePostForm() { + // console.log('location to go with post request', this.state.postData.location); + // const data = { + // username: this.state.username, + // location: this.state.postData.location, + // category: this.state.postData.category, + // rating: this.state.postData.rating, + // recommendation: this.state.postData.recommendation, + // reviewText: this.state.postData.reviewText + // }; + + // fetch('/users/submitreview', { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json' + // }, + // body: JSON.stringify(data) + // }) + // .then(() => { + // console.log('Success'); + // }) + // .catch(() => { + // console.error('Error'); + // }); + // } signup() { - // post data. if successfull go to header3 + // post data. if successfull go to RegisterPage const data = { firstname: this.state.firstname, lastname: this.state.lastname, @@ -337,17 +327,17 @@ class App extends Component { }); this.filterPosts(); // redirect to new page - this.props.history.push('/header2'); + this.props.history.push('/mainpage'); } }); } login() { const data = { - username: document.getElementById('username').value, - password: document.getElementById('password').value + username: this.state.username, + password: this.state.password }; - this.state.socket.emit('room', data.username); + socket.emit('room', data.username); fetch('/users/login', { method: 'POST', @@ -371,7 +361,7 @@ class App extends Component { user_id: json[0], firstname: json[2] }); - this.props.history.push('/header2'); + this.props.history.push('/mainpage'); this.fetchUsers(json[0]); } if (!(Array.isArray(json[3]) && json[3].length > 0)) { @@ -392,7 +382,7 @@ class App extends Component { this.filterPosts(); this.fetchLikes(); // redirect to new page - this.props.history.push('/header2'); + this.props.history.push('/mainpage'); } }); }); @@ -428,24 +418,34 @@ class App extends Component { let newfilteredPosts = this.state.posts; newfilteredPosts = newfilteredPosts.filter((post) => { let result = true; - if (this.state.postFilter.location && (this.state.postFilter.location !== post.location)) { + if (this.state.postFilter.location && (this.state.postFilter.location !== 'all') && (this.state.postFilter.location !== post.location)) { result = false; } - if (this.state.postFilter.category && (this.state.postFilter.category !== post.category)) { + if (this.state.postFilter.category && (this.state.postFilter.category !== 'all') && (this.state.postFilter.category !== post.category)) { result = false; } - if (this.state.postFilter.minrating && (this.state.minrating > post.rating)) { + if (this.state.postFilter.minrating && (this.state.postFilter.minrating > post.rating)) { result = false; } - if (this.state.postFilter.friends.length > 0 - && !(this.state.postFilter.friends.includes(Number(post.created_by))) - ) { + if (this.state.postFilter.source === 2 && (post.username !== this.state.username )) { result = false; } return result; }); this.setState({ filteredPosts: newfilteredPosts }); } + + handleDeleteReview(event) { + event.preventDefault(); + const num = Number(event.target.id); + fetch('/users/deletereview', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ id: num }) + }); + } fetchUsers(user_id) { fetch(`users/getusers?userId=${user_id}`, { @@ -474,31 +474,28 @@ class App extends Component { }); } - render() { - return ( - } + handleChangeItem={this.handleChangeItem}/>} /> - + - +
} /> - } + handleChangeItem={this.handleChangeItem} />} />
diff --git a/client/actions/actions.js b/client/actions/actions.js new file mode 100644 index 0000000..a449357 --- /dev/null +++ b/client/actions/actions.js @@ -0,0 +1,70 @@ +import * as types from '../constants/actionTypes' + +export const handleChangePostCategory = (category) => ({ + type: types.HC_POST_CATEGORY, + payload: category.target.value +}) + +export const handleChangePostLocation = (structuredFormatting) => ({ + type: types.HC_POST_LOCATION, + payload: structuredFormatting +}) + +export const handleChangePostRating = (rating) => ({ + type: types.HC_POST_RATING, + payload: rating.target.value +}) + +export const handleChangeRecommendation = (recommendation) => ({ + type: types.HC_RECOMMENDATION, + payload: recommendation.target.value +}) + +export const handleChangeReview = (review) => ({ + type: types.HC_REVIEW, + payload: review.target.value +}) + +// export const handlePostForm = () => ({ +// type: types.H_POST_FORM, +// payload: null +// }); + +// using redux-thunk +export const handlePostForm = () => { + return (dispatch, getState) => { + console.log(getState()) + const state = getState().frecco; + const data = { + username: state.username, + location: state.postData.location, + category: state.postData.category, + rating: state.postData.rating, + recommendation: state.postData.recommendation, + reviewText: state.postData.reviewText + }; + fetch('/users/submitreview', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }) + .then(() => { + console.log('Success'); + }) + .catch(() => { + console.error('Error'); + }); + } +} + +// export const handleChangeFollow = (follow_user) => ({ +// type: types.HC_FOLLOW, +// payload: follow_user +// }) + +// export const addFollow = () => ({ +// type: types.ADD_FOLLOW, +// payload: +// }) \ No newline at end of file diff --git a/client/components/FeedContainer.js b/client/components/FeedContainer.js index 0d5e87e..8e3b2a5 100644 --- a/client/components/FeedContainer.js +++ b/client/components/FeedContainer.js @@ -1,5 +1,5 @@ -import React, {Component} from "react"; -import FeedItem from "./FeedItem"; +import React from 'react'; +import FeedItem from './FeedItem'; function FeedContainer(props) { // loop through array of posts and render here @@ -23,6 +23,8 @@ function FeedContainer(props) { handleLikeReview={props.handleLikeReview} likes={post.likes} numberLikes={props.numberLikes} + handleDeleteReview={props.handleDeleteReview} + current_username = {props.current_username} //likeReview={props.likeReview} />); } @@ -33,4 +35,4 @@ function FeedContainer(props) { ); } -export default FeedContainer; \ No newline at end of file +export default FeedContainer; diff --git a/client/components/FeedContainer.test.js b/client/components/FeedContainer.test.js new file mode 100644 index 0000000..5a15080 --- /dev/null +++ b/client/components/FeedContainer.test.js @@ -0,0 +1,50 @@ +import React from 'react'; +import Enzyme, { shallow } from 'enzyme'; +import EnzymeAdapter from 'enzyme-adapter-react-16'; +import FeedContainer from './FeedContainer'; + +Enzyme.configure({ adapter: new EnzymeAdapter() }); + +const defaultProps = { + filteredPosts: [ + { + category: 'Food', + created_by: 29, + id: 193, + likes: 0, + location: '2nd Avenue, New York, NY, USA', + locationDetail: "Joey Pepperoni's Pizza", + rating: 5, + recommendation: 'Zac', + review_text: 'Food of the gods', + username: 'zach' + } + ], + likedPosts: [] +}; + +const setup = (props = {}) => { + const setupProps = { ...defaultProps, ...props }; + return shallow(); +}; + +// Renders w/o error +it('renders without error', () => { + const wrapper = setup(); + const feedContainer = wrapper.find('#feed-container'); + expect(feedContainer.length).toBe(1); +}); + +// Renders no items when filteredPosts is empty +it('renders no feedItem components when filteredPosts is empty', () => { + const wrapper = setup({ filteredPosts: [] }); + const feedContainer = wrapper.find('#feed-container'); + expect(feedContainer.children().length).toBe(0); +}); + +// Renders children when filteredPosts is populated +it('renders feedItem component when filteredPost contains post data', () => { + const wrapper = setup(); + const feedContainer = wrapper.find('#feed-container'); + expect(feedContainer.children().length).toBe(1); +}); diff --git a/client/components/FeedItem.js b/client/components/FeedItem.js index 65a8e07..cb722e9 100644 --- a/client/components/FeedItem.js +++ b/client/components/FeedItem.js @@ -1,11 +1,25 @@ import React from 'react'; -import { Button } from '@material-ui/core'; import { AiOutlineHeart, AiFillHeart } from 'react-icons/ai'; function FeedItem(props) { let heartIcon; - if (props.isLiked === false) heartIcon = props.handleLikeReview(event, props.id, props.isLiked)} style={{ cursor: 'pointer' }}/> - else heartIcon = props.handleLikeReview(event, props.id, props.isLiked)} style={{ cursor: 'pointer' }}/> + !props.isLiked + ? (heartIcon = ( + + props.handleLikeReview(event, props.id, props.isLiked) + } + style={{ cursor: 'pointer' }} + /> + )) + : (heartIcon = ( + + props.handleLikeReview(event, props.id, props.isLiked) + } + style={{ cursor: 'pointer' }} + /> + )); return (
@@ -21,11 +35,13 @@ function FeedItem(props) {
{props.review_text}
- {heartIcon} {props.likes} likes + {heartIcon} {props.likes} likes +    + {props.current_username === props.username && + }
) } - -export default FeedItem; \ No newline at end of file +export default FeedItem; diff --git a/client/components/FeedItem.test.js b/client/components/FeedItem.test.js new file mode 100644 index 0000000..6ecbb78 --- /dev/null +++ b/client/components/FeedItem.test.js @@ -0,0 +1,53 @@ +import React from 'react'; +import Enzyme, { shallow } from 'enzyme'; +import EnzymeAdapter from 'enzyme-adapter-react-16'; +import FeedItem from './FeedItem'; + +Enzyme.configure({ adapter: new EnzymeAdapter() }); + +const defaultProps = { + category: 'Food', + created_by: 29, + id: 193, + likes: 0, + location: '2nd Avenue, New York, NY, USA', + locationDetail: "Joey Pepperoni's Pizza", + rating: 5, + recommendation: 'Zac', + review_text: 'Food of the gods', + username: 'zach' +}; + +const setup = (props = {}) => { + const setupProps = { ...defaultProps, ...props }; + return shallow(); +}; + +it('renders without error', () => { + const wrapper = setup(); + const feedItem = wrapper.find('.feed-item'); + expect(feedItem.length).toBe(1); +}); + +it('renders four div children', () => { + const wrapper = setup(); + const feedItem = wrapper.find('.feed-item'); + expect(feedItem.children().find('div').length).toBe(4); +}); + +describe('renders heart icon correctly', () => { + it('renders heart outline & not filled heart when `isLiked` prop is false', () => { + const wrapper = setup(); + const heartOutline = wrapper.find('AiOutlineHeart'); + const filledHeart = wrapper.find('AiFillHeart'); + expect(heartOutline.length).toBe(1); + expect(filledHeart.length).toBe(0); + }); + it('renders filled heart & not heart outline when `isLiked prop is true`', () => { + const wrapper = setup({ isLiked: true }); + const heartOutline = wrapper.find('AiOutlineHeart'); + const filledHeart = wrapper.find('AiFillHeart'); + expect(heartOutline.length).toBe(0); + expect(filledHeart.length).toBe(1); + }); +}); diff --git a/client/components/FilterForm.js b/client/components/FilterForm.js index 3a910a7..0a59ebc 100644 --- a/client/components/FilterForm.js +++ b/client/components/FilterForm.js @@ -2,17 +2,16 @@ import React, {Component} from "react"; import Button from '@material-ui/core/Button'; import InputLabel from '@material-ui/core/InputLabel'; import MenuItem from '@material-ui/core/MenuItem'; -import FormHelperText from '@material-ui/core/FormHelperText'; +// import FormHelperText from '@material-ui/core/FormHelperText'; import FormControl from '@material-ui/core/FormControl'; import Select from '@material-ui/core/Select'; -import Slider from '@material-ui/core/Slider'; -import Chip from '@material-ui/core/Chip'; -import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +// import Slider from '@material-ui/core/Slider'; +// import TextField from '@material-ui/core/TextField'; +// import Autocomplete from '@material-ui/lab/Autocomplete'; export default function FilterForm(props) { - const locationItems = []; + const locationItems = []; props.locations.forEach((loc, i) => ( locationItems.push({loc}) )); @@ -27,14 +26,16 @@ export default function FilterForm(props) { Location    Category    @@ -49,7 +50,7 @@ export default function FilterForm(props) {    -
+ {/*
props.handleChangeFriendsFilter(e,value)} /> -
+
*/} + + Source of Review + +    diff --git a/client/components/Header1.js b/client/components/Header1.js deleted file mode 100644 index 76a1ccf..0000000 --- a/client/components/Header1.js +++ /dev/null @@ -1,21 +0,0 @@ -import React, {Component} from "react"; -import { Link, withRouter } from 'react-router-dom'; -import Button from '@material-ui/core/Button'; -import Input from '@material-ui/core/Input'; -const Header1 = (props) => { - return ( -
-

Frecco

- -    - - - - - - {props.message} -
- ) -}; - -export default Header1; \ No newline at end of file diff --git a/client/components/Header2.js b/client/components/Header2.js deleted file mode 100644 index 8e04d03..0000000 --- a/client/components/Header2.js +++ /dev/null @@ -1,14 +0,0 @@ -import React, { Component } from "react"; -import Button from '@material-ui/core/Button'; - -const Header2 = (props) => { - return ( -
-

frecco

-
Welcome to Frecco, {props.username}!
- -
- ) -}; - -export default Header2; \ No newline at end of file diff --git a/client/components/Header3.js b/client/components/Header3.js deleted file mode 100644 index eb3da93..0000000 --- a/client/components/Header3.js +++ /dev/null @@ -1,19 +0,0 @@ -import React, {Component} from "react"; -import Button from '@material-ui/core/Button'; -import Input from '@material-ui/core/Input'; - -const Header3 = (props) => { - return ( -
-

Frecco

- -    -    -    -    - {props.message} -
- ) -}; - -export default Header3; \ No newline at end of file diff --git a/client/components/HeaderMainPage.js b/client/components/HeaderMainPage.js new file mode 100644 index 0000000..e8db32c --- /dev/null +++ b/client/components/HeaderMainPage.js @@ -0,0 +1,14 @@ +import React from 'react'; +import Button from '@material-ui/core/Button'; + +const HeaderMainPage = (props) => { + return ( +
+

Frecco 2.0

+
Welcome to Frecco, {props.username}!
+ +
+ ); +}; + +export default HeaderMainPage; diff --git a/client/components/LeftContainer.js b/client/components/LeftContainer.js deleted file mode 100644 index 3c18da2..0000000 --- a/client/components/LeftContainer.js +++ /dev/null @@ -1,38 +0,0 @@ -import React, {Component} from "react"; -import PostForm from "./PostForm"; -import FriendsContainer from "./FriendsContainer"; -import UserDashboard from "./UserDashboard"; - -class LeftContainer extends React.Component { - - render() { - return ( -
- - - -
- ) - } -} - -export default LeftContainer; - diff --git a/client/components/LoginPage.js b/client/components/LoginPage.js new file mode 100644 index 0000000..8ea4059 --- /dev/null +++ b/client/components/LoginPage.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import Button from '@material-ui/core/Button'; +import Input from '@material-ui/core/Input'; + +const LoginPage = (props) => { + return ( +
+

Frecco 2.0

+ +    + + + + + + {props.message} +
+ ); +}; + +export default LoginPage; diff --git a/client/components/PostForm.js b/client/components/PostForm.js index 9b326fc..28916ba 100644 --- a/client/components/PostForm.js +++ b/client/components/PostForm.js @@ -27,7 +27,7 @@ function PostForm(props) {    Category - {categoryOptions} diff --git a/client/components/RegisterPage.js b/client/components/RegisterPage.js new file mode 100644 index 0000000..90ed6fc --- /dev/null +++ b/client/components/RegisterPage.js @@ -0,0 +1,19 @@ +import React from 'react'; +import Button from '@material-ui/core/Button'; +import Input from '@material-ui/core/Input'; + +const RegisterPage = (props) => { + return ( +
+

Frecco

+ +    +    +    +    + {props.message} +
+ ); +}; + +export default RegisterPage; diff --git a/client/components/RightContainer.js b/client/components/RightContainer.js deleted file mode 100644 index dea1273..0000000 --- a/client/components/RightContainer.js +++ /dev/null @@ -1,35 +0,0 @@ -import React, {Component} from "react"; -import PostForm from "./PostForm"; -import FeedContainer from "./FeedContainer"; -import FilterForm from "./FilterForm"; - -class RightContainer extends React.Component { - - render() { - - return ( -
- - -
- - ) - } -} - -export default RightContainer; \ No newline at end of file diff --git a/client/constants/actionTypes.js b/client/constants/actionTypes.js new file mode 100644 index 0000000..1699b70 --- /dev/null +++ b/client/constants/actionTypes.js @@ -0,0 +1,6 @@ +export const HC_POST_CATEGORY = 'HC_POST_CATEGORY'; +export const HC_POST_LOCATION = 'HC_POST_LOCATION'; +export const HC_POST_RATING = 'HC_POST_RATING'; +export const HC_RECOMMENDATION = 'HC_RECOMMENDATION'; +export const HC_REVIEW = 'HC_REVIEW'; +export const H_POST_FORM = 'H_POST_FORM'; diff --git a/client/containers/LeftContainer.js b/client/containers/LeftContainer.js new file mode 100644 index 0000000..f2c3d25 --- /dev/null +++ b/client/containers/LeftContainer.js @@ -0,0 +1,54 @@ +import React, { Component } from 'react'; +import {connect} from 'react-redux'; +import PostForm from '../components/PostForm'; +import FriendsContainer from '../components/FriendsContainer'; +import UserDashboard from '../components/UserDashboard'; +import {handleChangePostCategory, handleChangePostLocation, handleChangePostRating, handleChangeRecommendation, handleChangeReview, handlePostForm} from '../actions/actions'; + +const mapStateToProps = state => ({ + username: state.frecco.username, + firstname: state.frecco.firstname, + postLocationMessage: state.frecco.postLocationMessage, + postData: state.frecco.postData +}); + +const mapDispatchToProps = (dispatch) => ({ + handleChangePostCategory: (category) => dispatch(handleChangePostCategory(category)), + handleChangePostLocation: (e,value) => dispatch(handleChangePostLocation(value.structured_formatting)), + handleChangePostRating: (rating) => dispatch(handleChangePostRating(rating)), + handleChangeRecommendation: (recommendation) => dispatch(handleChangeRecommendation(recommendation)), + handleChangeReview: (review) => dispatch(handleChangeReview(review)), + handlePostForm: () => dispatch(handlePostForm()) +}); + +class LeftContainer extends Component { + render() { + return ( +
+ + + +
+ ); + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(LeftContainer); diff --git a/client/containers/MainContainer.js b/client/containers/MainContainer.js deleted file mode 100644 index e69de29..0000000 diff --git a/client/containers/MainContainer2.js b/client/containers/MainContainer2.js deleted file mode 100644 index e69de29..0000000 diff --git a/client/containers/RightContainer.js b/client/containers/RightContainer.js new file mode 100644 index 0000000..776e91c --- /dev/null +++ b/client/containers/RightContainer.js @@ -0,0 +1,33 @@ +import React, { Component } from 'react'; +import FeedContainer from '../components/FeedContainer'; +import FilterForm from '../components/FilterForm'; + +class RightContainer extends Component { + render() { + return ( +
+ + +
+ ); + } +} + +export default RightContainer; diff --git a/client/index.js b/client/index.js index 81f9e2c..f9296a5 100644 --- a/client/index.js +++ b/client/index.js @@ -1,8 +1,11 @@ -import React, { forwardRef } from 'react' -import App from './components/App' -import { render } from 'react-dom' -import styles from './stylesheets/styles.scss' -import { Link, Router, Route, Switch, withRouter } from 'react-router-dom'; +import React from 'react'; + +import { Provider } from 'react-redux'; +import { render } from 'react-dom'; +import { Router } from 'react-router-dom'; +import App from './App'; +import styles from './stylesheets/styles.scss'; import history from './components/history'; +import store from './store'; -render(, document.getElementById('root')); +render(, document.getElementById('root')); diff --git a/client/reducers/index.js b/client/reducers/index.js new file mode 100644 index 0000000..a1ca9b2 --- /dev/null +++ b/client/reducers/index.js @@ -0,0 +1,12 @@ +import { combineReducers } from 'redux'; + +// import all reducers here +import leftReducer from './leftReducer'; + +// combine reducers +const reducers = combineReducers({ + // if we had other reducers, they would go here + frecco: leftReducer +}); + +export default reducers; diff --git a/client/reducers/leftReducer.js b/client/reducers/leftReducer.js new file mode 100644 index 0000000..828544d --- /dev/null +++ b/client/reducers/leftReducer.js @@ -0,0 +1,147 @@ +import * as types from '../constants/actionTypes'; + +const initialState = { + username: null, + firstname: null, + postLocationMessage: null, + posts: [], + potentialFollows: [], + postData: { + location: null, + category: null, + rating: null, + recommendation: null, + review_text: null + } +}; + +const leftReducer = (state=initialState, action) => { + let username = state.username + let firstname = state.firstname + let postLocationMessage = state.postLocationMessage + let posts = state.posts.slice(); + let potentialFollows = state.potentialFollows.slice(); + let postData = {...state.postData}; + + switch(action.type) { + case types.HC_POST_CATEGORY: + return { + ...state, + postData:{ + ...state.postData, + category: action.payload + } + } + + case types.HC_POST_LOCATION: + const structuredFormatting = action.payload; + console.log(structuredFormatting); + console.log("post location") + return { + ...state, + postLocationMessage: null, + postData: { + ...state.postData, + location: `${structuredFormatting.main_text} ${structuredFormatting.secondary_text}` + } + } + case types.HC_POST_RATING: + return { + ...state, + postData: { + ...state.postData, + rating: action.payload + } + } + + case types.HC_RECOMMENDATION: + return { + ...state, + postData: { + ...state.postData, + recommendation: action.payload + } + } + + case types.HC_REVIEW: + return { + ...state, + postData: { + ...state.postData, + reviewText: action.payload + } + } + + + case types.H_POST_FORM: + const data = { + username: username, + location: postData.location, + category: postData.category, + rating: postData.rating, + recommendation: postData.recommendation, + reviewText: postData.reviewText + }; + console.log("posting form") + + return { + ...state + }; + + default: + return state; + } +}; + +export default leftReducer; + + +// handlePostForm() { +// console.log('location to go with post request', this.state.postData.location); +// const data = { +// username: this.state.username, +// location: this.state.postData.location, +// category: this.state.postData.category, +// rating: this.state.postData.rating, +// recommendation: this.state.postData.recommendation, +// reviewText: this.state.postData.reviewText +// }; + +// fetch('/users/submitreview', { +// method: 'POST', +// headers: { +// 'Content-Type': 'application/json' +// }, +// body: JSON.stringify(data) +// }) +// .then(() => { +// console.log('Success'); +// }) +// .catch(() => { +// console.error('Error'); +// }); +// } + +// handleChangeRecommendation(event) { +// this.setState({ postData: { ...this.state.postData, recommendation: event.target.value } }); +// } + +// handleChangeReview(event) { +// this.setState({ postData: { ...this.state.postData, reviewText: event.target.value } }); +// } + +// handleChangePostRating(event) { +// this.setState({ postData: { ...this.state.postData, rating: event.target.value } }); +// } + +// handleChangePostCategory(event) { +// this.setState({ postData: { ...this.state.postData, category: event.target.value } }); +// } + +// handleChangePostLocation(event, value) { +// // if (!value.structured_formatting.hasOwnProperty('secondary_text')) { +// // this.setState({ postLocationMessage: 'Please specify a more specific location' }); +// // } +// this.setState({ postLocationMessage: null }); +// this.setState({ postData: { ...this.state.postData, location: `${value.structured_formatting.main_text} ${value.structured_formatting.secondary_text}` } }); +// } \ No newline at end of file diff --git a/client/store.js b/client/store.js new file mode 100644 index 0000000..1f9c979 --- /dev/null +++ b/client/store.js @@ -0,0 +1,13 @@ +import { createStore, applyMiddleware } from 'redux'; +import { composeWithDevTools } from 'redux-devtools-extension'; +import thunk from 'redux-thunk'; +import reducers from './reducers/index'; + +// const store = createStore( +// reducers, applyMiddleware(thunk) +// ); + +const store = createStore(reducers, composeWithDevTools( + applyMiddleware(thunk))); + +export default store; diff --git a/client/stylesheets/styles.scss b/client/stylesheets/styles.scss index 325eec2..6888bc6 100644 --- a/client/stylesheets/styles.scss +++ b/client/stylesheets/styles.scss @@ -1,5 +1,7 @@ -$snow: #fafbfb; -$orange: #fe5722; +$snow: #c6d8dd; +$purple: rgb(226, 202, 226); +$darkpurple: rgb(187, 145, 187); +$lightpurple: rgb(241, 232, 241); $grey-font:#5a5a5a; $border: #dedede; $border-radius: 5px; @@ -12,13 +14,18 @@ $box-shadow: 0px 3px 5px 1px rgba(0,0,0,0.09); } +body { + background-color: $purple; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='%239C92AC' fill-opacity='0.4' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E"); +} + #header1, #header2, #header3 { padding-right: 5%; display: flex; justify-content: flex-end; margin: 10px; h1 { - color: $orange; + color: purple; font-weight: 800; padding-left: 5%; flex: 1; @@ -41,7 +48,7 @@ $box-shadow: 0px 3px 5px 1px rgba(0,0,0,0.09); #filter-form { height: auto; padding: 10px; - background-color: #ecf0f1; + background-color: $darkpurple; display: flex; align-self: bottom; justify-content: center; @@ -58,19 +65,21 @@ $box-shadow: 0px 3px 5px 1px rgba(0,0,0,0.09); #right-container { flex: 3; padding: 20px; - background-color: $snow; + background-color: $purple; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='%239C92AC' fill-opacity='0.4' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E"); overflow: auto; } #left-container { flex: 2; - background-color: $snow; + background-color: $purple; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='%239C92AC' fill-opacity='0.4' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E"); padding: 20px; overflow:auto; } #post-form-header { - background-color: #ecf0f1; + background-color: $darkpurple; padding: 15px; border-top: .5px solid $border; border-left: .5px solid $border; @@ -84,7 +93,7 @@ $box-shadow: 0px 3px 5px 1px rgba(0,0,0,0.09); } #post-form { - background-color: white; + background-color: $lightpurple; padding: 20px; border: .5px solid $border; border-radius: 0px 0px 5px 5px; @@ -100,7 +109,7 @@ $box-shadow: 0px 3px 5px 1px rgba(0,0,0,0.09); #feed-container { padding: 5px; - background-color: white; + background-color: $lightpurple; padding: 20px; border: .5px solid $border; border-radius: 0px 0x 5px 5px; @@ -114,7 +123,7 @@ $box-shadow: 0px 3px 5px 1px rgba(0,0,0,0.09); border: .5px solid $border; border-radius: $border-radius; &:hover { - background-color: $snow; + background-color: $purple; } } @@ -156,7 +165,7 @@ $box-shadow: 0px 3px 5px 1px rgba(0,0,0,0.09); justify-content: space-around; align-items: flex-start; color: $grey-font; - background-color: white; + background-color: $lightpurple; padding: 20px; height: 100px; border: .5px solid $border; @@ -177,7 +186,7 @@ $box-shadow: 0px 3px 5px 1px rgba(0,0,0,0.09); #friends-container { - background-color: white; + background-color: $lightpurple; padding: 15px; margin: 10px 0px; height: auto; diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..10a185a --- /dev/null +++ b/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + setupFilesAfterEnv: ['/setUpTests.js'] +}; diff --git a/package.json b/package.json index d24e040..a3ace10 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,10 @@ "main": "index.js", "scripts": { "start": "NODE_ENV=production nodemon server/server.js", + "prestart": "NODE_ENV=production webpack", "build": "NODE_ENV=production webpack", - "dev": "NODE_ENV=development concurrently \"nodemon server/server.js\" \"webpack-dev-server --open --hot\"" + "dev": "NODE_ENV=development concurrently \"nodemon server/server.js\" \"webpack-dev-server --open --hot\"", + "test": "jest --watch" }, "nodemonConfig": { "ignore": [ @@ -28,18 +30,25 @@ "@babel/core": "^7.8.7", "@babel/preset-env": "^7.8.7", "@babel/preset-react": "^7.8.3", + "babel-jest": "^25.1.0", "babel-loader": "^8.0.6", + "babel-plugin-syntax-class-properties": "^6.13.0", + "babel-plugin-transform-class-properties": "^6.24.1", "babel-preset-react": "^6.24.1", "concurrently": "^5.1.0", "css-loader": "^3.4.2", + "enzyme": "^3.11.0", + "enzyme-adapter-react-16": "^1.15.2", "eslint": "^6.8.0", "eslint-config-airbnb-base": "^14.0.0", "eslint-plugin-import": "^2.20.1", "eslint-plugin-jquery": "^1.5.1", "eslint-plugin-react": "^7.18.3", + "jest": "^25.1.0", "node-sass": "^4.13.1", "nodemon": "^2.0.2", "react-addons-test-utils": "^15.6.2", + "react-test-renderer": "^16.13.1", "sass-loader": "^8.0.2", "style-loader": "^1.1.3", "webpack": "^4.42.0", @@ -70,6 +79,7 @@ "redux": "^4.0.5", "redux-devtools-extension": "^2.13.8", "redux-saga": "^1.1.3", + "redux-thunk": "^2.3.0", "socket.io": "^2.3.0", "socket.io-client": "^2.3.0", "ws": "^7.2.3" diff --git a/server/controllers/cookieController.js b/server/controllers/cookieController.js index bfdc19e..ce46be0 100644 --- a/server/controllers/cookieController.js +++ b/server/controllers/cookieController.js @@ -24,6 +24,7 @@ cookieController.setSSID = (req, res, next) => { try { // Sets cookie res.cookie('xpgnssid', res.locals.cookie); + console.log('res.locals.cookie: ', res.locals.cookie); return next(); } catch { return next({ @@ -36,6 +37,7 @@ cookieController.setSSID = (req, res, next) => { // Removes authentication cookie cookieController.removeSSID = (req, res, next) => { + console.log('cookieController.removeSSID') try { // Removes cookie res.clearCookie('xpgnssid'); diff --git a/server/controllers/sessionController.js b/server/controllers/sessionController.js index 5b01cef..ffc8b6d 100644 --- a/server/controllers/sessionController.js +++ b/server/controllers/sessionController.js @@ -22,6 +22,7 @@ sessionController.start = (req, res, next) => { // Verifies that authentication cookie and sends user_id as response sessionController.verify = (req, res, next) => { + console.log('sessionController.verify') const queryStr = `SELECT s.user_id, u.username FROM ( SELECT user_id FROM sessions @@ -56,6 +57,7 @@ sessionController.verify = (req, res, next) => { // Removes authentication cookie and user_id from sessions table sessionController.end = (req, res, next) => { + console.log('sessionController.end') const queryStr = `DELETE FROM sessions WHERE ssid = $1`; const params = [req.cookies.xpgnssid]; @@ -70,34 +72,34 @@ sessionController.end = (req, res, next) => { }; // Enforces max of three sessions per user -sessionController.manage = (req, res, next) => { - const queryStr = `SELECT id FROM sessions - WHERE user_id = $1 - ORDER BY id DESC`; - const delStr = `DELETE FROM sessions - WHERE user_id = $1 - AND id <= $2`; +// sessionController.manage = (req, res, next) => { +// const queryStr = `SELECT id FROM sessions +// WHERE user_id = $1 +// ORDER BY id DESC`; +// const delStr = `DELETE FROM sessions +// WHERE user_id = $1 +// AND id <= $2`; - db.query(queryStr, [res.locals.user.id]) - .then((data) => { - if (data.rows.length > 3) { - const threshold = data.rows[3].id; - db.query(delStr, [res.locals.user.id, threshold]) - .then(() => next()) - .catch(() => next({ - log: 'A problem occured managing sessions', - status: 500, - message: { err: 'A problem occured managing sessions' } - })); - } - return next(); - }) +// db.query(queryStr, [res.locals.user.id]) +// .then((data) => { +// if (data.rows.length > 3) { +// const threshold = data.rows[3].id; +// db.query(delStr, [res.locals.user.id, threshold]) +// .then(() => next()) +// .catch(() => next({ +// log: 'A problem occured managing sessions', +// status: 500, +// message: { err: 'A problem occured managing sessions' } +// })); +// } +// return next(); +// }) - .catch(() => next({ - log: 'A problem occured managing sessions', - status: 500, - message: { err: 'A problem occured managing sessions' } - })); -}; +// .catch(() => next({ +// log: 'A problem occured managing sessions', +// status: 500, +// message: { err: 'A problem occured managing sessions' } +// })); +// }; module.exports = sessionController; diff --git a/server/controllers/userController.js b/server/controllers/userController.js index c4d6344..c24d1df 100644 --- a/server/controllers/userController.js +++ b/server/controllers/userController.js @@ -154,6 +154,7 @@ userController.submitReview = (req, res, next) => { .then((data) => { if (data.rows !== []) { [res.locals.review] = data.rows; + console.log('data.rows: ', data.rows); res.locals.review.username = res.locals.username; return next(); } @@ -228,8 +229,12 @@ userController.deleteReview = (req, res, next) => { const str = `DELETE from reviews WHERE id = $1`; const params = [req.body.id]; + console.log('before query deleteReview'); db.query(str, params) - .then(() => next()) + .then(() => { + console.log('after then deleteReview'); + next(); + }) .catch((err) => next(err)); }; diff --git a/server/routes/users.js b/server/routes/users.js index b581d5f..fc11f5a 100644 --- a/server/routes/users.js +++ b/server/routes/users.js @@ -41,7 +41,7 @@ router.post('/login', cookieController.setSSID, sessionController.start, userController.getReviews, - sessionController.manage, + // sessionController.manage, (req, res) => res.status(200).json([ res.locals.user.id, res.locals.user.username, res.locals.user.firstname, res.locals.reviews ])); @@ -66,6 +66,10 @@ router.post('/submitreview', userController.emitReview, (req, res) => res.sendStatus(204)); +router.post('/deletereview', + userController.deleteReview, + (req, res) => res.sendStatus(204)); + router.post('/follow', sessionController.verify, userController.follow, diff --git a/setUpTests.js b/setUpTests.js new file mode 100644 index 0000000..fc7b0dc --- /dev/null +++ b/setUpTests.js @@ -0,0 +1,4 @@ +import Enzyme from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; + +Enzyme.configure({ adapter: new Adapter() });