make it better

This commit is contained in:
Ray
2017-07-24 18:40:56 +01:00
commit 586fe2bec3
38 changed files with 4127 additions and 0 deletions

1
src/actions/index.js Normal file
View File

@@ -0,0 +1 @@
export * from './product';

18
src/actions/product.js Normal file
View File

@@ -0,0 +1,18 @@
import * as types from '../constants/types';
export function getProducts() {
return dispatch => {
fetch(`${process.env.REACT_APP_API_URL}/v1/api/products`)
.then(response => response.json())
.then(response => {
dispatch({
type: types.FETCH_PRODUCTS,
payload: response
});
})
}
}
export function compare(product) {
return {type: types.COMPARE_PRODUCT, product};
}

View File

@@ -0,0 +1,65 @@
.compare table {
background-color: #fff;
border-radius: 5px;
overflow: hidden;
box-shadow: 0px 13px 21px -5px rgba(0, 0, 0, 0.05);
border: 1px solid #eee;
font-size: 18px;
table-layout: fixed;
}
.compare .thead-default th {
background-color: #fff;
}
.compare table tr > td,
.compare table tr > th {
padding-top: 25px;
padding-bottom: 25px;
text-align: center;
}
.compare table thead th {
font-size: 20px;
color: #393c45;
font-weight: normal;
}
.compare table th[scope="row"] {
font-size: 20px;
color: #393c45;
font-weight: normal;
background-color: #f9f9f9;
border: 1px solid #eee;
text-align: left;
padding-left: 45px;
}
.compare table tr.condition {
color: #fff;
font-size: 20px;
}
.compare table tr.colors span {
height: 20px;
width: 20px;
background-color: #000;
display: inline-block;
margin-right: 5px;
border-radius: 100%;
}
.compare table td.red,
.compare table tr.colors span.red {
background-color: #ff715b;
}
.compare table td.green,
.compare table tr.colors span.green {
background-color: #48cfad;
}
.compare table td.blue,
.compare table tr.colors span.blue {
background-color: #0197F6;
}

View File

@@ -0,0 +1,140 @@
import React from 'react'
import {Table} from 'reactstrap'
import './index.css'
const Compare = ({products}) =>
<div className="row compare">
<div className="col-12 mt-5 text-center">
<div className={(products.length < 2
? 'hidden-xs-up'
: '')}>
<Table>
<thead className="thead-default">
<tr>
<th></th>
{products.map(product =>
<th key={product.id}>
{product.name}
</th>
)}
</tr>
</thead>
<tbody>
<tr className="price">
<th scope="row">Price</th>
{products.map(product =>
<td key={product.id} className="text-center">{product.price}</td>
)}
</tr>
<tr className="colors">
<th scope="row">Colors</th>
{products.map(product =>
<td key={product.id}>
{product.colors.map((color, index) =>
<span key={index} className={color}></span>
)}
</td>
)}
</tr>
<tr className="condition">
<th scope="row">Condition</th>
{products.map(product =>
<td key={product.id} className={product.condition === "Used" ? "red" : "green"}>
{product.condition}
</td>
)}
</tr>
</tbody>
</Table>
</div>
</div>
</div>;
// const Compare = ({products}) =>
// <div className="row">
// <div className="col-12 mt-5">
// <h3 className={"text-center " + (products.length > 1 ? 'hidden-xs-up' : '')}>
// Select two or more products
// </h3>
//
// <div className={(products.length < 2 ? 'hidden-xs-up' : '')}>
// <Table>
// <thead>
// <tr>
// <th></th>
// {products.map(product =>
// <td key={product.id}>
// <img width="100%"
// src={`${process.env.REACT_APP_API_URL}/v1/api/products/image/${product.img}`}
// alt={product.name}/>
// </td>
// )}
// </tr>
// </thead>
// <tbody>
// <tr>
// <th>Name</th>
// {products.map(product =>
// <td key={product.id} className="text-center">{product.name}</td>
// )}
// </tr>
// <tr>
// <th>Categories</th>
// {products.map(product =>
// <td key={product.id}>
// {product.categories.map((category, index) =>
// <div className="text-center category" key={index}>{category}</div>
// )}
// </td>
// )}
// </tr>
// <tr>
// <th>Balance Transfer Rates</th>
// {products.map(product =>
// <td key={product.id} className="text-center">
// <div className="rate">{product.rates.balance.rate}% for up to</div>
// <div className="period">{product.rates.balance.period} months</div>
// <div className="fee">({product.rates.balance.fee}% handling fee)</div>
// </td>
// )}
// </tr>
// <tr>
// <th>Money Transfer Rates</th>
// {products.map(product =>
// <td key={product.id} className="text-center">
// <div className="rate">{product.rates.money.rate}% for up to</div>
// <div className="period">{product.rates.money.period} months</div>
// <div className="fee">({product.rates.money.fee}% handling fee)</div>
// </td>
// )}
// </tr>
// <tr>
// <th>Card Purchases</th>
// {products.map(product =>
// <td key={product.id} className="text-center">
// <div className="rate">{product.rates.purchases.rate}% for up to</div>
// <div className="period">{product.rates.purchases.period} months</div>
// <div className="fee">({product.rates.purchases.fee}% handling fee)</div>
// </td>
// )}
// </tr>
// <tr>
// <th>Additional Info</th>
// {products.map(product =>
// <td key={product.id} className="text-center">{product.info}</td>
// )}
// </tr>
// <tr className="text-center">
// <td></td>
// {products.map(product =>
// <td key={product.id}>
// <Button color="primary">More info</Button>
// </td>
// )}
// </tr>
// </tbody>
// </Table>
// </div>
// </div>
// </div>;
export default Compare;

