Skip to content

Commit

Permalink
Add newsletter subscription form
Browse files Browse the repository at this point in the history
  • Loading branch information
aelassas committed Dec 7, 2024
1 parent 2d64161 commit 0b97d5f
Show file tree
Hide file tree
Showing 12 changed files with 853 additions and 669 deletions.
8 changes: 4 additions & 4 deletions api/src/controllers/userController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1540,7 +1540,7 @@ 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 } = body
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

Expand All @@ -1551,12 +1551,12 @@ export const sendEmail = async (req: Request, res: Response) => {
const mailOptions: nodemailer.SendMailOptions = {
from: env.SMTP_FROM,
to,
subject: i18n.t('CONTACT_SUBJECT'),
subject: isContactForm ? i18n.t('CONTACT_SUBJECT') : subject,
html:
`<p>
${i18n.t('FROM')}: ${from}<br>
${i18n.t('SUBJECT')}: ${subject}<br>
${i18n.t('MESSAGE')}:<br>${message.replace(/(?:\r\n|\r|\n)/g, '<br>')}<br>
${(isContactForm && `${i18n.t('SUBJECT')}: ${subject}<br>`) || ''}
${(message && `${i18n.t('MESSAGE')}:<br>${message.replace(/(?:\r\n|\r|\n)/g, '<br>')}<br>`) || ''}
</p>`,
}
await mailHelper.sendMail(mailOptions)
Expand Down
63 changes: 33 additions & 30 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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'

if (env.GOOGLE_ANALYTICS_ENABLED) {
initGA()
Expand Down Expand Up @@ -34,37 +35,39 @@ const Faq = lazy(() => import('@/pages/Faq'))

const App = () => (
<GlobalProvider>
<SuspenseRouter window={window}>
<div className="app">
<Suspense fallback={<></>}>
<Routes>
<Route path="/sign-in" element={<SignIn />} />
<Route path="/sign-up" element={<SignUp />} />
<Route path="/activate" element={<Activate />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/reset-password" element={<ResetPassword />} />
<Route path="/" element={<Home />} />
<Route path="/search" element={<Search />} />
<Route path="/checkout" element={<Checkout />} />
<Route path="/checkout-session/:sessionId" element={<CheckoutSession />} />
<Route path="/bookings" element={<Bookings />} />
<Route path="/booking" element={<Booking />} />
<Route path="/settings" element={<Settings />} />
<Route path="/notifications" element={<Notifications />} />
{/* <Route path="/change-password" element={<ChangePassword />} /> */}
<Route path="/about" element={<About />} />
<Route path="/privacy" element={<Privacy />} />
<Route path="/tos" element={<ToS />} />
<Route path="/contact" element={<Contact />} />
<Route path="/locations" element={<Locations />} />
<Route path="/suppliers" element={<Suppliers />} />
<Route path="/faq" element={<Faq />} />
<ReCaptchaProvider>
<SuspenseRouter window={window}>
<div className="app">
<Suspense fallback={<></>}>
<Routes>
<Route path="/sign-in" element={<SignIn />} />
<Route path="/sign-up" element={<SignUp />} />
<Route path="/activate" element={<Activate />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/reset-password" element={<ResetPassword />} />
<Route path="/" element={<Home />} />
<Route path="/search" element={<Search />} />
<Route path="/checkout" element={<Checkout />} />
<Route path="/checkout-session/:sessionId" element={<CheckoutSession />} />
<Route path="/bookings" element={<Bookings />} />
<Route path="/booking" element={<Booking />} />
<Route path="/settings" element={<Settings />} />
<Route path="/notifications" element={<Notifications />} />
{/* <Route path="/change-password" element={<ChangePassword />} /> */}
<Route path="/about" element={<About />} />
<Route path="/privacy" element={<Privacy />} />
<Route path="/tos" element={<ToS />} />
<Route path="/contact" element={<Contact />} />
<Route path="/locations" element={<Locations />} />
<Route path="/suppliers" element={<Suppliers />} />
<Route path="/faq" element={<Faq />} />

<Route path="*" element={<NoMatch />} />
</Routes>
</Suspense>
</div>
</SuspenseRouter>
<Route path="*" element={<NoMatch />} />
</Routes>
</Suspense>
</div>
</SuspenseRouter>
</ReCaptchaProvider>
</GlobalProvider>
)

Expand Down
4 changes: 4 additions & 0 deletions frontend/src/assets/css/common.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
--toastify-icon-color-info: #fff !important;
}

.grecaptcha-badge {
visibility: hidden !important;
}

.buttons button {
margin-right: 15px;
margin-left: 0 !important;
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/assets/css/footer.css
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ div.footer div.contact .social-icon {
margin-right: 12px;
}

div.footer div.newsletter {
margin-top: 80px;
}

div.footer section.payment {
width: 100%;
display: flex;
Expand All @@ -99,6 +103,14 @@ div.footer section.payment div.payment-text {
}

@media only screen and (width <=960px) {
div.footer section.main {
flex-direction: column;
}

div.footer div.main-section {
margin: 10px;
}

div.footer section.payment {
display: flex;
flex-direction: column;
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/assets/css/newsletter-form.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
form.newsletter-form div.form {
display: flex;
flex-direction: row;
align-items: center;
}

form.newsletter-form div.form .input {
margin-right: 15px;
}

form.newsletter-form div.form .btn {
height: 40px;
padding: 0 30px;
min-width: 120px;
}
144 changes: 71 additions & 73 deletions frontend/src/components/ContactForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ 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 ReCaptchaProvider from '@/components/ReCaptchaProvider'
import * as helper from '@/common/helper'

import '@/assets/css/contact-form.css'
Expand Down Expand Up @@ -97,7 +96,8 @@ const ContactForm = ({ user, className }: ContactFormProps) => {
subject,
message,
recaptchaToken,
ip
ip,
isContactForm: true,
}
const status = await UserService.sendEmail(payload)

Expand All @@ -124,83 +124,81 @@ const ContactForm = ({ user, className }: ContactFormProps) => {
}

return (
<ReCaptchaProvider>
<Paper className={`${className ? `${className} ` : ''}contact-form`} elevation={10}>
<h1 className="contact-form-title">
{' '}
{strings.CONTACT_HEADING}
{' '}
</h1>
<form onSubmit={handleSubmit}>
{!isAuthenticated && (
<FormControl fullWidth margin="dense">
<InputLabel className="required">{commonStrings.EMAIL}</InputLabel>
<OutlinedInput
type="text"
label={commonStrings.EMAIL}
error={!emailValid}
value={email}
onBlur={handleEmailBlur}
onChange={handleEmailChange}
required
autoComplete="off"
/>
<FormHelperText error={!emailValid}>
{(!emailValid && commonStrings.EMAIL_NOT_VALID) || ''}
</FormHelperText>
</FormControl>
)}

<FormControl fullWidth margin="dense">
<InputLabel className="required">{strings.SUBJECT}</InputLabel>
<OutlinedInput type="text" label={strings.SUBJECT} value={subject} required onChange={handleSubjectChange} autoComplete="off" />
</FormControl>

<Paper className={`${className ? `${className} ` : ''}contact-form`} elevation={10}>
<h1 className="contact-form-title">
{' '}
{strings.CONTACT_HEADING}
{' '}
</h1>
<form onSubmit={handleSubmit}>
{!isAuthenticated && (
<FormControl fullWidth margin="dense">
<InputLabel className="required">{strings.MESSAGE}</InputLabel>
<InputLabel className="required">{commonStrings.EMAIL}</InputLabel>
<OutlinedInput
type="text"
label={strings.MESSAGE}
onChange={handleMessageChange}
autoComplete="off"
value={message}
label={commonStrings.EMAIL}
error={!emailValid}
value={email}
onBlur={handleEmailBlur}
onChange={handleEmailChange}
required
multiline
minRows={7}
maxRows={7}
autoComplete="off"
/>
<FormHelperText error={!emailValid}>
{(!emailValid && commonStrings.EMAIL_NOT_VALID) || ''}
</FormHelperText>
</FormControl>

<div className="recaptcha">
<GoogleReCaptcha
refreshReCaptcha={refreshReCaptcha}
onVerify={handleRecaptchaVerify}
/>
</div>

<div className="buttons">
<Button type="submit" variant="contained" className="btn-primary btn-margin-bottom btn" size="small" disabled={sending}>
{
sending
? <CircularProgress color="inherit" size={24} />
: strings.SEND
}
</Button>
<Button
variant="contained"
className="btn-secondary btn-margin-bottom btn"
size="small"
onClick={() => {
navigate('/')
}}
>
{' '}
{commonStrings.CANCEL}
</Button>
</div>
</form>
</Paper>
</ReCaptchaProvider>
)}

<FormControl fullWidth margin="dense">
<InputLabel className="required">{strings.SUBJECT}</InputLabel>
<OutlinedInput type="text" label={strings.SUBJECT} value={subject} required onChange={handleSubjectChange} autoComplete="off" />
</FormControl>

<FormControl fullWidth margin="dense">
<InputLabel className="required">{strings.MESSAGE}</InputLabel>
<OutlinedInput
type="text"
label={strings.MESSAGE}
onChange={handleMessageChange}
autoComplete="off"
value={message}
required
multiline
minRows={7}
maxRows={7}
/>
</FormControl>

<div className="recaptcha">
<GoogleReCaptcha
refreshReCaptcha={refreshReCaptcha}
onVerify={handleRecaptchaVerify}
/>
</div>

<div className="buttons">
<Button type="submit" variant="contained" className="btn-primary btn-margin-bottom btn" size="small" disabled={sending}>
{
sending
? <CircularProgress color="inherit" size={24} />
: strings.SEND
}
</Button>
<Button
variant="contained"
className="btn-secondary btn-margin-bottom btn"
size="small"
onClick={() => {
navigate('/')
}}
>
{' '}
{commonStrings.CANCEL}
</Button>
</div>
</form>
</Paper>
)
}

Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Instagram,
} from '@mui/icons-material'
import { strings } from '@/lang/footer'
import NewsletterForm from '@/components/NewsletterForm'

import SecurePayment from '@/assets/img/secure-payment.png'
import '@/assets/css/footer.css'
Expand Down Expand Up @@ -51,6 +52,9 @@ const Footer = () => {
<Link href="https://www.linkedin.com/" target="_blank"><LinkedIn className="social-icon" /></Link>
<Link href="https://www.instagram.com/" target="_blank"><Instagram className="social-icon" /></Link>
</div>
<div className="newsletter">
<NewsletterForm />
</div>
</div>
</section>
<section className="payment">
Expand Down
Loading

0 comments on commit 0b97d5f

Please sign in to comment.