diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 248f37000..82e260bf3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -38,6 +38,7 @@ "leaflet": "^1.9.4", "leaflet-boundary-canvas": "^1.0.0", "react": "^19.0.0", + "react-circle-flags": "^0.0.23", "react-dom": "^19.0.0", "react-ga4": "^2.1.0", "react-google-recaptcha-v3": "^1.10.1", @@ -5897,6 +5898,18 @@ "node": ">=0.10.0" } }, + "node_modules/react-circle-flags": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/react-circle-flags/-/react-circle-flags-0.0.23.tgz", + "integrity": "sha512-J35vkTnG4Iz7cgitq/UIw4/1i8aUWEkz2Z0MQTTWycWOHKezuey5+3BPAsXNr78gYMdjSBtZWmSzQc0oEeK4oQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.0.0 || 17.x || 18.x || 19.x" + } + }, "node_modules/react-dom": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index ef00a605b..f46f51641 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -45,6 +45,7 @@ "leaflet": "^1.9.4", "leaflet-boundary-canvas": "^1.0.0", "react": "^19.0.0", + "react-circle-flags": "^0.0.23", "react-dom": "^19.0.0", "react-ga4": "^2.1.0", "react-google-recaptcha-v3": "^1.10.1", diff --git a/frontend/src/assets/css/header.css b/frontend/src/assets/css/header.css index 514121f8f..8af0977f8 100644 --- a/frontend/src/assets/css/header.css +++ b/frontend/src/assets/css/header.css @@ -11,6 +11,18 @@ z-index: 1401 !important; } +.menu div.language, +.btn div.language { + display: flex; + flex-direction: row; + align-items: center; +} + +.menu div.language .flag { + width: 24px; + margin-right: 10px; +} + .header-action { margin-right: 20px; } diff --git a/frontend/src/assets/css/home.css b/frontend/src/assets/css/home.css index 4caa73d98..69b88bfe8 100644 --- a/frontend/src/assets/css/home.css +++ b/frontend/src/assets/css/home.css @@ -102,7 +102,7 @@ div.home-subtitle { div.home div.search { z-index: 2; - background-color: #F9F9F9; + background-color: #fff; width: 100%; display: flex; flex-direction: column; @@ -121,7 +121,7 @@ div.home div.search div.home-search { } div.home div.why { - background-color: #F9F9F9; + background-color: #fff; width: 100%; padding: 40px 0; display: flex; diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 8f5b7173c..704e62e50 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react' -import { toast } from 'react-toastify' +import { useNavigate } from 'react-router-dom' import { AppBar, Toolbar, @@ -21,7 +21,6 @@ import { Mail as MailIcon, Notifications as NotificationsIcon, More as MoreIcon, - Language as LanguageIcon, Settings as SettingsIcon, Home as HomeIcon, InfoTwoTone as AboutIcon, @@ -34,7 +33,8 @@ import { PrivacyTip as PrivacyIcon, QuestionAnswer as FaqIcon, } from '@mui/icons-material' -import { useNavigate } from 'react-router-dom' +import { toast } from 'react-toastify' +import { CircleFlag } from 'react-circle-flags' import * as bookcarsTypes from ':bookcars-types' import env from '@/config/env.config' import { strings } from '@/lang/header' @@ -45,9 +45,12 @@ import Avatar from './Avatar' import * as langHelper from '@/common/langHelper' import * as helper from '@/common/helper' import { useGlobalContext, GlobalContextType } from '@/context/GlobalContext' +import * as StripeService from '@/services/StripeService' import '@/assets/css/header.css' +const flagHeight = 28 + interface HeaderProps { user?: bookcarsTypes.User hidden?: boolean @@ -69,6 +72,7 @@ const Header = ({ const [lang, setLang] = useState(helper.getLanguage(env.DEFAULT_LANGUAGE)) const [anchorEl, setAnchorEl] = useState(null) const [langAnchorEl, setLangAnchorEl] = useState(null) + const [currencyAnchorEl, setCurrencyAnchorEl] = useState(null) const [mobileMoreAnchorEl, setMobileMoreAnchorEl] = useState(null) const [sideAnchorEl, setSideAnchorEl] = useState(null) const [isSignedIn, setIsSignedIn] = useState(false) @@ -78,6 +82,7 @@ const Header = ({ const isMenuOpen = Boolean(anchorEl) const isMobileMenuOpen = Boolean(mobileMoreAnchorEl) const isLangMenuOpen = Boolean(langAnchorEl) + const isCurrencyMenuOpen = Boolean(currencyAnchorEl) const isSideMenuOpen = Boolean(sideAnchorEl) const classes = { @@ -112,6 +117,10 @@ const Header = ({ setLangAnchorEl(event.currentTarget) } + const handleCurrencyMenuOpen = (event: React.MouseEvent) => { + setCurrencyAnchorEl(event.currentTarget) + } + const refreshPage = () => { const params = new URLSearchParams(window.location.search) @@ -156,6 +165,21 @@ const Header = ({ } } + const handleCurrencyMenuClose = async (event: React.MouseEvent) => { + setCurrencyAnchorEl(null) + + const { code } = event.currentTarget.dataset + if (code) { + const currentCurrency = StripeService.getCurrency() + + if (code && code !== currentCurrency) { + StripeService.setCurrency(code) + // Refresh page + refreshPage() + } + } + } + const handleMenuClose = () => { setAnchorEl(null) handleMobileMenuClose() @@ -246,12 +270,12 @@ const Header = ({

{strings.SETTINGS}