View File

@@ -0,0 +1,3 @@
header {
margin-top: 10vh;
}

View File

@@ -0,0 +1,17 @@
import React from 'react'
import './index.css'
export default class Header extends React.Component {
render() {
return (
<div className="container">
<header>
<div className="row">
<div className="col-12 text-center">
</div>
</div>
</header>
</div>
);
}
}

View File

@@ -0,0 +1,102 @@
.product {
cursor: pointer;
position: relative;
padding-bottom: 100px;
border-radius: 5px;
overflow: hidden;
-webkit-transition: all 500ms ease-out;
-moz-transition: all 500ms ease-out;
-o-transition: all 500ms ease-out;
transition: all 500ms ease-out;
}
.product:hover {
box-shadow: 0px 13px 21px -5px rgba(0, 0, 0, 0.2);
}
.product img {
width: 100%;
}
.stats-container {
background: #fff;
padding: 25px 15px;
position: absolute;
bottom: 0;
width: 100%;
}
.stats-container .product_name {
font-size: 22px;
color: #393c45;
}
.stats-container p {
font-size: 16px;
color: #b1b1b3;
margin: 0;
}
.stats-container .product_price {
float: right;
color: #48cfad;
font-size: 22px;
font-weight: 600;
}
.image_overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #48cfad;
opacity: 0;
-webkit-transition: all 200ms ease-out;
-moz-transition: all 200ms ease-out;
-o-transition: all 200ms ease-out;
transition: all 200ms ease-out;
}
.product.compare .image_overlay,
.product:hover .image_overlay {
opacity: 0.7;
}
.view_details {
position: absolute;
top: 112px;
left: 50%;
margin-left: -85px;
border: 2px solid #fff;
color: #fff;
font-size: 19px;
text-align: center;
text-transform: uppercase;
font-weight: 700;
padding: 10px 0;
width: 172px;
opacity: 0;
-webkit-transition: all 200ms ease-out;
-moz-transition: all 200ms ease-out;
-o-transition: all 200ms ease-out;
transition: all 200ms ease-out;
}
.view_details:hover {
background: #fff;
color: #48cfad;
cursor: pointer;
}
.product:hover .view_details {
opacity: 1;
width: 152px;
font-size: 15px;
margin-left: -75px;
top: 150px;
-webkit-transition: all 200ms ease-out;
-moz-transition: all 200ms ease-out;
-o-transition: all 200ms ease-out;
transition: all 200ms ease-out;
}

View File

