@import url('https://fonts.googleapis.com/css?family=Montserrat:700|Open+Sans');
$white: #ffffff;
$gray: #f2f3f5;
$light_gray: #fafafa;
$bluish_gray: #e5e8ea;
$dark_gray: #718592;
$smoke: #F6F7F8;
$text_gray: #b3b3b3;
$text_dark_gray: #83949e;
$red: #FF3366;
$yellow: #ffce33;
$dark: #011627;
$green: #2EC4B6;
$blue: #20A4F3;
$pink: #ff33d8;
$text_white: $white;
$text_night_white: $light_gray;
$bg_white: #ffffff;
$bg_night_white: #192935;
$bg_gray: #f2f3f5;
$bg_night_gray: #0e161d;
$border_gray: $bluish_gray;
$border_night_gray: #284254;
$spacer: 5px;
$spacer_m: $spacer*2;
$spacer_xm: $spacer*3;
$spacer_xxm: $spacer*4;
$spacer_xxxm: $spacer*5;
$spacer_l: $spacer*6;
$spacer_xl: $spacer*7;
$spacer_xxl: $spacer*8;
$spacer_xxxl: $spacer*9;
$btn_height: 38px;
$btn_width: 110px;
$shadow-l0: 0 1px 3px rgba(0,0,0,0.1);
$shadow-l1: 0 4px 8px rgba(0,0,0,0.2);
$shadow-l2: 0 6px 20px rgba(0, 0, 0, 0.3);
$header_height: 50px;
$aside_width: 200px;
$aside_actions_height: 170px;
$cart_width: 350px;
$product_min_height: 260px;
$product_image_height: 200px;
$product_details_image_height: 300px;
$product_details_media_width: 550px;
$discount_height: 22px;
$colorpicker_items_per_row: 5;
$colorpicker_spacing: $spacer / 2;
$colorpicker_size: (($aside_width - ($spacer_xxm * 2)) / $colorpicker_items_per_row) - ($colorpicker_spacing * 2) - 1;
$tablet_breakpoint: 1024px;
$tablet_s_breakpoint: 800px;
$tablet_xs_breakpoint: 600px;
$header_z: 9;
$z_l0: 9;
$z_l1: 99;
$counter_size: 20px;
$ph_height: 15px;
$ph_radius: 5px;
$time: 0.2s;
* {
box-sizing: border-box;
word-break: break-word;
max-width: 100%;
font-family: 'Open Sans', sans-serif;
}
.clearfix:before,
.clearfix:after{
content: '';
display:table;
clear: both;
}
html,
body{
color: $dark;
}
html,
body,
#app,
.app-inner,
.page,
.page > .content,
.a-side{
margin: 0;
padding: 0;
height: 100%;
min-height: 100%;
}
.no-decoration{
text-decoration: none;
}
@mixin actionize($selector, $bgColor, $color: $white,$colorChangeStep: 5){
#{$selector}{
background-color: $bgColor;
color: $color;
cursor: pointer;
&:hover,
&:focus{
@if $bgColor == $white {
background-color: darken($bgColor, $colorChangeStep);
} @else {
background-color: lighten($bgColor, $colorChangeStep);
}
}
&:active{
@if $bgColor == $white {
background-color: darken($bgColor, $colorChangeStep * 2);
} @else {
background-color: darken($bgColor, $colorChangeStep);
}
}
}
}
@mixin actionizeBtn($selector, $bgColor, $color: $white,$colorChangeStep: 5){
$btnSelector: '.btn.'+$selector;
@include actionize($btnSelector, $bgColor, $color, $colorChangeStep);
}
.btn{
display: flex;
align-items: center;
justify-content: center;
border: none;
padding: $spacer_m;
border-radius: 7px;
font-size: 13px;
&.wide{
width: 100%;
}
.icon,
.text{
height: 18px;
line-height: 18px;
}
.icon{
font-size: 13px;
}
.icon + .text{
padding-left: $spacer;
}
}
@mixin bg($color_name: white, $color: #fff, $borderize: false, $darken: 20){
@if($borderize){
.bg-#{$color_name}-bordered{
background-color: $color;
border-color: darken($color, $darken);
}
&.night-mode .bg-#{$color_name}-bordered{
border-color: lighten($color, $darken);
}
} @else {
.bg-#{$color_name}{
background-color: $color;
}
}
}
$bg_colors: (white, $white),
(gray, $gray),
(bluish-gray, $bluish_gray),
(yellow, $yellow),
(dark, $dark),
(black, $dark),
(red, $red),
(blue, $blue),
(pink, $pink);
.app-inner {
overflow-y: scroll;
h1,h2,h3,.bold{
font-family: 'Montserrat', sans-serif;
letter-spacing: 1px;
}
&.night-mode{
color: $text_night_white;
}
@each $color_name, $color in $bg_colors {
@include bg($color_name, $color);
}
@each $color_name, $color in $bg_colors {
@include bg($color_name, $color, true);
}
@include actionizeBtn('blue', $blue);
@include actionizeBtn('red', $red);
@include actionizeBtn('green', $green);
@include actionizeBtn('dark', $dark);
@include actionizeBtn('white', $white, $dark);
&.night-mode {
@include actionizeBtn('white', $bg_night_white);
}
.header{
display: flex;
position: fixed;
height: $header_height;
background: $bg_white;
width: calc(100% - #{$aside_width});
margin-left: $aside_width;
padding: 0 $spacer_xxxm;
z-index: $header_z;
border-bottom: solid 1px $gray;
.cart-icon .counter{
position: absolute;
top: 8px;
right: 8px;
}
}
&.night-mode .header{
background-color: $bg_night_white;
border-bottom-color: $bg_night_gray;
}
.header-navigation{
flex-grow: 1;
padding: 0;
margin: 0;
.link {
display: inline-block;
padding: 0 $spacer_xm;
height: $header_height - 1;
line-height: $header_height;
text-decoration: none;
&.active-link{
font-weight: bold;
}
}
}
@include actionize('.header-navigation .link', $white, $dark);
&.night-mode {
@include actionize('.header-navigation .link', $bg_night_white, $white);
}
.page {
display: block;
background: $bg_gray;
height: auto;
.a-side{
position: fixed;
width: $aside_width;
min-width: $aside_width;
background: $bg_white;
border-right: solid 1px $gray;
height: 100dvh;
overflow-y: auto;
padding-bottom: $aside_actions_height;
.page-actions {
position: fixed;
width: $aside_width;
left: 0;
bottom: 0;
padding: 20px;
border-top: solid 1px $gray;
background: inherit;
}
}
.content{
padding: 0 $spacer_xxm;
margin-left: $aside_width;
min-height: calc(100vh - #{$header_height});
}
&.cart-is-opened{
margin-right: $cart_width;
}
.a-side,
.content{
padding-top: $header_height;
}
.content{
& > .title{
padding: 0 $spacer-m;
}
}
}
&.night-mode .page{
background-color: $bg_night_gray;
.a-side{
background: $bg_night_white;
border-right-color: $bg_night_gray;
.page-actions {
border-top-color: $bg_night_gray;
}
}
}
.products{
display: flex;
flex-wrap: wrap;
margin: 0;
padding: 0;
.product{
position: relative;
margin: $spacer-m;
background: $bg_white;
list-style-type: none;
flex-basis: calc(25% - (#{$spacer-m} * 2));
min-height: $product_min_height;
transition: box-shadow 0.15s ease-out;
box-shadow: $shadow-l0;
&:hover:not(.placeholder){
box-shadow: $shadow-l1;
}
&:active:not(.placeholder){
box-shadow: $shadow-l0;
}
.details{
display: flex;
.name,
.price{
color: $dark;
}
}
}
}
&.night-mode .products .product{
background-color: $bg_night_white;
.details{
.name,
.price{
color: $white;
}
}
}
&.night-mode .product.page > .content{
background-color: $bg_night_white;
}
.product{
&.page > .content{
background: $bg_white;
height: 100vh;
padding-top: $header_height + $spacer_xxm;
display: flex;
.media{
flex-basis: $product_details_media_width;
min-width: $product_details_media_width;
.image{
height: $product_details_image_height;
background-size: auto 100%;
background-repeat: no-repeat;
}
}
.details{
flex-grow: 1;
padding: 0 $spacer_xxm $spacer_xxm;
.title{
margin: 0;
}
.price-wrapper{
padding: $spacer_m 0 $spacer_xm;
font-size: 16px;
}
.subtitle{
color: $text_gray;
text-transform: uppercase;
font-size: 12px;
margin-bottom: $spacer;
}
.actions-wrapper{
padding-top: $spacer_xxm;
}
}
}
&.placeholder{
position: relative;
overflow: hidden;
background: $light_gray;
}
&.placeholder:after{
content:'';
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255,255,255,0.2) 50%, rgba(255, 255, 255, 0) 100%);
position: absolute;
top:0;
left:0;
bottom:0;
right:0;
transform: translateX(-100%);
animation: placehodler 2.5s ease-in-out infinite;
}
.image{
height: $product_image_height;
width: 100%;
background-size: cover;
background-position: center;
background-color: $bluish_gray;
}
.discount{
position: absolute;
background: $red;
color: $white;
transform: rotate(45deg);
transform-origin: 10px 24px;
height: $discount_height;
right: 0;
&:before,
&:after{
position: absolute;
content: '';
border: solid #{$discount_height / 2} transparent;
border-right-color: $red;
border-bottom-color: $red;
left: -#{$discount_height};
top: 0px;
}
&:after{
border-left-color: $red;
border-bottom-color: $red;
border-right-color: transparent;
right: -#{$discount_height};
left: auto;
}
}
.details{
padding: $spacer_xxm $spacer_m;
font-size: 14px;
.price{
margin-left: auto;
}
.old-price{
text-decoration: line-through;
color: $red;
}
}
}
@media screen and (max-width: #{$tablet_breakpoint}){
.products .product{
flex-basis: calc((100% / 3) - (#{$spacer-m} * 2));
}
}
@media screen and (max-width: #{$tablet_s_breakpoint}){
.products .product{
flex-basis: calc((100% / 2) - (#{$spacer-m} * 2));
}
}
@media screen and (max-width: #{$tablet_xs_breakpoint}){
.products .product{
flex-basis: calc(100% - (#{$spacer-m} * 2));
}
}
@keyframes placehodler{
0%, 100% {
transform: translateX(-100%);
}
50%{
transform: translateX(100%);
}
}
.informer {
padding: $spacer_m $spacer_xxm;
background: $bg_white;
box-shadow: #{$shadow-l0};
.title{
padding: $spacer_m 0;
margin: 0;
text-align: center;
}
}
&.night-mode .informer{
background: $bg_night_white;
}
.a-side{
padding-left: $spacer_xxm;
padding-right: $spacer_xxm;
}
label {
font-weight: 900;
font-family: 'Montserrat';
display: block;
font-size: 12px;
text-transform: uppercase;
}
select {
display: block;
width: 100%;
margin-top: $spacer;
padding: $spacer;
height: 30px;
background: $bluish_gray;
color: $dark;
border-color: transparent;
}
&.night-mode select{
background: $bg_night_gray;
color: $text_night_white;
}
.placeholder {
.item{
background: $bluish_gray;
border-radius: $ph_radius;
width: 100%;
position: relative;
overflow: hidden;
margin-bottom: $spacer_m;
&:after{
content:'';
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255,255,255,0.2) 50%, rgba(255, 255, 255, 0) 100%);
position: absolute;
top:0;
left:0;
bottom:0;
right:0;
transform: translateX(-100%);
animation: placehodler 2.5s ease-in-out infinite;
}
}
.text{
height: $ph_height;
}
.text + .text:last-of-type{
width: 50%;
}
&.button .text{
height: $btn_height;
width: $btn_width;
}
&.price .text{
margin: 0;
height: 20px;
width: 70px;
}
&.title .text{
margin: 0;
height: 34px;
width: 40%;
}
}
.counter {
background: $red;
color: $white;
width: $counter_size;
height: $counter_size;
display: inline-block;
text-align: center;
line-height: $counter_size;
border-radius: 50%;
font-size: 11px;
}
.cart-icon {
position: relative;
line-height: $header_height;
padding: 0 $spacer_xxxm;
.counter,
.icon{
display: inline-block;
}
.icon{
line-height: $header_height;
transition: 0.3s ease-out;
}
.counter{
&:not(.active){
transform: scale(0);
}
}
}
@include actionize('.cart-icon', $white, $dark);
&.night-mode {
@include actionize('.cart-icon', $bg_night_white, $white);
}
&.night-mode .cart{
background-color: $bg_night_white;
border-left-color: $bg_night_gray;
.actions-wrapper{
background-color: $bg_night_white;
border-top-color: $bg_night_gray;
}
.list .item{
border-bottom-color: $bg_night_gray;
}
}
.cart{
position: fixed;
top: 0;
right: 0;
width: $cart_width;
height: 100vh;
background: $bg_white;
padding: ($header_height + $spacer_m) $spacer_xxm $spacer_m;
box-shadow: $shadow-l2;
z-index: $z-l1;
.cart-topbar {
display: flex;
text-align: center;
position: absolute;
top: 0;
left: 0;
right: 0;
height: $header_height;
background: $white;
border-bottom: solid 1px $border_gray;
.cart-title{
margin: auto;
}
}
.empty{
text-align: center;
position: absolute;
height: 105px;
width: 100%;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
color: $text_gray;
.icon{
font-size: 48px;
}
.text{
font-size: 13px;
}
.hint{
padding: 0 $spacer_xxxm;
}
}
.list {
.item {
display:flex;
padding: $spacer 0;
border-bottom: solid 1px $gray;
.image{
width: 36px;
height: 36px;
}
.details{
padding-top: 0;
padding-bottom: 0;
}
.name{
font-size: 13px;
}
.price{
font-size: 13px;
}
.count{
margin-left: auto;
line-height: 36px;
}
}
}
.total{
display: flex;
padding: $spacer_xm 0;
.amount{
margin-left: auto;
}
}
.actions-wrapper{
position: fixed;
bottom: 0;
right: 0;
width: $cart_width - 1;
background: $bg_white;
border-top: solid 1px $gray;
padding: $spacer_xxm;
}
}
.toggler{
$status_icon_rotate: -270deg;
position: relative;
input[type="checkbox"]{
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
width: 100%;
height: 100%;
margin: 0;
z-index: 9;
cursor: pointer;
}
.view{
display: flex;
.label{
padding-right: $spacer_m;
font-size: 13px;
color: $text_dark_gray;
height: 18px;
line-height: 18px;
}
.state{
position: absolute;
width: 13px;
height: 13px;
top: -1px;
left: 2px;
right: 0;
bottom: 0;
margin: 0;
padding: 0;
line-height: initial;
&.on{
.icon{
transform: rotate($status_icon_rotate);
opacity: 0;
}
}
&.off{
}
.icon{
font-size: 13px;
color: $text_gray;
transform: rotate($status_icon_rotate);
transition: $time ease-out;
}
}
.body{
position: relative;
flex-grow: 1;
background: $bg_gray;
border-radius: 9px;
height: 18px;
border: solid 1px $border_gray;
max-width: 40px;
margin-left: auto;
.indicator{
position: absolute;
width: 18px;
height: 18px;
left: -1px;
top: -1px;
border-radius: 50%;
background: $bg_white;
transform: translate3d(0,0,0);
transition: $time ease-out;
box-shadow: $shadow-l0;
}
}
}
input:checked ~ .view {
.state.on .icon{
transform: rotate(0deg);
opacity: 1;
}
.state.off .icon{
opacity: 0;
transform: rotate($status_icon_rotate);
}
.indicator{
left: calc(100% + 1px);
transform: translateX(-100%);
}
}
}
&.night-mode .toggler{
.body {
background: $bg_night_gray;
border-color: $border_night_gray;
.indicator{
background: $border_night_gray;
}
}
}
.tabber {
display: flex;
border-radius: 7px;
overflow: hidden;
border: solid 1px $border_gray;
.item {
flex-grow: 1;
text-align: center;
background: $bg_gray;
&:not(:last-of-type) {
border-right: solid 1px $border_gray;
}
&.selected {
background: $bg_white;
}
}
@include actionize('.item:not(.selected)', $bg_gray);
}
&.night-mode .tabber{
border-color: $border_night_gray;
.item {
background: $bg_night_gray;
&:not(:last-of-type) {
border-right: solid 1px $border_night_gray;
}
&.selected {
background: $border_night_gray;
}
}
@include actionize('.item:not(.selected)', $bg_night_gray);
}
.colorpicker {
.list {
display: flex;
flex-flow: row wrap;
.item{
position: relative;
width: $colorpicker_size;
height: $colorpicker_size;
flex-grow: 0;
flex-shrink: 1;
flex-basis: $colorpicker_size;
border-radius: 50%;
border-style: solid;
border-width: 1px;
margin: $colorpicker_spacing;
transition: $time ease-out;
&:not(.selected) {
transform: scale(0.7);
}
&.active{
cursor: pointer;
}
&.none{
border-color: $border_gray;
&:after {
content: '';
position: absolute;
width: 100%;
top: 50%;
height: 1px;
background: $border_gray;
transform: rotate(-45deg);
}
}
&.selected{
border-width: 2px;
}
}
}
}
&.night-mode .colorpicker .list .item .none{
border-color: $border_night_gray;
&:after {
background: $border_night_gray;
}
}
}
.night-mode #devby{
filter: contrast(0) brightness(2);
}
View Compiled
console.clear();
// Initialize Firebase
const config = {
apiKey: "AIzaSyBOi39p524OJTY8KFo_vPUK0xK_aJX84hE",
authDomain: "react-store-bc88f.firebaseapp.com",
databaseURL: "https://react-store-bc88f.firebaseio.com",
projectId: "react-store-bc88f",
//storageBucket: "",
messagingSenderId: "638463324339"
};
firebase.initializeApp(config);
const db = firebase.firestore();
const settings = {
timestampsInSnapshots: true
};
db.settings(settings);
const productsRef = db.collection("products");
const statsRef = db.collection("stats");
const app = document.getElementById('app');
const dictionary = {
'nav_home' : {
'eng' : 'Homepage',
'ua' : 'Домашня сторінка',
},
'nav_for_her' : {
'eng' : 'For her',
'ua' : 'Для неï',
},
'nav_for_him' : {
'eng' : 'For him',
'ua' : 'Для нього',
},
'stitle_for_her' : {
'eng' : 'For her',
'ua' : 'Для неï',
},
'stitle_for_him' : {
'eng' : 'For him',
'ua' : 'Для нього',
},
'ptitle_home' : {
'eng' : 'Shoes for everyone',
'ua' : 'Взуття для кожного',
},
'ptitle_for_her' : {
'eng' : 'Best shoes for her',
'ua' : 'Найкраще взуття для неï',
},
'ptitle_for_him' : {
'eng' : 'Strongest shoes for him',
'ua' : 'Наймiцнiше взуття для нього',
},
'ptitle_cart' : {
'eng' : 'Cart',
'ua' : 'Кошик',
},
'header_description' : {
'eng' : 'Description',
'ua' : 'Опис',
},
'header_total' : {
'eng' : 'Total',
'ua' : 'Усього',
},
'header_product_not_found' : {
'eng' : 'Oh, no!',
'ua' : 'O, нi!',
},
'text_product_not_found' : {
'eng' : 'We couldn\'t find such shoes... Maybe try different filters? 🤷',
'ua' : 'Ми не змогли знайти таке взуття... Може спробуєте iнщi фiльтри? 🤷',
},
'label_shoes_for' : {
'eng' : 'Shoes For',
'ua' : 'Взуття Для',
},
'label_brand' : {
'eng' : 'Brand',
'ua' : 'Бренд',
},
'label_color' : {
'eng' : 'Color',
'ua' : 'Колiр',
},
'action_reset_filters' : {
'eng' : 'Reset Filters',
'ua' : 'Скинути Фiльтри',
},
'action_add_to_cart' : {
'eng' : 'Add To Cart',
'ua' : 'Додати До Кошика',
},
'action_checkout' : {
'eng' : 'Checkout',
'ua' : 'Придбати',
},
'label_lightness_mode' : {
'eng' : 'Lightness mode',
'ua' : 'Режим яскравостi',
},
'text_empty_cart' : {
'eng' : 'You cart is empty right now.\nClick "Add To Cart" button to start shopping.',
'ua' : 'Ваш кошик зараз попрожнiй.\nТацнiть по кнопцi "Додати До Кошика" щоб розпочати шопитись.',
},
'placeholder_any' : {
'eng' : 'Any',
'ua' : 'Будь-що',
},
'placeholder_any_m' : {
'eng' : 'Any',
'ua' : 'Будь-який',
},
'placeholder_any_f' : {
'eng' : 'Any',
'ua' : 'Будь-якa',
},
'placeholder_any_p' : {
'eng' : 'Any',
'ua' : 'Будь-якi',
},
'placeholder_any_n' : {
'eng' : 'Any',
'ua' : 'Будь-яке',
},
'placeholder_any_s' : {
'eng' : 'Any',
'ua' : 'Будь-чого',
},
};
const appData = {
product: {
fetchIsPending: false,
fetchIsFulfilled: false,
fetchIsRejected: false,
details: {
fetchIsPending: false,
fetchIsFulfilled: false,
fetchIsRejected: false,
data: {},
openedURL: '',
},
list: [],
filters: {
brands: [],
categories: [],
colors: []
},
appliedFilters: {
gender: '',
brand: '',
category: '',
color: '',
},
settings: {
itemsPerPage: 12,
},
errors: {
fetch: null,
},
cart: {
list: {},
isEmpty: true,
},
},
ui: {
cart: {
isOpened: false,
},
nightModeEnabled: false,
selectedLang: 'eng',
langs: ['eng', 'ua'],
dictionary: dictionary,
},
navigation: {
currentPageSlug : 'home',
headerLinks : [
{
'url' : '/home',
'name' : 'nav_home'
},
{
'url' : '/category/women',
'name' : 'nav_for_her'
},
{
'url' : '/category/men',
'name' : 'nav_for_him'
}
],
pages: {
'home' : {
title : 'ptitle_home'
},
'women' : {
title : 'ptitle_for_her'
},
'men' : {
title : 'ptitle_for_him'
}
},
},
pageData: {},
};
const {
BrowserRouter,
Switch,
Route,
Link,
NavLink,
Redirect,
DefaultRoute,
PropsRoute,
} = ReactRouterDOM;
const { withRouter } = ReactRouter;
Array.prototype.compare = (arrB) => {
if(!(Array.isArray(this) && Array.isArray(arr))) return false;
const str = (arr)=>{ return JSON.stringify(arr); };
return str(this) === str(arrB);
};
/* Redux stuff */
const {connect, Provider} = ReactRedux;
const { applyMiddleware, combineReducers, createStore} = Redux;
const ReduxThunk = window.ReduxThunk.default;
const logger = reduxLogger.logger;
const Actions = {
ui : {
filters : {
set: 'SET_UI_FILTERS'
},
cart : {
open: 'OPEN_CART',
close: 'CLOSE_CART',
},
toggleNightMode: 'TOGGLE_NIGHT_MODE',
setLang: 'SET_LANGUAGE'
},
navigation : {
to : 'NAVIGATE_TO'
},
product: {
products: {
fetch: {
pending: 'PRODUCT_FETCH_PENDING',
fulfilled: 'PRODUCT_FETCH_FULFILLED',
rejected: 'PRODUCT_FETCH_REJECTED',
store: 'PRODUCT_FETCH_STORE',
},
},
details: {
fetch: {
pending: 'PRODUCT_DETAILS_FETCH_PENDING',
fulfilled: 'PRODUCT_DETAILS_FETCH_FULFILLED',
rejected: 'PRODUCT_DETAILS_FETCH_REJECTED',
store: 'PRODUCT_DETAILS_FETCH_STORE',
},
set: {
openedURL: 'PRODUCT_DETAILS_SET_OPENED_URL',
},
unset: {
openedURL: 'PRODUCT_DETAILS_UNSET_OPENED_URL',
data: 'PRODUCT_DETAILS_UNSET_DATA',
}
},
filter: {
fetch: {
pending: 'PRODUCT_FILTER_FETCH_PENDING',
fulfilled: 'PRODUCT_FILTER_FETCH_FULFILLED',
rejected: 'PRODUCT_FILTER_FETCH_REJECTED',
store: 'PRODUCT_FILTER_FETCH_STORE',
},
set: {
gender: 'PRODUCT_SET_FILTER_GENDER',
brand: 'PRODUCT_SET_FILTER_BRAND',
category: 'PRODUCT_SET_FILTER_CATEGORY',
color: 'PRODUCT_SET_FILTER_COLOR',
},
unset: {
all: 'PRODUCT_RESET_ALL_FILTERS'
}
},
cart: {
add: {
product: 'CART_ADD_PRODUCT',
},
remove: {
product: 'CART_REMOVE_PRODUCT',
},
}
}
};
//Everything about ui goes here
const uiReducer = (state=appData.ui, action) => {
const actions = Actions.ui;
switch (action.type) {
case actions.toggleNightMode:
return {
...state,
nightModeEnabled: !state.nightModeEnabled
};
break;
case actions.setLang:
console.log(action.payload, state.ui);
return {
...state,
selectedLang: action.payload
};
break;
case actions.cart.open:
state = {...state, cart: {
isOpened: true
}};
return state;
break;
case actions.cart.close:
state = {...state, cart: {
isOpened: false
}};
return state;
break;
default:
return state;
break;
}
};
function toggleNightModeAction(){
return {
type: Actions.ui.toggleNightMode
};
}
function setLangAction(lang){
return {
type: Actions.ui.setLang,
payload: lang
};
}
function openCartAction(){
return {
type: Actions.ui.cart.open
};
}
function closeCartAction(){
return {
type: Actions.ui.cart.close
};
}
//Everything about navigation and current page goes here
const navigationReducer = (state=appData.navigation, action) => {
const actions = Actions.navigation;
switch (action.type) {
case actions.to:
return {
...state,
currentPageSlug: action.payload
};
break;
default:
return state;
}
};
function navigateAction(pageSlug){
return {
type: Actions.navigation.to,
payload: pageSlug
};
}
// store.dispatch(navigateAction('home'))
const productReducer = (state=appData.product, action) => {
const actions = Actions.product;
switch (action.type) {
//Fetch
case actions.products.fetch.pending:
//clear current list
return {
...state,
list: appData.product.list,
fetchIsPending: true,
fetchIsFulfilled: false,
fetchIsRejected: false,
};
break;
case actions.products.fetch.fulfilled:
return {
...state,
list: appData.product.list,
fetchIsPending: false,
fetchIsFulfilled: true,
fetchIsRejected: false,
};
break;
case actions.products.fetch.rejected:
state = {
...state,
list: appData.product.list,
fetchIsPending: false,
fetchIsFulfilled: false,
fetchIsRejected: true,
};
state.errors.fetch = action.payload;
return state;
break;
case actions.products.fetch.store:
return {
...state,
list: action.payload,
};
break;
//ProductDetails
case actions.details.fetch.pending:
state = {...state};
state.details.data = appData.product.details.data;
state.details.fetchIsPending = true;
state.details.fetchIsFulfilled = false;
state.details.fetchIsRejected = false;
return state;
break;
case actions.details.fetch.fulfilled:
state = {...state};
state.details.data = appData.product.details.data;
state.details.fetchIsPending = false;
state.details.fetchIsFulfilled = true;
state.details.fetchIsRejected = false;
return state;
break;
case actions.details.fetch.rejected:
state = {...state};
state.details.data = appData.product.details.data;
state.details.fetchIsPending = false;
state.details.fetchIsFulfilled = false;
state.details.fetchIsRejected = true;
return state;
break;
case actions.details.fetch.store:
state = {...state};
state.details.data = action.payload;
return state;
break;
case actions.details.set.openedURL:
state = {...state};
state.details.openedURL = action.payload;
return state;
break;
case actions.details.unset.openedURL:
state = {...state};
state.details.openedURL = null;
return state;
break;
case actions.details.unset.data:
state = {...state};
state.details.data = {};
return state;
break;
//Filters
case actions.filter.fetch.pending:
//update only if changed, so return current state
return state;
break;
case actions.filter.fetch.fulfilled:
state = {...state};
state.filters.brands = action.payload.brands;
state.filters.categories = action.payload.categories;
state.filters.colors = action.payload.colors;
return state;
break;
case actions.filter.fetch.rejected:
state = {...state};
state.errors.fetch = action.payload;
return state;
break;
case actions.filter.set.gender:
state = {...state};
state.appliedFilters.gender=action.payload;
return state;
break;
case actions.filter.set.brand:
state = {...state};
state.appliedFilters.brand=action.payload;
return state;
break;
case actions.filter.set.category:
state = {...state};
state.appliedFilters.category=action.payload;
return state;
break;
case actions.filter.set.color:
state = {...state};
state.appliedFilters.color=action.payload;
return state;
break;
case actions.filter.unset.all:
state = {...state};
state.appliedFilters.color='';
state.appliedFilters.category='';
state.appliedFilters.brand='';
return state;
break;
//Cart
case actions.cart.add.product:
const {product} = action.payload;
const {url} = product;
state = {...state};
const list = {...state.cart.list};
if(list[url]){
list[url].count += 1;
} else {
list[url] = {
...product,
count: 1
};
}
state.cart.isEmpty = false;
state.cart.list = list;
return state;
default:
return state;
}
};
function setProductsGenderAction(value){
const actions = Actions.product.filter;
return {
type: actions.set.gender,
payload: value
};
}
function setProductsBrandAction(value){
const actions = Actions.product.filter;
return {
type: actions.set.brand,
payload: value
};
}
function setProductsCategoryAction(value){
const actions = Actions.product.filter;
return {
type: actions.set.category,
payload: value
};
}
function setProductsColorAction(value){
const actions = Actions.product.filter;
return {
type: actions.set.color,
payload: value
};
}
function unsetProductsFilters(value){
const actions = Actions.product.filter;
return {
type: actions.unset.all,
payload: value
};
}
function startFetchingProductsAction(){
const actions = Actions.product.products;
return {
type: actions.fetch.pending
};
}
function storeProductsAction(products){
const actions = Actions.product.products;
return {
type: actions.fetch.store,
payload: products
};
}
function fetchingProductsFulfilledAction(){
const actions = Actions.product.products;
return {
type: actions.fetch.fulfilled
};
}
const fetchProducts = (dispatch) => {
const state = store.getState();
dispatch(startFetchingProductsAction());
const {appliedFilters} = state.product;
const genderKey = appliedFilters.gender ? 'gender' : '_'
const genderQuery = appliedFilters.gender || true;
const brandKey = appliedFilters.brand ? 'brand' : '_'
const brandQuery = appliedFilters.brand || true;
const catKey = appliedFilters.category ? 'category' : '_'
const catQuery = appliedFilters.category || true;
const colorKey = appliedFilters.color ? 'color' : '_'
const colorQuery = appliedFilters.color || true;
productsRef
.where(genderKey,'==',genderQuery)
.where(brandKey, '==',brandQuery)
.where(catKey, '==',catQuery)
.where(colorKey, '==',colorQuery)
// .orderBy('posted')
.limit(state.product.settings.itemsPerPage)
.get()
.then(querySnapshot=>{
const state = store.getState();
const products = [];
querySnapshot.forEach(doc => {
products.push(doc.data());
});
/*
Note: Prevent usage of deprecated data
e.g. when user clicks to fast on
different links
*/
if(state.product.fetchIsPending){
dispatch(fetchingProductsFulfilledAction());
dispatch(storeProductsAction(products));
}
})
.catch(err=>{
console.error(err);
});
};
//store.dispatch(fetchProducts);
function startFetchingProductDetailsAction(){
const actions = Actions.product.details;
return {
type: actions.fetch.pending
};
}
function storeProductDetailsAction(productData){
const actions = Actions.product.details;
return {
type: actions.fetch.store,
payload: productData
};
}
function storeOpenedProductDetailsAction(openedURL){
const actions = Actions.product.details;
return {
type: actions.set.openedURL,
payload: openedURL
};
}
function unsetOpenedProductURLAction(openedURL){
const actions = Actions.product.details;
return {
type: actions.unset.openedURL
};
}
function unsetProductDetailsDataAction(){
const actions = Actions.product.details;
return {
type: actions.unset.data
};
}
function fetchingProductDetailsFulfilledAction(){
const actions = Actions.product.details;
return {
type: actions.fetch.fulfilled
};
}
const fetchProductDetails = (dispatch) => {
const state = store.getState();
dispatch(startFetchingProductDetailsAction());
const {details} = state.product;
const urlKey = 'url'
const urlQuery = details.openedURL;
productsRef
.where(urlKey,'==',urlQuery)
.limit(1)
.get()
.then(querySnapshot=>{
const state = store.getState();
const products = [];
querySnapshot.forEach(doc => {
products.push(doc.data());
});
const productDetails = products[0];
console.log(productDetails);
/*
Note: Prevent usage of deprecated data
e.g. when user clicks to fast on
different links
*/
if(state.product.details.fetchIsPending){
dispatch(fetchingProductDetailsFulfilledAction());
dispatch(storeProductDetailsAction(productDetails));
}
})
.catch(err=>{
console.error(err);
});
};
function startFetchingFiltersAction(){
const actions = Actions.product.filter;
return {
type: actions.fetch.pending
};
}
function storeProductFiltersAction(filters){
const actions = Actions.product.filter;
return {
type: actions.fetch.fulfilled,
payload: filters
};
}
const fetchProductFilters = (dispatch) => {
const state = store.getState();
dispatch(startFetchingFiltersAction());
const {appliedFilters} = state.product;
statsRef
.doc('options')
.get()
.then(documentSnapshot=>{
const filters = documentSnapshot.data();
console.log(filters);
dispatch(storeProductFiltersAction(filters));
})
.catch(err=>{
console.error(err);
});
};
//store.dispatch(fetchProductFilters);
function addProductToCart(product){
const actions = Actions.product.cart;
return {
type: actions.add.product,
payload: {
product: product
}
};
}
const reducers = combineReducers({
ui: uiReducer,
navigation: navigationReducer,
product: productReducer
});
const initialState = {
someval: 1
};
const middleware = applyMiddleware(ReduxThunk, logger);
const store = createStore(reducers, appData, middleware);
store.subscribe(()=>{
});
const Icon = ({name}) => (<i className="icon material-icons">{name}</i>);
class Dictionary extends React.Component {
handleText(str){
return str.replace(/\\n/gm, '<br/>');
}
render(){
const {lang, dictionary, tag} = this.props;
console.log('))))', lang, dictionary, tag);
return dictionary[tag] ?
this.handleText(dictionary[tag][lang]) :
'';
}
};
const Dict = connect((state, ownProps)=>{
return {
lang: state.ui.selectedLang,
dictionary: state.ui.dictionary,
tag: ownProps.tag
};
})(Dictionary);
class TextItem extends React.Component {
handleText(str){
return str.replace(/\\n/gm, '<br/>');
}
getText(){
const {lang, dictionary, tag} = this.props;
return dictionary[tag] ?
this.handleText(dictionary[tag][lang]) :
'';
}
render(){
return (
<span className="text">{this.getText()}</span>
)
}
};
const Text = connect((state, ownProps)=>{
return {
lang: state.ui.selectedLang,
dictionary: state.ui.dictionary,
tag: ownProps.tag
};
})(TextItem);
const HeaderNavigationLink = (props) => {
return (
<NavLink
className="link"
to={props.link.url}
activeClassName="active-link"
>
{props.link.name}
</NavLink>
);
};
const HeaderNavigationLinkWR = withRouter(HeaderNavigationLink);
class HeaderNavigation extends React.Component {
render(){
const links = this.props.headerLinks.map(link => {
return (
<NavLink
className="link"
to={link.url}
key={link.url}
activeClassName="active-link"
>
<Text tag={link.name} />
</NavLink>
);
});
return (
<ul className="header-navigation">
{links}
</ul>
);
}
}
const HeaderNavigationContainer = connect(state => {
return {
headerLinks: state.navigation.headerLinks
}
})(withRouter(HeaderNavigation));
function countItemsInCart(items={}){
let count = 0;
for (var url in items){
if(items.hasOwnProperty(url)){
count += items[url].count;
}
}
return count;
}
function countPriceOfItemsInCart(items={}){
let price = 0;
let currency = '';
for (var url in items){
if(items.hasOwnProperty(url)){
const item = items[url];
price += (item.count * item.price);
currency = item.price_currency;
}
}
return prettifyCurrency(currency) + prettifyPrice(Math.round(price * 100) / 100);
}
const CartIcon = (props) => {
const count = countItemsInCart(props.list);
const counterDefaultClass = "counter bold";
const counterActiveClass = count > 0 ? "active" : "";
const counterClass = [
counterDefaultClass,
counterActiveClass
].join(" ");
return (
<div className="cart-icon" onClick={props.onClick}>
<Icon name="shopping_basket" />
<span className={counterClass}>
{count}
</span>
</div>
)
};
const CartItem = (props) => {
return(
<div className="product item">
<ProductImage product={props.data} />
<div className="details">
<div className="name bold">
{props.data.name}
</div>
<div className="price">
{prettifyCurrency(props.data.price_currency)}
{prettifyPrice(props.data.price)}
</div>
</div>
<div className="count">
{props.data.count}
</div>
</div>
);
};
const CartEmpty = (props) => {
return (
<div className="empty">
<Icon name="shopping_cart"/>
<p className="hint text">
<Text tag={'text_empty_cart'} />
</p>
</div>
);
};
class Cart extends React.Component {
renderCartItems(){
const {list} = this.props.product.cart;
let items = [];
for(let itemKey in list){
if(list.hasOwnProperty(itemKey)){
items.push(<CartItem key={itemKey} data={list[itemKey]} />);
}
}
return items;
}
renderActiveCart(){
const {cart} = this.props.product;
return (
<div className="cart-content">
<div className="list">
{this.renderCartItems()}
</div>
<div className="total">
<span className="text bold">
<Text tag={'header_total'} />
</span>
<span className="amount bold">
{countPriceOfItemsInCart(cart.list)}
</span>
</div>
<div className="actions-wrapper">
<button className="btn red wide">
<Text tag={'action_checkout'} />
</button>
</div>
</div>
);
}
renderContent(){
const {cart} = this.props.product;
return cart.isEmpty ?
(<CartEmpty />) :
(this.renderActiveCart());
}
render(){
return (
<div className="cart">
<div className="cart-topbar">
<a href="javascript:;" className="btn">
<i className="icon material-icons">close</i>
</a>
<h3 className="cart-title">
<i className="icon material-icons">shopping_basket</i>
<Text tag="ptitle_cart"/>
</h3>
</div>
{this.renderContent()}
</div>
);
}
}
const CartContainer = connect(state=>(state))(Cart);
class Header extends React.Component {
onCartIconClick(){
const {dispatch, ui} = this.props;
const {isOpened} = ui.cart;
if(isOpened){
dispatch(closeCartAction());
} else {
dispatch(openCartAction());
}
}
render(){
const {list} = this.props.product.cart;
return (
<header className="header">
<HeaderNavigationContainer/>
<CartIcon list={list} onClick={this.onCartIconClick.bind(this)}/>
</header>
);
}
}
const HeaderContainer = connect(
state=>{
return state;
}
)(Header);
class Selector extends React.Component {
render(){
let options = this.props.list.map((item, i)=>{
return (
<option
key={i}
value={item.url}
>
{item.name}
</option>
);
});
//insert placehodler option
if(!this.props.disableDefault){
options.unshift(<option key={'default'} value=''><Dict tag={this.props.placeholder || "placeholder_any"} /></option>);
}
return (
<div>
<label htmlFor={this.props.id}>
<Text tag={this.props.label} />
</label>
<select
id={this.props.id}
name={this.props.name}
onChange={this.props.onChange}
value={this.props.selected}
>
{options}
</select>
</div>
);
}
}
class Toggler extends React.Component {
constructor(props){
super(props);
this.state = this.props;
}
renderIcon(name){
return (<Icon name={name} />);
}
renderIconOn(){
return this.state.iconOn ?
this.renderIcon(this.state.iconOn) :
null;
}
renderIconOff(){
return this.state.iconOn ?
this.renderIcon(this.state.iconOff) :
null;
}
// componentWillReceiveProps(nextProps){
// this.setState({isChecked: nextProps.isChecked});
// }
renderIndicator(){
return (
<div className="indicator">
<div className="state on">
{this.renderIconOn()}
</div>
<div className="state off">
{this.renderIconOff()}
</div>
</div>
);
}
handleChange(){
this.setState({isChecked:!this.state.isChecked});
if(this.state.onChange){this.state.onChange();}
}
renderCheckbox(){
const {name, onChange, isChecked} = this.state;
return (
<input
type="checkbox"
name={name}
onChange={this.handleChange.bind(this)}
checked={isChecked ? 'true' : ''}
/>
);
}
renderLabel(){
const {label} = this.state;
return label ?
(
<div className="label">
<Text tag={label} />
</div>
) :
null;
}
bakeClassName(){
const {name, onChange, isChecked, label} = this.state;
const mainClass = "toggler";
const labelClass = label ? "with-label" : "";
return [mainClass, labelClass].join(" ");
}
render(){
const className=this.bakeClassName();
return (
<div className={className}>
{this.renderCheckbox()}
<div className="view">
{this.renderLabel()}
<div className="body">
<div className="fill" />
{this.renderIndicator()}
</div>
</div>
</div>
);
}
};
const NightModeToggler = (props) =>{
const {dispatch} = props;
const label = "label_lightness_mode";
return (
<Toggler
name="night-mode"
iconOn="brightness_3"
iconOff="brightness_7"
label={label}
onChange={()=>{dispatch(toggleNightModeAction())}}
isChecked={props.ui.nightModeEnabled}
/>
);
};
const NightModeTogglerContainer = connect(state=>(state))(NightModeToggler);
class TabberOption extends React.Component {
constructor(props){
super(props);
this.state = {
...this.props,
isSelected: this.props.selectedTab === this.props.value
};
}
componentWillReceiveProps(newProps){
if(this.props.selectedTab != newProps.selectedTab){
this.setState({
selectedTab: newProps.selectedTab,
isSelected: newProps.selectedTab === newProps.value
});
}
}
handleClick(){
const {onSelect, value} = this.state;
onSelect(value);
}
bakeClassName(){
const {isSelected} = this.state;
const mainClass = "item"
const selectedClass = isSelected ? 'selected' : '';
return [mainClass, selectedClass].join(" ");
}
render(){
return (
<div
className={this.bakeClassName()}
onClick={this.handleClick.bind(this)}
>
{this.state.children}
</div>
);
}
};
class Tabber extends React.Component {
constructor(props){
super(props);
this.state = {
...this.props,
selectedTab: this.props.default || null,
};
}
handleSelect(value){
const {onSelect} = this.state;
this.setState({selectedTab: value});
if(onSelect) onSelect(value);
}
renderOptions(){
const {selectedTab, children} = this.state;
return React.Children.map(children, (child) =>
React.cloneElement(child, {
onSelect: this.handleSelect.bind(this),
selectedTab: selectedTab,
})
);
}
render(){
return (
<div className="tabber">
{
this.renderOptions()
}
</div>
);
}
}
class LanguageTabber extends React.Component {
handleSelect(lang){
const {dispatch} = this.props;
dispatch(setLangAction(lang));
}
render(){
return (
<Tabber default={this.props.ui.selectedLang} className="lang" onSelect={this.handleSelect.bind(this)}>
<TabberOption value="ua">
<span className="flag-icon flag-icon-ua"></span>
</TabberOption>
<TabberOption value="eng">
<span className="flag-icon flag-icon-us"></span>
</TabberOption>
</Tabber>
);
}
}
const LangTabber = connect(state=>(state))(LanguageTabber);
class ColorPicker extends React.Component {
constructor(props){
super(props);
this.state = {
...this.props,
selected: this.props.selected || ""
};
}
componentWillReceiveProps(nextProps){
if(!this.state.list.compare(nextProps.list)){
this.setState({list: nextProps.list});
}
}
handleColorSelect(e){
const colorURL = e.target.getAttribute('value') || '';
this.setState({selected: colorURL});
if(this.state.onChange) this.state.onChange(colorURL);
}
renderColorItem(color, key){
const {selected} = this.state;
const value = !color ? "" : color.url;
const colorType = (!color && 'white') ||
(color.url === 'gray' && 'bluish-gray') ||
color.url;
const mainClass = 'item color';
const noneClass = !color ? 'none' : '';
const colorClass = `bg-${colorType}-bordered`;
const selectedClass = selected === value ? 'selected' : '';
const activeClass = selected != value ? 'active' : '';
const itemClass = [mainClass, noneClass, colorClass, selectedClass, activeClass].join(' ');
return (
<div
key={key}
className={itemClass}
value={value}
onClick={this.handleColorSelect.bind(this)}
></div>
);
}
renderLabel(){
const {label} = this.state;
return label ? (<label className="label"><Text tag={label}/></label>) : null;
}
renderItems(){
let list = this.state.list.map((color, key)=>{
return this.renderColorItem(color, key);
});
list.unshift(this.renderColorItem(null, Math.random()));
return list;
}
render(){
return(
<div
id={this.state.id || null}
className="colorpicker"
>
{this.renderLabel()}
<div className="list">
{this.renderItems()}
</div>
</div>
);
}
}
class SideA extends React.Component {
navToBySelector(path){
const {dispatch, history, location} = this.props;
if(location.pathname === path){
dispatch(fetchProducts);
} else {
history.push(path);
}
}
handleSectorChange(){
const {history, appliedFilters} = this.props;
switch(appliedFilters.gender){
case "male":
this.navToBySelector('/category/men');
break;
case "female":
this.navToBySelector('/category/women');
break;
default:
this.navToBySelector('/home');
break;
}
}
handleBrandChange(event){
const {dispatch} = this.props;
const brandFilter = event.target.value;
dispatch(setProductsBrandAction(brandFilter));
this.handleSectorChange();
// dispatch(fetchProducts);
}
handleCategoryChange(event){
const {dispatch} = this.props;
const catFilter = event.target.value;
dispatch(setProductsCategoryAction(catFilter));
this.handleSectorChange();
// dispatch(fetchProducts);
}
handleColorChange(colorURL){
const {dispatch} = this.props;
const colorFilter = colorURL;
console.log('change');
dispatch(setProductsColorAction(colorFilter));
// dispatch(fetchProducts);
this.handleSectorChange();
}
handleResetButtonClick(){
const {dispatch} = this.props;
dispatch(unsetProductsFilters());
// dispatch(fetchProducts);
this.handleSectorChange();
}
renderBrands(){
const selected = this.props.selectedBrand;
return (
<Selector
id="side-brand"
label="label_brand"
name="brand"
placeholder="placeholder_any_m"
list={this.props.brands}
selected={selected}
onChange={this.handleBrandChange.bind(this)}
disableDefault={false}
/>
);
}
renderCategories(){
const selected = this.props.selectedCategory;
return (
<Selector
id="side-category"
label="label_shoes_for"
name="category"
placeholder="placeholder_any_s"
list={this.props.categories}
selected={selected}
onChange={this.handleCategoryChange.bind(this)}
disableDefault={false}
/>
);
}
renderColors(){
const selected = this.props.selectedColor;
return (
<ColorPicker
id="side-color"
label="label_color"
list={this.props.colors}
selected={selected}
onChange={this.handleColorChange.bind(this)}
/>
// <Selector
// id="side-color"
// label="label_color"
// placeholder="placeholder_any_m"
// name="color"
// list={this.props.colors}
// selected={selected}
// onChange={this.handleColorChange.bind(this)}
// disableDefault={false}
// />
);
}
renderResetButton(){
const {ui} = this.props;
const btnMainClass = "btn";
const btnType = "wide";
const btnColor = "white";
const btbClass = [btnMainClass,btnType,btnColor].join(" ");
return (
<button
className={btbClass}
onClick={this.handleResetButtonClick.bind(this)}
>
<Icon name="replay" />
<Text tag="action_reset_filters" />
</button>
);
}
toggleNightMode(){
const {dispatch} = this.props;
dispatch(toggleNightModeAction());
}
renderNightModeToggler(){
const {ui} = this.props;
return (
<Toggler
iconOn="brightness_3"
iconOff="brightness_7"
onChange={this.toggleNightMode.bind(this)}
setAsChecked={this.props.ui.nightModeEnabled}
/>
);
}
componentDidMount(){
this.props.dispatch(fetchProductFilters);
}
render(){
return (
<aside id="a-side" className="a-side">
{this.renderCategories()}
<br/>
{this.renderBrands()}
<br/>
{this.renderColors()}
<br/>
{this.renderResetButton()}
<div className="page-actions">
<NightModeTogglerContainer />
<br/>
<LangTabber />
<br/>
<DevBy />
</div>
</aside>
);
}
}
const SideAContainer = connect(state=>{
const {filters, appliedFilters} = state.product;
return {
appliedFilters: appliedFilters,
selectedBrand: appliedFilters.brand,
brands: filters.brands,
selectedCategory: appliedFilters.category,
categories: filters.categories,
selectedColor: appliedFilters.color,
colors: filters.colors,
ui: state.ui,
};
})(withRouter(SideA));
const ProductImage = (props) => {
const {product} = props;
const styles = {
backgroundImage: `url('${product.image}')`
};
return (
<div className="image" style={styles}></div>
);
};
function prettifyCurrency(currencyName){
if(currencyName && currencyName.toLowerCase() === 'usd'){
return '$';
} else {
return '';
}
};
function prettifyPrice(price=''){
return price.toString().indexOf('.') > -1 ? price : price + '.00';
}
const ProductPricePlaceholder = ()=>{
return (
<div className="price placeholder">
<div className="item text"></div>
</div>
);
};
const ProductPrice = (props) => {
const prettyCurrency = prettifyCurrency(props.currency);
const prettyPrice = prettifyPrice(props.price);
const price = prettyCurrency + prettyPrice;
return (
<span className="price bold">
{" "}
{ props.old_price && (
<span className="old-price">
{prettyCurrency}
{props.old_price}
{" "}
</span>
)}
{price}
</span>
);
};
class Product extends React.Component {
constructor(props){
super(props);
this.state = props;
}
renderDiscount(){
const product = this.state.data;
if(!product.old_price){ return; }
const percents = Math.floor(100/product.old_price * product.price) + '%';
return (
<div className="discount">
{'-'}
{percents}
{' Off!'}
</div>
);
}
renderProduct(){
const product = this.state.data;
return (
<div className="product-content">
{this.renderDiscount()}
<ProductImage product={product} />
<div className="details">
<span className="name bold">
{product.name}
</span>
<ProductPrice price={product.price} currency={product.price_currency} />
</div>
</div>
);
}
render(){
const product = this.state.data;
const productURL = `/product/${product.url}`;
return (
<li className="product">
<NavLink
className="no-decoration"
to={productURL}
activeClassName="active-link"
>
{this.renderProduct()}
</NavLink>
</li>
);
}
};
class ProductPlaceholder extends React.Component {
constructor(props){
super(props);
this.state = props;
}
render(){
return (
<li className='product placeholder'>
<div className='image'></div>
</li>
);
}
}
class Products extends React.Component {
renderItem(product, index){
return (<Product key={index} data={product}/>);
}
renderPlaceholder(data, index){
return (<ProductPlaceholder key={index}/>);
}
renderProductsList(){
const { product } = this.props;
return product.fetchIsFulfilled && product.list.length > 0 ?
product.list.map(this.renderItem) :
null;
}
renderPlaceholderList(){
const { product } = this.props;
const itemsPerPage = product.settings.itemsPerPage;
return product.fetchIsPending ?
Array.from({ length: itemsPerPage }).map(this.renderPlaceholder) :
null;
}
renderEmptyList(){
const { product } = this.props;
return product.fetchIsFulfilled && product.list.length === 0 ?
(
<div className="informer">
<h3 className="title">
<Text tag="header_product_not_found" />
</h3>
<div className="text">
<Text tag="text_product_not_found" />
</div>
</div>
) :
null;
}
render(){
const { product } = this.props;
return (
<div className="products-wrapper" data-filter={product.appliedFilters.gender}>
<ul className="products">
{ this.renderProductsList() }
{ this.renderPlaceholderList() }
{ this.renderEmptyList() }
</ul>
<div className="clearfix"></div>
</div>
);
};
}
const ProductsContainer = connect(
state=>{
return {product: state.product};
}
)(Products);
class ProductsView extends React.Component {
renderCart(){
const cart = this.props.cart;
return cart.isOpened ?
(<CartContainer />) :
null;
}
render(){
const cart = this.props.cart;
const pageDefaultClass = "page";
const cartOpenedClass = cart.isOpened ? "cart-is-opened" : "";
const pageClass = [
pageDefaultClass,
cartOpenedClass,
].join(" ");
return (
<div id="homepage" className={pageClass}>
<SideAContainer />
<div className="content">
<h1 className="title">
<Text tag={this.props.pageData.title} />
</h1>
<ProductsContainer />
</div>
{this.renderCart()}
</div>
);
}
}
const ProductsViewContainer = connect(state=>{
return {
pageData: state.navigation.pages[state.navigation.currentPageSlug],
cart : state.ui.cart,
}
})(ProductsView);
const ProductDescriptionPlaceholder = (props) => {
const linesSize = (props.lines || 5);
const placeholderLines = Array.from(Array(linesSize).keys()).map((l,i)=>{
return (<div className="item text" key={i}></div>) ;
});
return (
<div className="description placeholder">
{placeholderLines}
</div>
);
};
const ProductDescription = ({description = ""}) => {
const descriptionHTML = description.split('\\n').map((item, key) => {
return (<span key={key}>{item}<br/></span>);
});
return (
<div className="description">
{descriptionHTML}
</div>
);
};
const ProductTitlePlaceholder = (props)=>{
return (
<div className="title placeholder">
<div className="item text"></div>
</div>
);
}
const ProductTitle = (props)=>{
return (
<h1 className="title">
{props.title}
</h1>
);
}
const ProductBuyButtonPlaceholder = (props)=>{
return (
<div className="button placeholder">
<div className="item text"></div>
</div>
);
}
class ProductDetails extends React.Component {
componentDidMount(){
const {dispatch} = this.props;
const productURL = this.props.openedURL;
dispatch(storeOpenedProductDetailsAction(productURL));
dispatch(fetchProductDetails);
}
componentWillUnmount() {
const {dispatch} = this.props;
dispatch(unsetOpenedProductURLAction());
dispatch(unsetProductDetailsDataAction());
}
renderPrice(){
const productData = this.props.product.details.data;
return this.props.product.details.fetchIsPending ?
(<ProductPricePlaceholder/>) :
(
<ProductPrice
price={productData.price}
currency={productData.price_currency}
old_price={productData.old_price}
/>
);
}
renderDescription(){
const details = this.props.product.details;
return details.fetchIsPending ?
(<ProductDescriptionPlaceholder lines={5}/>) :
(<ProductDescription description={details.data.description}/>);
}
renderTitle(){
const details = this.props.product.details;
return details.fetchIsPending ?
(<ProductTitlePlaceholder/>) :
(<ProductTitle title={details.data.name}/>);
}
handleButtonClick(){
const {dispatch} = this.props;
const productData = this.props.product.details.data;
dispatch(addProductToCart(productData));
}
renderCart(){
const cart = this.props.ui.cart;
return cart.isOpened ?
(<CartContainer />) :
null;
}
renderButton(){
const details = this.props.product.details;
return details.fetchIsPending ?
(<ProductBuyButtonPlaceholder/>) :
(
<button
className="btn red"
onClick={this.handleButtonClick.bind(this)}
>
<Text tag="action_add_to_cart" />
</button>
);
}
renderGender(){
const details = this.props.product.details;
const productData = details.data;
const genderText = 'stitle_for_' +
(
(productData.gender === 'male' && 'him') ||
(productData.gender === 'female' && 'her') ||
'everyone'
);
return details.fetchIsPending ?
(
<div className="gender placeholder">
<span className="item text"></span>
</div>
) :
(<span className="gender">
<Text tag={genderText} />
</span>);
}
render(){
const productData = this.props.product.details.data;
const cart = this.props.ui.cart;
const pageDefaultClass = "page product product-details";
const cartOpenedClass = cart.isOpened ? "cart-is-opened" : "";
const genderClass = `product-${productData.gender}`;
const pageClass = [
pageDefaultClass,
cartOpenedClass,
genderClass
].join(" ");
return (
<div id="details" className={pageClass}>
<SideAContainer />
<div className="content">
<div className="media">
<ProductImage product={productData} />
</div>
<div className="details">
<div className="title-wrapper">
{this.renderTitle()}
</div>
<div className="gender-wrapper">
{this.renderGender()}
</div>
<div className="price-wrapper">
{this.renderPrice()}
</div>
<div className="description-wrapper">
<div className="subtitle">
<span className="text bold">
<Text tag="header_description"/>
</span>
</div>
{this.renderDescription()}
</div>
<div className="actions-wrapper">
{this.renderButton()}
</div>
</div>
</div>
{this.renderCart()}
</div>
);
}
}
const ProductDetailsContainer = connect(store => (store))(ProductDetails);
class Homepage extends React.Component {
componentDidMount(){
const {dispatch} = this.props;
dispatch(navigateAction('home'));
dispatch(setProductsGenderAction(''));
dispatch(fetchProducts);
}
render(){
return (
<ProductsViewContainer />
);
}
};
const HomepageContainer = connect(store=>(store))(Homepage);
const DevBy = () => (
<a
id="devby"
href="https://levchenkod.com?utm_source=old_react_store&utm_medium=devby&utm_campaign=codepen"
target="_blank"
style={{
opacity: 0.8,
backgroundColor: "transparent",
color: "rgb(25, 25, 25)",
height: 20,
fontSize: 13,
padding: "0.2rem 0.4rem 0.2rem 0.6rem",
gap: "0.2rem",
display: "flex",
margin: "0px auto",
maxWidth: "max-content",
placeContent: "center",
placeItems: "center",
borderRadius: "2rem",
fontFamily:
"__Inter_aaf875, __Inter_Fallback_aaf875, Geneva, Verdana, sans-serif",
textDecoration: "none",
boxSizing: "content-box"
}}
>
<span>Developed by</span>
<img
title="Dmytro Levchenko"
alt="Levchenko Dmytro Monogram Logo"
fetchpriority="high"
width={18}
height={18}
decoding="async"
data-nimg={1}
className="transition duration-500"
style={{ color: "transparent", width: 18, height: 18 }}
src="https://levchenkod.com/img/levchenkod-logo-symbol-32-dark.svg"
/>
</a>
);
class WomenPage extends React.Component {
componentDidMount(){
const {dispatch} = this.props;
dispatch(navigateAction('women'));
dispatch(setProductsGenderAction('female'));
dispatch(fetchProducts);
}
render(){
return (
<ProductsViewContainer />
);
}
};
const WomenPageContainer = connect(store=>(store))(WomenPage);
class MenPage extends React.Component {
componentDidMount(){
const {dispatch} = this.props;
dispatch(navigateAction('men'));
dispatch(setProductsGenderAction('male'));
dispatch(fetchProducts);
}
render(){
return (
<ProductsViewContainer />
);
}
};
const MenPageContainer = connect(store=>(store))(MenPage);
class Roster extends React.Component {
render(){
return (
<Switch>
<Route
exact path='/home'
render={
()=>{
return (<HomepageContainer/>);
}
}
/>
<Route
exact path='/category/women'
render={
()=>{
return (<WomenPageContainer/>);
}
}
/>
<Route
exact path='/category/men'
render={
()=>{
return (<MenPageContainer/>);
}
}
/>
<Route
path='/product/:product_url'
render={
(data)=>{
const openedURL = data.match.params.product_url;
return (<ProductDetailsContainer openedURL={openedURL}/>);
}
}
/>
<Redirect from='/' to="/home"/>
</Switch>
);
}
}
class App extends React.Component {
render(){
const innerClass = "app-inner";
const nightModeClass = this.props.ui.nightModeEnabled ? "night-mode" : "";
const appClassName = [innerClass, nightModeClass].join(" ");
return (
<BrowserRouter>
<div className={appClassName}>
<HeaderContainer />
<Roster />
</div>
</BrowserRouter>
);
}
}
const AppContainer = connect(state=>(state))(App);
ReactDOM.render((
<Provider store={store}>
<AppContainer />
</Provider>
), app);
View Compiled