- + {/*

{strings.LANGUAGE}

-
+
*/} @@ -276,7 +300,32 @@ const Header = ({ { env._LANGUAGES.map((language) => ( - {language.label} +
+ + {language.label} +
+
+ )) + } + + ) + + const currencyMenuId = 'currency-menu' + const renderCurrencyMenu = ( + + { + env._CURRENCIES.map((_currency) => ( + + {_currency.code} )) } @@ -349,23 +398,30 @@ const Header = ({ {(env.isMobile || !headerTitle) &&
}
- {isSignedIn && ( - - 0 ? notificationCount : null} color="error"> - - - - )} {!hideSignin && !isSignedIn && isLoaded && !loading && ( - )} {isLoaded && !loading && ( - )} + {isLoaded && !loading && ( + + )} + {isSignedIn && ( + + 0 ? notificationCount : null} color="error"> + + + + )} {isSignedIn && ( @@ -373,9 +429,17 @@ const Header = ({ )}
- {!isSignedIn && !loading && ( - + )} + {!loading && ( + )} {isSignedIn && ( @@ -397,6 +461,7 @@ const Header = ({ {renderMobileMenu} {renderMenu} {renderLanguageMenu} + {renderCurrencyMenu}
)) || <> ) diff --git a/frontend/src/config/env.config.ts b/frontend/src/config/env.config.ts index 3209a1999..0abcaf106 100644 --- a/frontend/src/config/env.config.ts +++ b/frontend/src/config/env.config.ts @@ -1,7 +1,7 @@ import * as bookcarsTypes from ':bookcars-types' import Const from './const' -type Language = { code: string, label: string } +type Language = { code: string, countryCode: string, label: string } /** * ISO 639-1 language codes and their labels @@ -12,14 +12,17 @@ type Language = { code: string, label: string } const LANGUAGES: Language[] = [ { code: 'en', + countryCode: 'us', label: 'English', }, { code: 'fr', + countryCode: 'fr', label: 'Français', }, { code: 'es', + countryCode: 'es', label: 'Español', }, ] diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 8b1397395..2dac0787a 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -44,8 +44,10 @@ import { strings as settingsStrings } from '@/lang/settings' import { strings as signInStrings } from '@/lang/sign-in' import { strings as signUpStrings } from '@/lang/sign-up' import { strings as tosStrings } from '@/lang/tos' - -// import 'github-fork-ribbon-css/gh-fork-ribbon.css' +import { strings as newsletterFormStrings } from '@/lang/newsletter-form' +import { strings as privacyStrings } from '@/lang/privacy' +import { strings as faqListStrings } from '@/lang/faq-list' +import { strings as checkoutStatusStrings } from '@/lang/checkout-status' import 'react-toastify/dist/ReactToastify.min.css' import '@/assets/css/common.css' @@ -145,6 +147,10 @@ if (lang) { signUpStrings.setLanguage(_lang) tosStrings.setLanguage(_lang) carSpecsStrings.setLanguage(_lang) + newsletterFormStrings.setLanguage(_lang) + privacyStrings.setLanguage(_lang) + faqListStrings.setLanguage(_lang) + checkoutStatusStrings.setLanguage(_lang) } if (env.SET_LANGUAGE_FROM_IP && !storedLang) { @@ -166,25 +172,30 @@ const isEs = language === 'es' const theme = createTheme( { - // palette: { - // primary: { - // main: '#1976D2', - // contrastText: '#121212', - // dark: '#1976D2', - // }, - // }, + palette: { + primary: { + main: '#003B95', + // main: '#006CE4', + + // contrastText: '#003B95', + // dark: '#003B95', + }, + // text: { + // primary: '#003B95', + // } + }, typography: { fontFamily: [ + '-apple-system', + 'BlinkMacSystemFont', + '"Segoe UI"', 'Roboto', - "'Helvetica Neue'", + '"Helvetica Neue"', 'Arial', 'sans-serif', - "'Apple Color Emoji'", - "'Segoe UI Emoji'", - "'Segoe UI Symbol'", - '-apple-system', - 'BlinkMacSystemFont', - "'Segoe UI'", + '"Apple Color Emoji"', + '"Segoe UI Emoji"', + '"Segoe UI Symbol"', ].join(','), }, components: { @@ -264,14 +275,6 @@ ReactDOM.createRoot(document.getElementById('root')!).render( pauseOnHover theme="dark" /> - {/* - Fork me on GitHub - */} , ) diff --git a/frontend/src/services/StripeService.ts b/frontend/src/services/StripeService.ts index 4ff43f2d4..e9b6dc34b 100644 --- a/frontend/src/services/StripeService.ts +++ b/frontend/src/services/StripeService.ts @@ -51,7 +51,9 @@ export const createPaymentIntent = (payload: bookcarsTypes.CreatePaymentPayload) * @param {string} currency */ export const setCurrency = (currency: string) => { - localStorage.setItem('bc-fe-currency', currency) + if (currency && bookcarsHelper.checkCurrency(currency.toUpperCase())) { + localStorage.setItem('bc-fe-currency', currency.toUpperCase()) + } } /** @@ -61,8 +63,8 @@ export const setCurrency = (currency: string) => { */ export const getCurrency = () => { const currency = localStorage.getItem('bc-fe-currency') - if (currency && bookcarsHelper.checkCurrency(currency)) { - return currency + if (currency && bookcarsHelper.checkCurrency(currency.toUpperCase())) { + return currency.toUpperCase() } return env.BASE_CURRENCY }