Skip to content

Commit 9c3bc75

Browse files
Added currency support and custom dropdown (#25)
* Added currency support and custom dropdown * Fetching list of currencies from server * Removed currency codes from translation files * Minor refactor
1 parent f7579b1 commit 9c3bc75

File tree

11 files changed

+252
-28
lines changed

11 files changed

+252
-28
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"node-sass": "^4.13.0",
1616
"react": "^16.13.1",
1717
"react-axe": "^3.5.2",
18+
"react-cool-onclickoutside": "^1.5.2",
1819
"react-dom": "^16.13.1",
1920
"react-router-dom": "^5.2.0",
2021
"react-script-hook": "^1.1.1",

src/AppHeader.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,14 @@ import React from 'react';
22
import { Link } from "react-router-dom";
33
import { ImageContainer } from './ImageContainer';
44
import { useTranslation } from './app-state';
5+
import { LanguageDropdown } from './LanguageDropdown';
56

67
import './AppHeader.scss';
78
import headerLogo from './images/site-images/Company-Logo.svg';
89

910

1011
export const AppHeader: React.FC = () => {
11-
const { t, selectedLanguage, setLanguage } = useTranslation();
12-
13-
const onChangeLang = (e: React.FocusEvent<HTMLSelectElement>) => {
14-
setLanguage(e.target.value);
15-
};
12+
const { t } = useTranslation();
1613

1714
return (
1815
<div className="appheader">
@@ -22,10 +19,7 @@ export const AppHeader: React.FC = () => {
2219
</Link>
2320
</div>
2421
<div className="appheader__language">
25-
<select value={selectedLanguage} aria-label={t('language')} onChange={onChangeLang} onBlur={onChangeLang}>
26-
<option value="en">{t('english')}</option>
27-
<option value="fr">{t('french')}</option>
28-
</select>
22+
<LanguageDropdown />
2923
</div>
3024
<div className="appheader__moltincartcontainer">
3125
<span className="moltin-cart-button"></span>

src/Category.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useState, useEffect } from 'react';
22
import { useParams } from 'react-router-dom';
33
import { loadCategoryProducts } from './service';
4-
import { useCategories } from './app-state';
4+
import { useCategories, useTranslation, useCurrency } from './app-state';
55
import { ProductThumbnail } from './ProductThumbnail';
66
import { createCategoryUrl } from './routes';
77
import { Pagination } from './Pagination';
@@ -10,6 +10,9 @@ import { useResolve } from './hooks';
1010
import './Category.scss';
1111

1212
function useCategoryProducts(categoryId: string | undefined, pageNum: number) {
13+
const { selectedLanguage } = useTranslation();
14+
const { selectedCurrency } = useCurrency();
15+
1316
const [totalPages, setTotalPages] = useState<number>();
1417

1518
useEffect(() => {
@@ -20,11 +23,11 @@ function useCategoryProducts(categoryId: string | undefined, pageNum: number) {
2023
const [products] = useResolve(async () => {
2124
// during initial loading of categories categoryId might be undefined
2225
if (categoryId) {
23-
const result = await loadCategoryProducts(categoryId, pageNum);
26+
const result = await loadCategoryProducts(categoryId, pageNum, selectedLanguage, selectedCurrency);
2427
setTotalPages(result.pagination.totalPages);
2528
return result;
2629
}
27-
}, [categoryId, pageNum]);
30+
}, [categoryId, pageNum, selectedLanguage, selectedCurrency]);
2831

2932
return { products, totalPages };
3033
}

src/LanguageDropdown.scss

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
@import './theme/common';
2+
3+
.languagedropdown {
4+
$block: &;
5+
6+
position: relative;
7+
8+
#{$block}__selectorbtn {
9+
padding: 5px 5px;
10+
cursor: pointer;
11+
12+
&:focus {
13+
box-shadow: 0 0 0px 2px $btnFocusOutlineColor;
14+
}
15+
}
16+
17+
#{$block}__dropdown {
18+
position: absolute;
19+
border-radius: 5px;
20+
box-shadow: 0 0 10px hsla(0, 0, 0, 0.2);
21+
width: 150px;
22+
right: 0;
23+
top: 100%;
24+
background-color: white;
25+
padding: 5px 0;
26+
z-index: 10;
27+
28+
#{$block}__section {
29+
padding: 5px 0;
30+
border-bottom: 1px solid hsl(0, 0, 95%);
31+
32+
&:last-child {
33+
border-bottom: none;
34+
}
35+
36+
#{$block}__title {
37+
padding: 5px 12px;
38+
font-size: smaller;
39+
font-weight: bold;
40+
}
41+
42+
#{$block}__itembtn {
43+
display: block;
44+
width: 100%;
45+
padding: 5px 12px;
46+
cursor: pointer;
47+
48+
&:focus {
49+
box-shadow: 0 0 0px 2px $btnFocusOutlineColor inset;
50+
}
51+
52+
&:hover:not(:disabled) {
53+
background-color: hsl(0, 0, 95%);
54+
}
55+
56+
&:disabled {
57+
color: hsl(0, 0, 80%);
58+
}
59+
}
60+
}
61+
}
62+
}

