diff --git a/api/src/controllers/userController.ts b/api/src/controllers/userController.ts
index 73dc9c108..a68ac6c6b 100644
--- a/api/src/controllers/userController.ts
+++ b/api/src/controllers/userController.ts
@@ -1539,15 +1539,18 @@ export const verifyRecaptcha = async (req: Request, res: Response) => {
*/
export const sendEmail = async (req: Request, res: Response) => {
try {
- const { body }: { body: bookcarsTypes.SendEmailPayload } = req
- const { from, to, subject, message, recaptchaToken: token, ip, isContactForm } = body
- const result = await axios.get(`https://www.google.com/recaptcha/api/siteverify?secret=${encodeURIComponent(env.RECAPTCHA_SECRET)}&response=${encodeURIComponent(token)}&remoteip=${ip}`)
- const { success } = result.data
-
- if (!success) {
- return res.sendStatus(400)
+ const whitelist = [
+ helper.trimEnd(env.BACKEND_HOST, '/'),
+ helper.trimEnd(env.FRONTEND_HOST, '/'),
+ ]
+ const { origin } = req.headers
+ if (!origin || whitelist.indexOf(helper.trimEnd(origin, '/')) === -1) {
+ throw new Error('Unauthorized!')
}
+ const { body }: { body: bookcarsTypes.SendEmailPayload } = req
+ const { from, to, subject, message, isContactForm } = body
+
const mailOptions: nodemailer.SendMailOptions = {
from: env.SMTP_FROM,
to,
@@ -1563,8 +1566,8 @@ export const sendEmail = async (req: Request, res: Response) => {
return res.sendStatus(200)
} catch (err) {
- logger.error(`[user.delete] ${i18n.t('DB_ERROR')} ${JSON.stringify(req.body)}`, err)
- return res.status(400).send(i18n.t('DB_ERROR') + err)
+ logger.error(`[user.sendEmail] ${JSON.stringify(req.body)}`, err)
+ return res.status(400).send(err)
}
}
diff --git a/api/src/routes/bookingRoutes.ts b/api/src/routes/bookingRoutes.ts
index 1f847f4c1..7a472a599 100644
--- a/api/src/routes/bookingRoutes.ts
+++ b/api/src/routes/bookingRoutes.ts
@@ -11,7 +11,7 @@ routes.route(routeNames.update).put(authJwt.verifyToken, bookingController.updat
routes.route(routeNames.updateStatus).post(authJwt.verifyToken, bookingController.updateStatus)
routes.route(routeNames.delete).post(authJwt.verifyToken, bookingController.deleteBookings)
routes.route(routeNames.deleteTempBooking).delete(bookingController.deleteTempBooking)
-routes.route(routeNames.getBooking).get(authJwt.verifyToken, bookingController.getBooking)
+routes.route(routeNames.getBooking).get(bookingController.getBooking)
routes.route(routeNames.getBookingId).get(bookingController.getBookingId)
routes.route(routeNames.getBookings).post(authJwt.verifyToken, bookingController.getBookings)
routes.route(routeNames.hasBookings).get(authJwt.verifyToken, bookingController.hasBookings)
diff --git a/frontend/index.html b/frontend/index.html
index c584ad38e..18b57a264 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1,15 +1,18 @@
-
-
-
-
-
-
- BookCars Rental Service
-
-
-
-
-
+
+
+
+
+
+
+
+ BookCars Rental Service
+
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 40ddb838e..25a3c15b1 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -41,7 +41,6 @@
"react-circle-flags": "^0.0.23",
"react-dom": "^19.0.0",
"react-ga4": "^2.1.0",
- "react-google-recaptcha-v3": "^1.10.1",
"react-leaflet": "^5.0.0-rc.2",
"react-localization": "^1.0.19",
"react-router-dom": "^7.0.2",
@@ -5939,19 +5938,6 @@
"integrity": "sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==",
"license": "MIT"
},
- "node_modules/react-google-recaptcha-v3": {
- "version": "1.10.1",
- "resolved": "https://registry.npmjs.org/react-google-recaptcha-v3/-/react-google-recaptcha-v3-1.10.1.tgz",
- "integrity": "sha512-K3AYzSE0SasTn+XvV2tq+6YaxM+zQypk9rbCgG4OVUt7Rh4ze9basIKefoBz9sC0CNslJj9N1uwTTgRMJQbQJQ==",
- "license": "MIT",
- "dependencies": {
- "hoist-non-react-statics": "^3.3.2"
- },
- "peerDependencies": {
- "react": "^16.3 || ^17.0 || ^18.0",
- "react-dom": "^17.0 || ^18.0"
- }
- },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index d5c9a0430..30aa1ee13 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -48,7 +48,6 @@
"react-circle-flags": "^0.0.23",
"react-dom": "^19.0.0",
"react-ga4": "^2.1.0",
- "react-google-recaptcha-v3": "^1.10.1",
"react-leaflet": "^5.0.0-rc.2",
"react-localization": "^1.0.19",
"react-router-dom": "^7.0.2",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 15a9766c4..a7ebfdaf1 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -4,7 +4,6 @@ import SuspenseRouter from '@/components/SuspenseRouter'
import env from '@/config/env.config'
import { GlobalProvider } from '@/context/GlobalContext'
import { init as initGA } from '@/common/ga4'
-import ReCaptchaProvider from '@/components/ReCaptchaProvider'
import ScrollToTop from '@/components/ScrollToTop'
if (env.GOOGLE_ANALYTICS_ENABLED) {
@@ -36,41 +35,39 @@ const Faq = lazy(() => import('@/pages/Faq'))
const App = () => (
-
-
-
+
+
-
-
>}>
-
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- {/* } /> */}
- } />
- } />
- } />
- } />
- } />
- } />
- } />
+
+ >}>
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ {/* } /> */}
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
- } />
-
-
-
-
-
+ } />
+
+
+
+
)
diff --git a/frontend/src/assets/css/home.css b/frontend/src/assets/css/home.css
index 0cc80b702..1eda644d9 100644
--- a/frontend/src/assets/css/home.css
+++ b/frontend/src/assets/css/home.css
@@ -73,8 +73,9 @@ div.home div.home-content::after {
width: 100%;
top: 0;
left: 0;
- background: #321807;
- opacity: 0.5;
+ /* background: #321807;
+ opacity: 0.5; */
+ background: #003B95;
z-index: -1;
}
@@ -91,18 +92,27 @@ div.home div.home-cover {
color: #fff;
text-align: center;
-webkit-font-smoothing: antialiased;
- letter-spacing: .065rem;
+ letter-spacing: .095rem;
padding: 0 20px;
}
div.home-subtitle {
color: #fff;
- font-size: 26px;
+ font-size: 28px;
+ padding-top: 10px;
+ font-weight: 400;
+ padding: 10px;
+ text-align: center;
+}
+
+div.home div.home-cover,
+div.home-subtitle {
+ font-family: "Blue Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important;
}
div.home div.search {
z-index: 2;
- background-color: #fff;
+ background-color: #FFF;
width: 100%;
display: flex;
flex-direction: column;
@@ -120,8 +130,51 @@ div.home div.search div.home-search {
width: fit-content;
}
+div.home div.search div.search-header {
+ height: 52px;
+ /* width: calc(100% + 28px); */
+ width: calc(100% + 18px);
+ background-color: #003B95;
+ /* background-color: #006CE4; */
+ /* margin: -17px 0 20px 0; */
+ margin: -19px 0 20px 0;
+ border-radius: 5px;
+ content: '';
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+}
+
+div.home div.search div.search-header .search-icon-btn {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ color: #FFF;
+ margin: 0 5px;
+ width: 38px;
+}
+
+div.home div.search div.search-header .search-icon {
+ font-size: 28px;
+}
+
+div.home div.search div.search-header .search-icon-btn:hover>*,
+div.home div.search div.search-header .search-icon-selected {
+ color: #FFB700;
+}
+
+div.home div.search div.search-header .search-icon-btn:hover>svg {
+ font-size: 32px;
+}
+
+div.home div.search div.search-header .search-icon-btn span {
+ font-size: 11px;
+ font-weight: 300;
+}
+
div.home div.why {
- background-color: #fff;
+ background-color: #FFF;
width: 100%;
padding: 40px 0;
display: flex;
@@ -153,6 +206,7 @@ div.home div.why div.why-boxes div.why-box div.why-icon-wrapper {
div.home div.why div.why-boxes div.why-box div.why-icon-wrapper .why-icon {
font-size: 48px;
+ /* color: #003B95; */
}
div.home div.why div.why-boxes div.why-box div.why-text-wrapper {
@@ -163,6 +217,7 @@ div.home div.why div.why-boxes div.why-box div.why-text-wrapper {
div.home div.why div.why-boxes div.why-box div.why-text-wrapper span.why-title {
font-weight: bold;
margin-bottom: 5px;
+ /* color: #003B95; */
}
div.home div.services {
@@ -214,6 +269,12 @@ div.home div.services div.services-boxes div.services-box div.services-text-wrap
margin-bottom: 15px;
}
+div.home div.home-suppliers-static {
+ width: 100%;
+ max-width: 840px;
+ margin: 20px 0;
+}
+
div.home div.home-suppliers {
width: 80%;
margin: 20px 0;
@@ -319,9 +380,55 @@ div.home div.car-size div.boxes div.box div.car-size-action .btn-car-size {
width: 100%;
}
-div.home div.home-map {
- width: 80%;
- margin: 15px 0 30px;
+div.home div.blog {
+ padding: 50px 0 100px 0;
+}
+
+div.home div.blog h1 {
+ font-weight: 500;
+ padding: 0 10px;
+ text-align: center;
+}
+
+div.home div.blog div.blog-posts {
+ display: grid;
+ grid-template-columns: auto auto auto;
+}
+
+div.home div.blog div.blog-posts div.blog-post {
+ display: flex;
+ flex-direction: column;
+ width: 326px;
+ margin: 10px 30px 10px 0;
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+div.home div.blog div.blog-posts div.blog-post img {
+ width: 100%;
+ height: 200px;
+ object-fit: cover;
+}
+
+div.home div.blog div.blog-posts div.blog-post h1 {
+ min-height: 108px;
+ font-size: 24px;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ word-break: break-word;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+div.home div.blog div.blog-posts div.blog-post p {
+ min-height: 144px;
+ display: -webkit-box;
+ -webkit-line-clamp: 6;
+ -webkit-box-orient: vertical;
+ word-break: break-word;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
div.home div.faq {
@@ -332,6 +439,11 @@ div.home div.faq {
align-items: center;
}
+div.home div.home-map {
+ width: 80%;
+ margin: 15px 0 30px;
+}
+
div.home div.customer-care {
background-color: #f9f9f9;
width: 100%;
@@ -371,6 +483,8 @@ div.home div.customer-care div.customer-care-text div.customer-care-boxes div.cu
}
div.home div.customer-care div.customer-care-text div.customer-care-boxes .customer-care-icon {
+ /* color: #006CE4; */
+ color: #003B95;
margin-right: 10px;
}
@@ -401,6 +515,10 @@ div.home div.customer-care div.customer-care-img img {
margin-bottom: 20px;
}
+ div.home-subtitle {
+ font-size: 22px;
+ }
+
div.home-cover {
font-size: 40px;
font-weight: 800;
@@ -408,9 +526,12 @@ div.home div.customer-care div.customer-care-img img {
}
div.home div.search div.home-search {
- width: calc(100% - 40px) !important;
- padding: 20px;
- margin-top: -187px;
+ padding: 20px 10px;
+ margin: -187px 20px 0 20px;
+ }
+
+ div.home div.home-suppliers-static {
+ width: calc(100% - 40px)
}
div.home div.why div.why-boxes,
@@ -428,6 +549,15 @@ div.home div.customer-care div.customer-care-img img {
margin-bottom: 30px;
}
+ div.home div.blog div.blog-posts {
+ display: flex;
+ flex-direction: column;
+ }
+
+ div.home div.blog div.blog-posts div.blog-post {
+ margin-bottom: 40px;
+ }
+
div.home div.home-map {
width: 100%;
padding: 5px;
@@ -461,13 +591,13 @@ div.home div.customer-care div.customer-care-img img {
}
div.home div.home-cover {
- font-size: 47px;
- font-weight: 700;
+ font-size: 68px;
+ font-weight: 800;
margin-bottom: 5px;
}
div.home div.search div.home-search {
- padding: 20px;
+ padding: 20px 10px;
margin-top: -75px;
}
diff --git a/frontend/src/assets/css/search.css b/frontend/src/assets/css/search.css
index 9f5e8b277..58ed87e4d 100644
--- a/frontend/src/assets/css/search.css
+++ b/frontend/src/assets/css/search.css
@@ -23,12 +23,10 @@ div.search div.col-1 .map .view-on-map {
top: 10px;
right: 10px;
z-index: 10000;
-
background-color: #062638;
color: #fff;
border: 0;
padding: 5px 10px;
-
display: flex;
flex-direction: row;
border-radius: 5px;
@@ -64,6 +62,11 @@ div.search div.col-1 .filter {
justify-content: center;
}
+div.search .btn-filters {
+ max-width: 480px;
+ margin-top: 5px;
+}
+
/* Device width is less than or equal to 960px */
@media only screen and (width <=960px) {
diff --git a/frontend/src/common/helper.ts b/frontend/src/common/helper.ts
index 2bfec7c79..c4fb18ed7 100644
--- a/frontend/src/common/helper.ts
+++ b/frontend/src/common/helper.ts
@@ -5,6 +5,7 @@ import { strings } from '@/lang/cars'
import { strings as commonStrings } from '@/lang/common'
import env from '@/config/env.config'
import * as StripeService from '@/services/StripeService'
+import * as UserService from '@/services/UserService'
/**
* Get language.
@@ -638,3 +639,22 @@ export const downloadURI = (uri: string, name: string = '') => {
link.click()
link.remove()
}
+
+/**
+ * Verify reCAPTCHA token.
+ *
+ * @async
+ * @param {string} token
+ * @returns {Promise}
+ */
+export const verifyReCaptcha = async (token: string): Promise => {
+ try {
+ const ip = await UserService.getIP()
+ const status = await UserService.verifyRecaptcha(token, ip)
+ const valid = status === 200
+ return valid
+ } catch (err) {
+ error(err)
+ return false
+ }
+}
diff --git a/frontend/src/components/CheckoutStatus.tsx b/frontend/src/components/CheckoutStatus.tsx
index d94953756..24f2d3498 100644
--- a/frontend/src/components/CheckoutStatus.tsx
+++ b/frontend/src/components/CheckoutStatus.tsx
@@ -63,7 +63,9 @@ const CheckoutStatus = (
diff --git a/frontend/src/components/ContactForm.tsx b/frontend/src/components/ContactForm.tsx
index cf1b8d03e..ae25953e9 100644
--- a/frontend/src/components/ContactForm.tsx
+++ b/frontend/src/components/ContactForm.tsx
@@ -1,5 +1,4 @@
-import React, { useCallback, useEffect, useState } from 'react'
-import { GoogleReCaptcha } from 'react-google-recaptcha-v3'
+import React, { useEffect, useState } from 'react'
import {
OutlinedInput,
InputLabel,
@@ -16,6 +15,8 @@ import env from '@/config/env.config'
import { strings as commonStrings } from '@/lang/common'
import { strings } from '@/lang/contact-form'
import * as UserService from '@/services/UserService'
+import useReCaptcha from '@/hooks/useRecaptcha'
+
import * as helper from '@/common/helper'
import '@/assets/css/contact-form.css'
@@ -27,14 +28,13 @@ interface ContactFormProps {
const ContactForm = ({ user, className }: ContactFormProps) => {
const navigate = useNavigate()
+ const { reCaptchaLoaded, generateReCaptchaToken } = useReCaptcha()
const [email, setEmail] = useState('')
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [emailValid, setEmailValid] = useState(true)
const [subject, setSubject] = useState('')
const [message, setMessage] = useState('')
- const [recaptchaToken, setRecaptchaToken] = useState('')
- const [refreshReCaptcha, setRefreshReCaptcha] = useState(false)
const [sending, setSending] = useState(false)
useEffect(() => {
@@ -74,29 +74,33 @@ const ContactForm = ({ user, className }: ContactFormProps) => {
setMessage(e.target.value)
}
- const handleRecaptchaVerify = useCallback(async (token: string) => {
- setRecaptchaToken(token)
- }, [])
-
const handleSubmit = async (e: React.FormEvent) => {
try {
- setSending(true)
e.preventDefault()
+ setSending(true)
const _emailValid = await validateEmail(email)
if (!_emailValid) {
return
}
- const ip = await UserService.getIP()
+ let recaptchaToken = ''
+ if (reCaptchaLoaded) {
+ recaptchaToken = await generateReCaptchaToken()
+ if (!(await helper.verifyReCaptcha(recaptchaToken))) {
+ recaptchaToken = ''
+ }
+
+ if (!recaptchaToken) {
+ helper.error('reCAPTCHA error')
+ }
+ }
const payload: bookcarsTypes.SendEmailPayload = {
from: email,
to: env.CONTACT_EMAIL,
subject,
message,
- recaptchaToken,
- ip,
isContactForm: true,
}
const status = await UserService.sendEmail(payload)
@@ -115,7 +119,6 @@ const ContactForm = ({ user, className }: ContactFormProps) => {
helper.error(err)
} finally {
setSending(false)
- setRefreshReCaptcha((refresh) => !refresh)
}
}
@@ -170,15 +173,8 @@ const ContactForm = ({ user, className }: ContactFormProps) => {
/>
-
-
-
-
-