@@ -0,0 +1,34 @@
import React from 'react'
import './index.css'
// const Product = ({product, compare}) =>
// <div key={product.id} className="col-3 product-card">
// <Card inverse
// onClick={() => compare(product)}
// color={product.compare ? "success" : "primary"}
// >
// <CardBlock>
// <CardTitle>{product.name}</CardTitle>
// </CardBlock>
// </Card>
// </div>;
const Product = ({product, compare}) =>
<div key={product.id} className="col-3">
<div className={"product " + (product.compare ? "compare" : "")} >
<img src={product.image} alt={product.name} />
<div className="image_overlay"></div>
<div className="view_details" onClick={() => compare(product)}>
{product.compare ? "Remove" : "Compare"}
</div>
<div className="stats">
<div className="stats-container">
<span className="product_price">{product.price}</span>
<span className="product_name">{product.name}</span>
<p>Men's running shirt</p>
</div>
</div>
</div>
</div>;
export default Product;

View File

@@ -0,0 +1,13 @@
import React from 'react'
import Product from '../Product'
const ProductList = ({products, compare}) =>
<div>
<div className="row mt-3">
{products.map(product =>
<Product key={product.id} product={product} compare={compare} />
)}
</div>
</div>;
export default ProductList;

2
src/constants/types.js Normal file
View File

@@ -0,0 +1,2 @@
export const FETCH_PRODUCTS = 'FETCH_PRODUCTS';
export const COMPARE_PRODUCT = 'COMPARE_PRODUCT';

View File

@@ -0,0 +1,25 @@
import React, {Component} from 'react';
import Header from '../../components/Header'
import {Route, Switch} from 'react-router-dom'
import './style.css';
import Home from '../Home';
import NotFound from '../NotFound';
export default class App extends Component {
render() {
return (
<div className="App">
<Header />
<div className="container mt-4">
<Switch>
<Route exact path="/" component={Home}/>
<Route component={NotFound}/>
</Switch>
</div>
</div>
);
}
}

View File

View File

@@ -0,0 +1,42 @@
import React from 'react';
import {bindActionCreators} from 'redux';
import ProductList from '../../components/ProductList';
import Compare from '../../components/Compare';
import * as productActions from '../../actions';
import {connect} from 'react-redux';
class Home extends React.Component {
componentWillMount() {
this.props.actions.getProducts();
}
render() {
const {products, actions} = this.props;
const compareProducts = products.filter(product => product.compare);
return (
<div className="Home mt-5">
<ProductList products={products} compare={actions.compare}/>
<Compare products={compareProducts}/>
</div>
);
}
}
function mapStateToProps(state) {
return {
products: state.product.products
}
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(productActions, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Home);

View File

@@ -0,0 +1,12 @@
import React from 'react'
export default class NotFound extends React.Component {
render() {
return (
<div>
<h1>404</h1>
<h3>Page not found</h3>
</div>
);
}
}

7
src/index.css Normal file
View File

@@ -0,0 +1,7 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
background: #eaebec;
color: #393c45;
}

31
src/index.js Normal file
View File

@@ -0,0 +1,31 @@
import registerServiceWorker from './registerServiceWorker';
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter} from 'react-router-dom'
import {createStore, applyMiddleware} from 'redux'
import {Provider} from 'react-redux'
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import reducer from './reducers'
import App from './containers/App'
import 'bootstrap/dist/css/bootstrap.css'
import './index.css'
const loggerMiddleware = createLogger()
const store = createStore(
reducer,
applyMiddleware(
thunkMiddleware, // lets us dispatch() functions
loggerMiddleware // neat middleware that logs actions
)
)
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>, document.getElementById('root'));
registerServiceWorker();

8
src/reducers/index.js Normal file
View File

@@ -0,0 +1,8 @@
import { combineReducers } from 'redux'
import product from './product_reducer'
const compareApp = combineReducers({
product
})
export default compareApp

View File

@@ -0,0 +1,26 @@
import * as types from '../constants/types';
const INITIAL_STATE = {
products: []
};
export default function (state = INITIAL_STATE, action) {
switch (action.type) {
case types.FETCH_PRODUCTS:
return {
...state, products: action.payload.map(product =>
({...product, compare: false})
)
};
case types.COMPARE_PRODUCT:
return {
...state, products: state.products.map(product =>
product.id === action.product.id ?
({...product, compare: !product.compare}) :
product
)
};
default:
return state;
}
}

View File

@@ -0,0 +1,108 @@
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (!isLocalhost) {
// Is not local host. Just register service worker
registerValidSW(swUrl);
} else {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
}
});
}
}
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}