src/LanguageDropdown.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React, { useState } from 'react';
2+
import useOnclickOutside from 'react-cool-onclickoutside';
3+
import { useTranslation, useCurrency } from './app-state';
4+
5+
import './LanguageDropdown.scss';
6+
7+
const languages = [
8+
{ key: 'en', name: 'english' },
9+
{ key: 'fr', name: 'french' },
10+
];
11+
12+
export const LanguageDropdown: React.FC = () => {
13+
const [isOpen, setIsOpen] = useState(false);
14+
const { t, selectedLanguage, setLanguage } = useTranslation();
15+
const { allCurrencies, selectedCurrency, setCurrency } = useCurrency();
16+
17+
const selectedLangName = languages.find(l => l.key === selectedLanguage)!.name;
18+
19+
const handleSelectorClicked = () => {
20+
setIsOpen(true);
21+
};
22+
23+
const ref = useOnclickOutside(() => {
24+
setIsOpen(false);
25+
});
26+
27+
const handleLanguageSelected = (lang: string) => {
28+
setLanguage(lang);
29+
setIsOpen(false);
30+
}
31+
32+
const handleCurrencySelected = (currency: string) => {
33+
setCurrency(currency);
34+
setIsOpen(false);
35+
};
36+
37+
return (
38+
<div className="languagedropdown">
39+
<button className="languagedropdown__selectorbtn" onClick={handleSelectorClicked}>
40+
{`${t(selectedLangName)}/${selectedCurrency}`}
41+
</button>
42+
{isOpen && (
43+
<div ref={ref} className="languagedropdown__dropdown">
44+
<div className="languagedropdown__section">
45+
<div className="languagedropdown__title">
46+
{t('language')}
47+
</div>
48+
{languages.map(l => (
49+
<button
50+
key={l.key}
51+
className="languagedropdown__itembtn"
52+
disabled={selectedLanguage === l.key}
53+
onClick={() => handleLanguageSelected(l.key)}
54+
>
55+
{t(l.name)}
56+
</button>
57+
))}
58+
</div>
59+
<div className="languagedropdown__section">
60+
<div className="languagedropdown__title">
61+
Currency
62+
</div>
63+
{allCurrencies.map(c => (
64+
<button
65+
key={c.id}
66+
className="languagedropdown__itembtn"
67+
disabled={selectedCurrency === c.code}
68+
onClick={() => handleCurrencySelected(c.code)}
69+
>
70+
{c.code}
71+
</button>
72+
))}
73+
</div>
74+
</div>
75+
)}
76+
</div>
77+
);
78+
};

src/Product.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import { useParams } from 'react-router-dom';
33
import { useResolve } from './hooks';
44
import { loadProductBySlug } from './service';
55
import { CompareCheck } from './CompareCheck';
6-
import { useTranslation } from './app-state';
6+
import { ProductMainImage } from './ProductMainImage';
7+
import { useTranslation, useCurrency } from './app-state';
78

89
import './Product.scss';
9-
import { ProductMainImage } from './ProductMainImage';
1010

1111

1212
interface ProductParams {
@@ -16,7 +16,11 @@ interface ProductParams {
1616
export const Product: React.FC = () => {
1717
const { productSlug } = useParams<ProductParams>();
1818
const { t, selectedLanguage } = useTranslation();
19-
const [product] = useResolve(async () => loadProductBySlug(productSlug, selectedLanguage), [productSlug, selectedLanguage]);
19+
const { selectedCurrency } = useCurrency();
20+
const [product] = useResolve(
21+
async () => loadProductBySlug(productSlug, selectedLanguage, selectedCurrency),
22+
[productSlug, selectedLanguage, selectedCurrency]
23+
);
2024

2125
return (
2226
<div className="product">

src/app-state.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState, useEffect } from 'react';
22
import constate from 'constate';
33
import { Category, loadCategoryTree, Product } from './service';
4+
import * as service from './service';
45
import { config } from './config';
56

67
import en from './locales/en.json';
@@ -11,7 +12,7 @@ const translations: { [lang: string]: { [name: string]: string } } = {
1112
fr,
1213
};
1314

14-
const fallbackLanguage = 'en';
15+
const defaultLanguage = 'en';
1516

1617
function getInitialLanguage(): string {
1718
const savedLanguage = localStorage.getItem('selectedLanguage');
@@ -44,7 +45,7 @@ function getInitialLanguage(): string {
4445
}
4546
}
4647

47-
return fallbackLanguage;
48+
return defaultLanguage;
4849
}
4950

5051
function checkTranslations() {
@@ -101,6 +102,48 @@ function useTranslationState() {
101102
};
102103
}
103104

105+
const defaultCurrency = 'USD';
106+
107+
function useCurrencyState() {
108+
const [allCurrencies, setAllCurrencies] = useState<service.Currency[]>([]);
109+
// Set previously saved or defautlt currency before fetching the list of supported ones
110+
const [selectedCurrency, setSelectedCurrency] = useState(localStorage.getItem('selectedCurrency') ?? defaultCurrency);
111+
112+
const setCurrency = (newCurrency: string) => {
113+
localStorage.setItem('selectedCurrency', newCurrency);
114+
setSelectedCurrency(newCurrency);
115+
};
116+
117+
useEffect(() => {
118+
// Only fetch currencies once
119+
if (allCurrencies.length > 0) {
120+
return;
121+
}
122+
123+
service.loadEnabledCurrencies().then(currencies => {
124+
// Check if we need to update selectedCurrency
125+
const selected = currencies.find(c => c.code === selectedCurrency);
126+
127+
if (!selected) {
128+
// Saved or default currency we initially selected was not found in the list of server currencies
129+
// Switch selectedCurrency to server default one if exist or first one in the list
130+
setSelectedCurrency(currencies.find(c => c.default)?.code ?? currencies[0].code);
131+
132+
// Clear selection in local storage
133+
localStorage.removeItem('selectedCurrency');
134+
}
135+
136+
setAllCurrencies(currencies);
137+
});
138+
}, [allCurrencies.length, selectedCurrency]);
139+
140+
return {
141+
allCurrencies,
142+
selectedCurrency,
143+
setCurrency,
144+
}
145+
}
146+
104147
function getCategoryPaths(categories: Category[]): { [categoryId: string]: Category[] } {
105148
const lastCat = categories[categories.length - 1];
106149

@@ -177,9 +220,11 @@ function useCompareProductsState() {
177220

178221
function useGlobalState() {
179222
const translation = useTranslationState();
223+
const currency = useCurrencyState();
180224

181225
return {
182226
translation,
227+
currency,
183228
categories: useCategoriesState(translation.selectedLanguage),
184229
compareProducts: useCompareProductsState(),
185230
};
@@ -188,11 +233,13 @@ function useGlobalState() {
188233
export const [
189234
AppStateProvider,
190235
useTranslation,
236+
useCurrency,
191237
useCategories,
192238
useCompareProducts,
193239
] = constate(
194240
useGlobalState,
195241
value => value.translation,
242+
value => value.currency,
196243
value => value.categories,
197244
value => value.compareProducts
198245
);

src/index.scss

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ body {
2424
height: 100%;
2525
}
2626

27-
$btnColor: hsl(190, 70%, 34%);
28-
2927
.epbtn {
3028
position: relative;
3129
text-align: center;
@@ -62,7 +60,7 @@ $btnColor: hsl(190, 70%, 34%);
6260
}
6361

6462
&:focus {
65-
box-shadow: 0 0 0px 2px change-color($color: $btnColor, $saturation: 35%, $lightness: 75%);
63+
box-shadow: 0 0 0px 2px btnFocusOutlineColor;
6664
}
6765

6866
&:active {

0 commit comments

Comments
 (0)