From b60f4a1c0e2a582dc263ed441e44628100ba1a33 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 28 Mar 2026 05:06:12 +0000 Subject: [PATCH] Alemdesta design1 --- backend/src/index.js | 3 +- backend/src/routes/contactForm.js | 108 +++++ frontend/src/components/NavBarItem.tsx | 3 +- .../src/components/marketing/InquiryForm.tsx | 230 +++++++++++ .../components/marketing/MarketingButton.tsx | 48 +++ .../components/marketing/MarketingLayout.tsx | 348 ++++++++++++++++ .../marketing/MarketingPageHero.tsx | 26 ++ .../components/marketing/MarketingSection.tsx | 32 ++ .../src/components/marketing/MarketingSeo.tsx | 25 ++ .../components/marketing/NewsletterForm.tsx | 119 ++++++ .../src/components/marketing/marketingData.ts | 276 +++++++++++++ frontend/src/css/main.css | 59 +++ frontend/src/layouts/Authenticated.tsx | 3 +- frontend/src/pages/about.tsx | 58 +++ frontend/src/pages/coffee-origins.tsx | 52 +++ frontend/src/pages/coffee-philosophy.tsx | 55 +++ frontend/src/pages/contact.tsx | 102 +++++ frontend/src/pages/export-services.tsx | 119 ++++++ frontend/src/pages/index.tsx | 390 +++++++++++------- frontend/src/pages/logistics-export.tsx | 109 +++++ frontend/src/pages/quality-control.tsx | 79 ++++ frontend/src/pages/sustainability.tsx | 47 +++ frontend/src/pages/vision-mission-values.tsx | 72 ++++ 23 files changed, 2207 insertions(+), 156 deletions(-) create mode 100644 frontend/src/components/marketing/InquiryForm.tsx create mode 100644 frontend/src/components/marketing/MarketingButton.tsx create mode 100644 frontend/src/components/marketing/MarketingLayout.tsx create mode 100644 frontend/src/components/marketing/MarketingPageHero.tsx create mode 100644 frontend/src/components/marketing/MarketingSection.tsx create mode 100644 frontend/src/components/marketing/MarketingSeo.tsx create mode 100644 frontend/src/components/marketing/NewsletterForm.tsx create mode 100644 frontend/src/components/marketing/marketingData.ts create mode 100644 frontend/src/pages/about.tsx create mode 100644 frontend/src/pages/coffee-origins.tsx create mode 100644 frontend/src/pages/coffee-philosophy.tsx create mode 100644 frontend/src/pages/contact.tsx create mode 100644 frontend/src/pages/export-services.tsx create mode 100644 frontend/src/pages/logistics-export.tsx create mode 100644 frontend/src/pages/quality-control.tsx create mode 100644 frontend/src/pages/sustainability.tsx create mode 100644 frontend/src/pages/vision-mission-values.tsx diff --git a/backend/src/index.js b/backend/src/index.js index b02317b..f811808 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -6,7 +6,6 @@ const passport = require('passport'); const path = require('path'); const fs = require('fs'); const bodyParser = require('body-parser'); -const db = require('./db/models'); const config = require('./config'); const swaggerUI = require('swagger-ui-express'); const swaggerJsDoc = require('swagger-jsdoc'); @@ -16,6 +15,7 @@ const fileRoutes = require('./routes/file'); const searchRoutes = require('./routes/search'); const sqlRoutes = require('./routes/sql'); const pexelsRoutes = require('./routes/pexels'); +const contactFormRoutes = require('./routes/contactForm'); const openaiRoutes = require('./routes/openai'); @@ -116,6 +116,7 @@ app.use(bodyParser.json()); app.use('/api/auth', authRoutes); app.use('/api/file', fileRoutes); app.use('/api/pexels', pexelsRoutes); +app.use('/api/contact-form', contactFormRoutes); app.enable('trust proxy'); diff --git a/backend/src/routes/contactForm.js b/backend/src/routes/contactForm.js index e69de29..7b00e20 100644 --- a/backend/src/routes/contactForm.js +++ b/backend/src/routes/contactForm.js @@ -0,0 +1,108 @@ +const express = require('express'); + +const InquiriesService = require('../services/inquiries'); +const NewsletterSubscribersService = require('../services/newsletter_subscribers'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const inquiryTypes = new Set(['contact_message', 'request_samples', 'get_a_quote', 'partner_with_us']); +const buyerTypes = new Set(['roaster', 'importer', 'distributor', 'specialty_buyer', 'wholesaler', 'other']); +const incoterms = new Set(['fob', 'cif', 'cfr', 'exw', 'dap', 'other']); +const contactMethods = new Set(['email', 'phone', 'whatsapp']); +const newsletterSources = new Set(['home_footer', 'home_cta', 'contact_page', 'popup', 'other']); + +const isValidEmail = (value) => /\S+@\S+\.\S+/.test(value || ''); + +const badRequest = (message) => { + const error = new Error(message); + error.code = 400; + return error; +}; + +router.post( + '/inquiry', + wrapAsync(async (req, res) => { + const data = req.body && req.body.data ? req.body.data : {}; + + if (!data.full_name || !data.email || !data.company_name || !data.country || !data.message) { + throw badRequest('Full name, email, company name, country, and message are required.'); + } + + if (!isValidEmail(data.email)) { + throw badRequest('A valid email address is required.'); + } + + if (data.inquiry_type && !inquiryTypes.has(data.inquiry_type)) { + throw badRequest('Invalid inquiry type.'); + } + + if (data.buyer_type && !buyerTypes.has(data.buyer_type)) { + throw badRequest('Invalid buyer type.'); + } + + if (data.incoterm && !incoterms.has(data.incoterm)) { + throw badRequest('Invalid incoterm.'); + } + + if (data.preferred_contact_method && !contactMethods.has(data.preferred_contact_method)) { + throw badRequest('Invalid preferred contact method.'); + } + + if (data.target_volume_kg !== undefined && data.target_volume_kg !== null && Number.isNaN(Number(data.target_volume_kg))) { + throw badRequest('Target volume must be a number.'); + } + + await InquiriesService.create({ + inquiry_type: data.inquiry_type || 'contact_message', + status: 'new', + full_name: data.full_name, + email: data.email, + phone: data.phone || null, + company_name: data.company_name, + country: data.country, + buyer_type: data.buyer_type || 'other', + message: data.message, + target_volume_kg: + data.target_volume_kg !== undefined && data.target_volume_kg !== null && data.target_volume_kg !== '' + ? Number(data.target_volume_kg) + : null, + incoterm: data.incoterm || 'fob', + preferred_contact_method: data.preferred_contact_method || 'email', + submitted_at: new Date(), + }); + + res.status(200).send({ success: true }); + }), +); + +router.post( + '/newsletter', + wrapAsync(async (req, res) => { + const data = req.body && req.body.data ? req.body.data : {}; + + if (!data.email || !isValidEmail(data.email)) { + throw badRequest('A valid email address is required.'); + } + + if (data.source && !newsletterSources.has(data.source)) { + throw badRequest('Invalid newsletter source.'); + } + + await NewsletterSubscribersService.create({ + email: data.email, + full_name: data.full_name || null, + company_name: data.company_name || null, + country: data.country || null, + source: data.source || 'other', + is_confirmed: false, + subscribed_at: new Date(), + }); + + res.status(200).send({ success: true }); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index 55559d2..93ddba0 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -1,6 +1,5 @@ -import React, {useEffect, useRef} from 'react' +import React, { useEffect, useRef, useState } from 'react' import Link from 'next/link' -import { useState } from 'react' import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import BaseDivider from './BaseDivider' import BaseIcon from './BaseIcon' diff --git a/frontend/src/components/marketing/InquiryForm.tsx b/frontend/src/components/marketing/InquiryForm.tsx new file mode 100644 index 0000000..a7724b6 --- /dev/null +++ b/frontend/src/components/marketing/InquiryForm.tsx @@ -0,0 +1,230 @@ +import axios from 'axios'; +import React, { useEffect, useMemo, useState } from 'react'; + +import MarketingButton from './MarketingButton'; +import { buyerTypes, contactMethods, incoterms, inquiryTypes } from './marketingData'; + +type InquiryType = 'request_samples' | 'get_a_quote' | 'partner_with_us' | 'contact_message'; +type BuyerType = 'roaster' | 'importer' | 'distributor' | 'specialty_buyer' | 'wholesaler' | 'other'; +type ContactMethod = 'email' | 'phone' | 'whatsapp'; +type Incoterm = 'fob' | 'cif' | 'cfr' | 'exw' | 'dap' | 'other'; + +type Props = { + initialInquiryType?: string; +}; + +type InquiryState = { + inquiry_type: InquiryType; + full_name: string; + email: string; + phone: string; + company_name: string; + country: string; + buyer_type: BuyerType; + message: string; + target_volume_kg: string; + incoterm: Incoterm; + preferred_contact_method: ContactMethod; +}; + +const defaultState: InquiryState = { + inquiry_type: 'request_samples', + full_name: '', + email: '', + phone: '', + company_name: '', + country: '', + buyer_type: 'importer', + message: '', + target_volume_kg: '', + incoterm: 'fob', + preferred_contact_method: 'email', +}; + +const allowedInquiryTypes = new Set(['request_samples', 'get_a_quote', 'partner_with_us', 'contact_message']); + +export default function InquiryForm({ initialInquiryType }: Props) { + const [form, setForm] = useState(defaultState); + const [errorMessage, setErrorMessage] = useState(''); + const [successMessage, setSuccessMessage] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + if (initialInquiryType && allowedInquiryTypes.has(initialInquiryType)) { + setForm((current) => ({ + ...current, + inquiry_type: initialInquiryType as InquiryType, + })); + } + }, [initialInquiryType]); + + const title = useMemo(() => { + const currentType = inquiryTypes.find((item) => item.value === form.inquiry_type); + return currentType ? currentType.label : 'Inquiry'; + }, [form.inquiry_type]); + + const onChange = ( + event: React.ChangeEvent, + ) => { + setForm((current) => ({ + ...current, + [event.target.name]: event.target.value, + })); + }; + + const onSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setErrorMessage(''); + setSuccessMessage(''); + + if (!form.full_name.trim() || !form.email.trim() || !form.company_name.trim() || !form.country.trim() || !form.message.trim()) { + setErrorMessage('Please complete name, email, company, country, and message.'); + return; + } + + setIsSubmitting(true); + + try { + await axios.post('/contact-form/inquiry', { + data: { + ...form, + target_volume_kg: form.target_volume_kg ? Number(form.target_volume_kg) : null, + }, + }); + setSuccessMessage(`Thank you. Your ${title.toLowerCase()} request has been received and our team will respond shortly.`); + setForm((current) => ({ + ...defaultState, + inquiry_type: current.inquiry_type, + })); + } catch (error) { + console.error('Inquiry submission failed', error); + setErrorMessage('Your request could not be submitted right now. Please try again or email info@alemdesta.com.'); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+
+

Buyer inquiry

+

{title}

+

+ Share your sourcing goals and we will tailor a response around origins, order size, process, and export handling. +

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +