Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de96e0cfc2 | ||
|
|
b60f4a1c0e |
BIN
assets/pasted-20260328-052034-76e61f64.png
Normal file
BIN
assets/pasted-20260328-052034-76e61f64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 960 KiB |
BIN
assets/pasted-20260328-052613-0e0e2c33.png
Normal file
BIN
assets/pasted-20260328-052613-0e0e2c33.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
BIN
assets/pasted-20260328-080040-133ede7f.jpg
Normal file
BIN
assets/pasted-20260328-080040-133ede7f.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/pasted-20260328-090924-08e1b476.png
Normal file
BIN
assets/pasted-20260328-090924-08e1b476.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
@ -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');
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
BIN
frontend/public/assets/vm-shot-2026-03-28T07-59-44-940Z.jpg
Normal file
BIN
frontend/public/assets/vm-shot-2026-03-28T07-59-44-940Z.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 664 KiB |
@ -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'
|
||||
|
||||
230
frontend/src/components/marketing/InquiryForm.tsx
Normal file
230
frontend/src/components/marketing/InquiryForm.tsx
Normal file
@ -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<InquiryState>(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<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>,
|
||||
) => {
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
[event.target.name]: event.target.value,
|
||||
}));
|
||||
};
|
||||
|
||||
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
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 (
|
||||
<div className="rounded-[2rem] border border-[#1F3A2D]/10 bg-white p-6 shadow-[0_24px_80px_rgba(20,38,29,0.08)] md:p-8">
|
||||
<div className="mb-6">
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.35em] text-[#9FB06F]">Buyer inquiry</p>
|
||||
<h3 className="font-brand-display mt-3 text-3xl text-[#1F3A2D]">{title}</h3>
|
||||
<p className="mt-3 max-w-2xl text-sm leading-7 text-[#556257]">
|
||||
Share your sourcing goals and we will tailor a response around origins, order size, process, and export handling.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="space-y-4" onSubmit={onSubmit}>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<label className="space-y-2 text-sm font-medium text-[#1F3A2D]">
|
||||
Inquiry type
|
||||
<select className="coffee-input" name="inquiry_type" value={form.inquiry_type} onChange={onChange}>
|
||||
{inquiryTypes.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="space-y-2 text-sm font-medium text-[#1F3A2D]">
|
||||
Buyer type
|
||||
<select className="coffee-input" name="buyer_type" value={form.buyer_type} onChange={onChange}>
|
||||
{buyerTypes.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<label className="space-y-2 text-sm font-medium text-[#1F3A2D]">
|
||||
Full name
|
||||
<input className="coffee-input" name="full_name" placeholder="Your name" value={form.full_name} onChange={onChange} />
|
||||
</label>
|
||||
<label className="space-y-2 text-sm font-medium text-[#1F3A2D]">
|
||||
Email
|
||||
<input className="coffee-input" name="email" type="email" placeholder="you@company.com" value={form.email} onChange={onChange} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<label className="space-y-2 text-sm font-medium text-[#1F3A2D]">
|
||||
Phone / WhatsApp
|
||||
<input className="coffee-input" name="phone" placeholder="Contact number" value={form.phone} onChange={onChange} />
|
||||
</label>
|
||||
<label className="space-y-2 text-sm font-medium text-[#1F3A2D]">
|
||||
Preferred contact method
|
||||
<select className="coffee-input" name="preferred_contact_method" value={form.preferred_contact_method} onChange={onChange}>
|
||||
{contactMethods.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<label className="space-y-2 text-sm font-medium text-[#1F3A2D]">
|
||||
Company name
|
||||
<input className="coffee-input" name="company_name" placeholder="Company" value={form.company_name} onChange={onChange} />
|
||||
</label>
|
||||
<label className="space-y-2 text-sm font-medium text-[#1F3A2D]">
|
||||
Country
|
||||
<input className="coffee-input" name="country" placeholder="Country" value={form.country} onChange={onChange} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<label className="space-y-2 text-sm font-medium text-[#1F3A2D]">
|
||||
Target volume (kg)
|
||||
<input
|
||||
className="coffee-input"
|
||||
min="0"
|
||||
name="target_volume_kg"
|
||||
placeholder="Optional"
|
||||
type="number"
|
||||
value={form.target_volume_kg}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-2 text-sm font-medium text-[#1F3A2D]">
|
||||
Preferred Incoterm
|
||||
<select className="coffee-input" name="incoterm" value={form.incoterm} onChange={onChange}>
|
||||
{incoterms.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="space-y-2 text-sm font-medium text-[#1F3A2D]">
|
||||
Message
|
||||
<textarea
|
||||
className="coffee-input min-h-[160px] resize-y pt-3"
|
||||
name="message"
|
||||
placeholder="Tell us the origins, process style, order timing, or partnership goals you are exploring."
|
||||
value={form.message}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<p className="text-sm text-[#667367]">Submissions are routed to the admin inquiry queue for follow-up.</p>
|
||||
<MarketingButton className="w-full md:w-auto" type="submit">
|
||||
{isSubmitting ? 'Sending request...' : 'Submit inquiry'}
|
||||
</MarketingButton>
|
||||
</div>
|
||||
|
||||
{errorMessage ? <p className="text-sm text-red-600">{errorMessage}</p> : null}
|
||||
{successMessage ? (
|
||||
<div className="rounded-2xl border border-[#9FB06F]/30 bg-[#EEF3E7] px-4 py-3 text-sm text-[#1F3A2D]">{successMessage}</div>
|
||||
) : null}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
frontend/src/components/marketing/MarketingButton.tsx
Normal file
48
frontend/src/components/marketing/MarketingButton.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
type?: 'button' | 'submit';
|
||||
variant?: 'primary' | 'secondary' | 'ghost';
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const baseClassName =
|
||||
'inline-flex items-center justify-center rounded-full px-5 py-3 text-sm font-semibold transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-[#9FB06F] focus:ring-offset-2';
|
||||
|
||||
const variantMap = {
|
||||
primary:
|
||||
'bg-[#9FB06F] text-[#18271F] shadow-[0_16px_40px_rgba(159,176,111,0.25)] hover:-translate-y-0.5 hover:bg-[#B0C482]',
|
||||
secondary:
|
||||
'border border-[#B9C88D]/55 bg-white/12 text-white backdrop-blur hover:border-[#C5D39A] hover:bg-white/24',
|
||||
ghost:
|
||||
'border border-[#1F3A2D]/15 bg-white text-[#1F3A2D] hover:border-[#1F3A2D]/30 hover:bg-[#F4F1E6]',
|
||||
};
|
||||
|
||||
export default function MarketingButton({
|
||||
href,
|
||||
onClick,
|
||||
type = 'button',
|
||||
variant = 'primary',
|
||||
className = '',
|
||||
children,
|
||||
}: Props) {
|
||||
const classes = `${baseClassName} ${variantMap[variant]} ${className}`;
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Link href={href} className={classes}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button className={classes} onClick={onClick} type={type}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
348
frontend/src/components/marketing/MarketingLayout.tsx
Normal file
348
frontend/src/components/marketing/MarketingLayout.tsx
Normal file
@ -0,0 +1,348 @@
|
||||
import { mdiArrowRight, mdiChevronDown, mdiClose, mdiMenu } from '@mdi/js';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import BaseIcon from '../BaseIcon';
|
||||
import MarketingButton from './MarketingButton';
|
||||
import NewsletterForm from './NewsletterForm';
|
||||
import { brand, exportServiceItems, navigationItems } from './marketingData';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
type NavigationItem = (typeof navigationItems)[number];
|
||||
type NavigationChild = (typeof exportServiceItems)[number];
|
||||
|
||||
const isActivePath = (pathname: string, href: string) => {
|
||||
if (href === '/') {
|
||||
return pathname === '/';
|
||||
}
|
||||
|
||||
return pathname === href;
|
||||
};
|
||||
|
||||
const isNavItemActive = (pathname: string, item: NavigationItem) => {
|
||||
if (isActivePath(pathname, item.href)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return item.children?.some((child) => isActivePath(pathname, child.href)) ?? false;
|
||||
};
|
||||
|
||||
const getCurrentLabel = (pathname: string) => {
|
||||
const currentChild = exportServiceItems.find((item) => item.href === pathname);
|
||||
|
||||
if (currentChild) {
|
||||
return currentChild.label;
|
||||
}
|
||||
|
||||
const currentItem = navigationItems.find((item) => item.href === pathname);
|
||||
return currentItem ? currentItem.label : 'Discover';
|
||||
};
|
||||
|
||||
const getDesktopNavClasses = (isActive: boolean, isHomeHeroMode: boolean) => {
|
||||
if (isHomeHeroMode) {
|
||||
return isActive
|
||||
? 'border border-white/15 bg-white/16 text-white shadow-[0_16px_45px_rgba(0,0,0,0.18)] backdrop-blur-md'
|
||||
: 'border border-white/10 bg-[#173125]/58 text-[#F4F1E6] shadow-[0_14px_35px_rgba(0,0,0,0.18)] backdrop-blur-md hover:bg-[#224032]/74 hover:text-white';
|
||||
}
|
||||
|
||||
return isActive ? 'bg-[#1F3A2D] text-[#F4F1E6]' : 'text-[#32453A] hover:bg-[#F4F1E6]';
|
||||
};
|
||||
|
||||
const getDesktopDropdownClasses = (isHomeHeroMode: boolean) => {
|
||||
return isHomeHeroMode
|
||||
? 'border-white/10 bg-[#14261D]/96 text-white shadow-[0_26px_80px_rgba(0,0,0,0.32)]'
|
||||
: 'border-[#1F3A2D]/10 bg-white text-[#22332B] shadow-[0_26px_80px_rgba(20,38,29,0.14)]';
|
||||
};
|
||||
|
||||
const getDesktopDropdownItemClasses = (pathname: string, item: NavigationChild, isHomeHeroMode: boolean) => {
|
||||
const isActive = isActivePath(pathname, item.href);
|
||||
|
||||
if (isHomeHeroMode) {
|
||||
return isActive
|
||||
? 'bg-white/12 text-white'
|
||||
: 'text-white/78 hover:bg-white/8 hover:text-white';
|
||||
}
|
||||
|
||||
return isActive ? 'bg-[#F4F1E6] text-[#1F3A2D]' : 'text-[#32453A] hover:bg-[#F4F1E6]/70';
|
||||
};
|
||||
|
||||
export default function MarketingLayout({ children }: Props) {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isExportMenuOpen, setIsExportMenuOpen] = useState(false);
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 24);
|
||||
};
|
||||
|
||||
handleScroll();
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMenuOpen(false);
|
||||
setIsExportMenuOpen(false);
|
||||
}, [router.pathname]);
|
||||
|
||||
const currentLabel = useMemo(() => getCurrentLabel(router.pathname), [router.pathname]);
|
||||
const isHomeHeroMode = router.pathname === '/' && !isScrolled && !isMenuOpen;
|
||||
|
||||
return (
|
||||
<div className="font-brand-body min-h-screen bg-[#FBF9F1] text-[#22332B]">
|
||||
<header
|
||||
className={`sticky top-0 z-50 transition-all duration-300 ${
|
||||
isHomeHeroMode
|
||||
? 'border-b border-white/10 bg-[#102118]/62 shadow-[0_18px_50px_rgba(0,0,0,0.18)] backdrop-blur-xl'
|
||||
: 'border-b border-[#1F3A2D]/10 bg-[#FBF9F1]/92 shadow-[0_18px_50px_rgba(20,38,29,0.08)] backdrop-blur-xl'
|
||||
}`}
|
||||
>
|
||||
<div className="mx-auto flex max-w-7xl items-center justify-between gap-4 px-5 py-4 lg:px-8">
|
||||
<Link href="/" className="flex items-center gap-3">
|
||||
<div
|
||||
className={`flex h-12 w-12 items-center justify-center rounded-full border text-sm font-semibold shadow-lg transition-colors duration-300 ${
|
||||
isHomeHeroMode
|
||||
? 'border-white/28 bg-white/12 text-[#F4F1E6] backdrop-blur-md'
|
||||
: 'border-[#9FB06F]/50 bg-[#1F3A2D] text-[#F4F1E6]'
|
||||
}`}
|
||||
>
|
||||
AD
|
||||
</div>
|
||||
<div>
|
||||
<p className={`font-brand-display text-lg font-bold transition-colors duration-300 ${isHomeHeroMode ? 'text-[#F4F1E6]' : 'text-[#1F3A2D]'}`}>
|
||||
Alem Desta
|
||||
</p>
|
||||
<p className={`text-xs uppercase tracking-[0.28em] transition-colors duration-300 ${isHomeHeroMode ? 'text-white/60' : 'text-[#72816F]'}`}>
|
||||
Coffee Export
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<nav className="hidden items-center gap-1 xl:flex">
|
||||
{navigationItems.map((item) => {
|
||||
const isActive = isNavItemActive(router.pathname, item);
|
||||
const classes = getDesktopNavClasses(isActive, isHomeHeroMode);
|
||||
|
||||
if (!item.children) {
|
||||
return (
|
||||
<Link key={item.href} href={item.href} className={`rounded-full px-4 py-2 text-sm font-medium transition-all duration-300 ${classes}`}>
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={item.href} className="group relative">
|
||||
<Link href={item.href} className={`inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium transition-all duration-300 ${classes}`}>
|
||||
{item.label}
|
||||
<BaseIcon path={mdiChevronDown} className="transition-transform duration-300 group-hover:rotate-180 group-focus-within:rotate-180" />
|
||||
</Link>
|
||||
<div
|
||||
className={`invisible absolute left-0 top-full mt-3 w-[22rem] translate-y-2 rounded-[1.75rem] border p-3 opacity-0 transition-all duration-300 group-hover:visible group-hover:translate-y-0 group-hover:opacity-100 group-hover:pointer-events-auto group-focus-within:visible group-focus-within:translate-y-0 group-focus-within:opacity-100 ${getDesktopDropdownClasses(
|
||||
isHomeHeroMode,
|
||||
)}`}
|
||||
>
|
||||
{item.children.map((child) => (
|
||||
<Link
|
||||
key={child.href}
|
||||
href={child.href}
|
||||
className={`block rounded-[1.25rem] px-4 py-3 transition-all duration-300 ${getDesktopDropdownItemClasses(
|
||||
router.pathname,
|
||||
child,
|
||||
isHomeHeroMode,
|
||||
)}`}
|
||||
>
|
||||
<p className="text-sm font-semibold">{child.label}</p>
|
||||
<p className={`mt-1 text-xs leading-6 ${isHomeHeroMode ? 'text-white/58' : 'text-[#68766A]'}`}>{child.description}</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="hidden items-center gap-3 xl:flex">
|
||||
<MarketingButton href="/login" variant={isHomeHeroMode ? 'secondary' : 'ghost'}>
|
||||
Admin Login
|
||||
</MarketingButton>
|
||||
<MarketingButton href="/contact?type=get_a_quote">Get a Quote</MarketingButton>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsMenuOpen((current) => !current)}
|
||||
className={`inline-flex h-12 w-12 items-center justify-center rounded-full border transition-all duration-300 xl:hidden ${
|
||||
isHomeHeroMode
|
||||
? 'border-white/20 bg-white/10 text-[#F4F1E6] backdrop-blur-md'
|
||||
: 'border-[#1F3A2D]/10 bg-white text-[#1F3A2D]'
|
||||
}`}
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<BaseIcon path={isMenuOpen ? mdiClose : mdiMenu} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isMenuOpen ? (
|
||||
<div className="border-t border-[#1F3A2D]/10 bg-[#FBF9F1] px-5 py-4 xl:hidden">
|
||||
<div className="mb-4 rounded-2xl border border-[#9FB06F]/30 bg-[#F4F1E6] px-4 py-3 text-sm text-[#5F6C61]">
|
||||
Currently viewing: <span className="font-semibold text-[#1F3A2D]">{currentLabel}</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{navigationItems.map((item) => {
|
||||
if (!item.children) {
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`rounded-2xl px-4 py-3 text-sm font-medium ${
|
||||
isNavItemActive(router.pathname, item) ? 'bg-[#1F3A2D] text-[#F4F1E6]' : 'bg-white text-[#1F3A2D]'
|
||||
}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={item.href} className="overflow-hidden rounded-[1.5rem] border border-[#1F3A2D]/10 bg-white">
|
||||
<div className="flex items-center">
|
||||
<Link
|
||||
href={item.href}
|
||||
className={`flex-1 px-4 py-3 text-sm font-medium ${
|
||||
isNavItemActive(router.pathname, item) ? 'text-[#1F3A2D]' : 'text-[#32453A]'
|
||||
}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsExportMenuOpen((current) => !current)}
|
||||
className="px-4 py-3 text-[#1F3A2D]"
|
||||
aria-label="Toggle export services submenu"
|
||||
>
|
||||
<BaseIcon
|
||||
path={mdiChevronDown}
|
||||
className={`transition-transform duration-300 ${isExportMenuOpen ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{isExportMenuOpen ? (
|
||||
<div className="border-t border-[#1F3A2D]/10 bg-[#FBF9F1] p-2">
|
||||
{item.children.map((child) => (
|
||||
<Link
|
||||
key={child.href}
|
||||
href={child.href}
|
||||
className={`block rounded-2xl px-4 py-3 ${
|
||||
isActivePath(router.pathname, child.href) ? 'bg-[#1F3A2D] text-[#F4F1E6]' : 'text-[#1F3A2D]'
|
||||
}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<p className="text-sm font-medium">{child.label}</p>
|
||||
<p className={`mt-1 text-xs leading-6 ${isActivePath(router.pathname, child.href) ? 'text-white/70' : 'text-[#68766A]'}`}>
|
||||
{child.description}
|
||||
</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
<MarketingButton href="/login" variant="ghost">
|
||||
Admin Login
|
||||
</MarketingButton>
|
||||
<MarketingButton href="/contact?type=request_samples">Request Samples</MarketingButton>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
<main>{children}</main>
|
||||
|
||||
<section className="border-y border-[#1F3A2D]/10 bg-[#1F3A2D] px-5 py-16 text-white lg:px-8">
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-10 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="max-w-3xl">
|
||||
<p className="text-sm uppercase tracking-[0.35em] text-[#B7C78B]">Ready to source with confidence?</p>
|
||||
<h2 className="font-brand-display mt-4 text-4xl leading-tight text-[#F4F1E6] md:text-5xl">
|
||||
Discover Ethiopian coffee backed by heritage, clear communication, and export discipline.
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<MarketingButton href="/contact?type=request_samples">Request Samples</MarketingButton>
|
||||
<MarketingButton href="/contact?type=get_a_quote" variant="secondary">
|
||||
Get a Quote
|
||||
</MarketingButton>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer className="bg-[#14261D] px-5 py-16 text-white lg:px-8">
|
||||
<div className="mx-auto grid max-w-7xl gap-10 lg:grid-cols-[1.1fr_0.7fr_0.95fr_1fr]">
|
||||
<div>
|
||||
<p className="font-brand-display text-3xl text-[#F4F1E6]">{brand.name}</p>
|
||||
<p className="mt-4 max-w-xl text-sm leading-7 text-white/72">{brand.tagline}</p>
|
||||
<div className="mt-8 flex flex-wrap gap-3">
|
||||
<MarketingButton href="/contact?type=partner_with_us">Partner With Us</MarketingButton>
|
||||
<MarketingButton href="/login" variant="secondary">
|
||||
Admin Interface
|
||||
</MarketingButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.28em] text-[#9FB06F]">Main pages</p>
|
||||
<div className="mt-5 grid gap-3">
|
||||
{navigationItems.map((item) => (
|
||||
<Link key={item.href} href={item.href} className="inline-flex items-center gap-2 text-sm text-white/75 transition hover:text-white">
|
||||
<BaseIcon path={mdiArrowRight} className="text-[#9FB06F]" />
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.28em] text-[#9FB06F]">Export services</p>
|
||||
<div className="mt-5 grid gap-3">
|
||||
{exportServiceItems.map((item) => (
|
||||
<Link key={item.href} href={item.href} className="inline-flex items-center gap-2 text-sm text-white/75 transition hover:text-white">
|
||||
<BaseIcon path={mdiArrowRight} className="text-[#9FB06F]" />
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8 space-y-2 text-sm text-white/75">
|
||||
<p>{brand.address}</p>
|
||||
<p>{brand.email}</p>
|
||||
{brand.phones.map((phone) => (
|
||||
<p key={phone}>{phone}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.28em] text-[#9FB06F]">Newsletter subscription</p>
|
||||
<p className="mt-4 text-sm leading-7 text-white/72">
|
||||
Receive market-ready updates, sourcing availability, and company news tailored for international buyers.
|
||||
</p>
|
||||
<div className="mt-5">
|
||||
<NewsletterForm compact source="home_footer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
frontend/src/components/marketing/MarketingPageHero.tsx
Normal file
27
frontend/src/components/marketing/MarketingPageHero.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
};
|
||||
|
||||
export default function MarketingPageHero({ eyebrow, title, description, image }: Props) {
|
||||
return (
|
||||
<section className="px-5 pb-8 pt-12 lg:px-8 lg:pb-12 lg:pt-16">
|
||||
<div className="mx-auto grid max-w-7xl gap-8 overflow-hidden rounded-[2rem] bg-[#F4F1E6] p-6 shadow-[0_28px_90px_rgba(31,58,45,0.08)] lg:grid-cols-[1.05fr_0.95fr] lg:p-10">
|
||||
<div className="flex flex-col justify-center">
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.35em] text-[#9FB06F]">{eyebrow}</p>
|
||||
<h1 className="font-brand-display mt-4 text-4xl leading-tight text-[#1F3A2D] md:text-6xl">{title}</h1>
|
||||
<p className="mt-6 max-w-2xl text-base leading-8 text-[#556257]">{description}</p>
|
||||
</div>
|
||||
<div className="relative min-h-[320px] overflow-hidden rounded-[1.75rem]">
|
||||
<img className="absolute inset-0 h-full w-full object-cover saturate-[0.88]" loading="eager" src={image} alt={title} />
|
||||
<div className="absolute inset-0 bg-[linear-gradient(160deg,rgba(16,33,24,0.08),rgba(36,63,48,0.22),rgba(16,33,24,0.42))]" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-[#102118]/54 via-transparent to-transparent" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
32
frontend/src/components/marketing/MarketingSection.tsx
Normal file
32
frontend/src/components/marketing/MarketingSection.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
eyebrow?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
centered?: boolean;
|
||||
};
|
||||
|
||||
export default function MarketingSection({
|
||||
eyebrow,
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
className = '',
|
||||
centered = false,
|
||||
}: Props) {
|
||||
return (
|
||||
<section className={`px-5 py-16 lg:px-8 lg:py-24 ${className}`}>
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className={centered ? 'mx-auto max-w-3xl text-center' : 'max-w-3xl'}>
|
||||
{eyebrow ? <p className="text-sm font-semibold uppercase tracking-[0.35em] text-[#9FB06F]">{eyebrow}</p> : null}
|
||||
<h2 className="font-brand-display mt-4 text-4xl leading-tight text-[#1F3A2D] md:text-5xl">{title}</h2>
|
||||
{description ? <p className="mt-5 text-base leading-8 text-[#556257]">{description}</p> : null}
|
||||
</div>
|
||||
{children ? <div className="mt-12">{children}</div> : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
25
frontend/src/components/marketing/MarketingSeo.tsx
Normal file
25
frontend/src/components/marketing/MarketingSeo.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import Head from 'next/head';
|
||||
import React from 'react';
|
||||
|
||||
import { brand } from './marketingData';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description: string;
|
||||
keywords?: string;
|
||||
};
|
||||
|
||||
export default function MarketingSeo({ title, description, keywords }: Props) {
|
||||
const fullTitle = `${title} | ${brand.name}`;
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<title>{fullTitle}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta
|
||||
name="keywords"
|
||||
content={keywords || 'Ethiopian coffee export, specialty coffee, green coffee beans, single origin coffee'}
|
||||
/>
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
119
frontend/src/components/marketing/NewsletterForm.tsx
Normal file
119
frontend/src/components/marketing/NewsletterForm.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import axios from 'axios';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import MarketingButton from './MarketingButton';
|
||||
|
||||
type Props = {
|
||||
source?: string;
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
type NewsletterState = {
|
||||
email: string;
|
||||
full_name: string;
|
||||
company_name: string;
|
||||
country: string;
|
||||
};
|
||||
|
||||
const initialState: NewsletterState = {
|
||||
email: '',
|
||||
full_name: '',
|
||||
company_name: '',
|
||||
country: '',
|
||||
};
|
||||
|
||||
export default function NewsletterForm({ source = 'home_footer', compact = false }: Props) {
|
||||
const [form, setForm] = useState<NewsletterState>(initialState);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [successMessage, setSuccessMessage] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
[event.target.name]: event.target.value,
|
||||
}));
|
||||
};
|
||||
|
||||
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setErrorMessage('');
|
||||
setSuccessMessage('');
|
||||
|
||||
if (!form.email.trim()) {
|
||||
setErrorMessage('Email is required.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
await axios.post('/contact-form/newsletter', {
|
||||
data: {
|
||||
...form,
|
||||
source,
|
||||
},
|
||||
});
|
||||
setSuccessMessage('Thank you. You have been added to our coffee export updates list.');
|
||||
setForm(initialState);
|
||||
} catch (error) {
|
||||
console.error('Newsletter subscription failed', error);
|
||||
setErrorMessage('Subscription could not be completed right now. Please try again shortly.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="space-y-3" onSubmit={onSubmit}>
|
||||
{!compact && (
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<input
|
||||
className="h-12 rounded-2xl border border-white/15 bg-white/10 px-4 text-sm text-white placeholder:text-white/55 focus:outline-none focus:ring-2 focus:ring-[#9FB06F]"
|
||||
name="full_name"
|
||||
placeholder="Name"
|
||||
value={form.full_name}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<input
|
||||
className="h-12 rounded-2xl border border-white/15 bg-white/10 px-4 text-sm text-white placeholder:text-white/55 focus:outline-none focus:ring-2 focus:ring-[#9FB06F]"
|
||||
name="company_name"
|
||||
placeholder="Company"
|
||||
value={form.company_name}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={`grid gap-3 ${compact ? 'md:grid-cols-[minmax(0,1fr)_auto]' : 'md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]'}`}>
|
||||
<input
|
||||
className="h-12 rounded-2xl border border-white/15 bg-white/10 px-4 text-sm text-white placeholder:text-white/55 focus:outline-none focus:ring-2 focus:ring-[#9FB06F]"
|
||||
name="email"
|
||||
placeholder="Email address"
|
||||
type="email"
|
||||
value={form.email}
|
||||
onChange={onChange}
|
||||
/>
|
||||
{compact ? (
|
||||
<MarketingButton className="w-full md:w-auto" type="submit">
|
||||
{isSubmitting ? 'Subscribing...' : 'Subscribe'}
|
||||
</MarketingButton>
|
||||
) : (
|
||||
<input
|
||||
className="h-12 rounded-2xl border border-white/15 bg-white/10 px-4 text-sm text-white placeholder:text-white/55 focus:outline-none focus:ring-2 focus:ring-[#9FB06F]"
|
||||
name="country"
|
||||
placeholder="Country"
|
||||
value={form.country}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!compact && (
|
||||
<MarketingButton className="w-full md:w-auto" type="submit">
|
||||
{isSubmitting ? 'Subscribing...' : 'Subscribe to updates'}
|
||||
</MarketingButton>
|
||||
)}
|
||||
{errorMessage ? <p className="text-sm text-[#f8caca]">{errorMessage}</p> : null}
|
||||
{successMessage ? <p className="text-sm text-[#d9c08e]">{successMessage}</p> : null}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
276
frontend/src/components/marketing/marketingData.ts
Normal file
276
frontend/src/components/marketing/marketingData.ts
Normal file
@ -0,0 +1,276 @@
|
||||
export const brand = {
|
||||
name: 'Alem Desta Coffee Export',
|
||||
tagline: 'Premium Ethiopian green coffee sourced with heritage, integrity, and export discipline.',
|
||||
email: 'info@alemdesta.com',
|
||||
phones: ['+251 930105914', '+251 904186868'],
|
||||
address: 'Kirkos Sub-city, Wereda 01, Addis Ababa, Ethiopia',
|
||||
};
|
||||
|
||||
export const exportServiceItems = [
|
||||
{
|
||||
href: '/coffee-origins',
|
||||
label: 'Coffee Origins',
|
||||
description: 'Single-origin coffees from iconic Ethiopian regions with distinct altitude, profile, and terroir.',
|
||||
},
|
||||
{
|
||||
href: '/quality-control',
|
||||
label: 'Quality Control',
|
||||
description: 'Disciplined cherry selection, moisture monitoring, and cupping protocols before export.',
|
||||
},
|
||||
{
|
||||
href: '/sustainability',
|
||||
label: 'Sustainability',
|
||||
description: 'Long-term sourcing built around farmer respect, environmental care, and transparent relationships.',
|
||||
},
|
||||
{
|
||||
href: '/logistics-export',
|
||||
label: 'Logistics & Export',
|
||||
description: 'Documentation, shipment coordination, and responsive communication from Addis Ababa to destination.',
|
||||
},
|
||||
];
|
||||
|
||||
export const navigationItems = [
|
||||
{ href: '/', label: 'Home' },
|
||||
{ href: '/about', label: 'About' },
|
||||
{ href: '/export-services', label: 'Export Services', children: exportServiceItems },
|
||||
{ href: '/contact', label: 'Contact' },
|
||||
];
|
||||
|
||||
export const featureHighlights = [
|
||||
{
|
||||
title: 'Traceability & Transparency',
|
||||
description:
|
||||
'Every shipment is sourced with documented origin details, responsible handling, and buyer-ready clarity from farm gate to export.',
|
||||
},
|
||||
{
|
||||
title: 'Premium Quality Control',
|
||||
description:
|
||||
'Careful cherry selection, moisture monitoring, cupping review, and export preparation protect cup quality at every stage.',
|
||||
},
|
||||
{
|
||||
title: 'Direct Farmer Partnerships',
|
||||
description:
|
||||
'We build relationships with skilled producers and washing stations to secure consistency, trust, and long-term value.',
|
||||
},
|
||||
{
|
||||
title: 'Sustainable Sourcing',
|
||||
description:
|
||||
'Commercial success is paired with responsible sourcing practices that respect communities, land, and future harvests.',
|
||||
},
|
||||
];
|
||||
|
||||
export const galleryItems = [
|
||||
{
|
||||
title: 'Green coffee landscapes',
|
||||
caption: 'Highland farms and careful cultivation create the foundation for exceptional green coffee lots.',
|
||||
image:
|
||||
'https://images.unsplash.com/photo-1447933601403-0c6688de566e?auto=format&fit=crop&w=1200&q=80',
|
||||
},
|
||||
{
|
||||
title: 'Washing station discipline',
|
||||
caption: 'Lot separation, drying control, and station handling protect specialty green coffee quality before export.',
|
||||
image:
|
||||
'https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?auto=format&fit=crop&w=1200&q=80',
|
||||
},
|
||||
{
|
||||
title: 'Lot evaluation',
|
||||
caption: 'Cupping, moisture review, and sample assessment help buyers select green coffee with confidence.',
|
||||
image:
|
||||
'https://images.unsplash.com/photo-1511920170033-f8396924c348?auto=format&fit=crop&w=1200&q=80',
|
||||
},
|
||||
{
|
||||
title: 'Export-ready preparation',
|
||||
caption: 'Packaging, documentation, and shipment coordination move export-ready green coffee toward global buyers.',
|
||||
image:
|
||||
'https://images.unsplash.com/photo-1520607162513-77705c0f0d4a?auto=format&fit=crop&w=1200&q=80',
|
||||
},
|
||||
];
|
||||
|
||||
export const founderStory = [
|
||||
'Alem Desta Coffee Export was shaped by a life rooted in Ethiopian coffee culture, where coffee is more than a crop; it is hospitality, identity, and daily ritual. Growing up with that tradition created a deep respect for the people, landscapes, and craftsmanship behind every cup.',
|
||||
'Family tradition made coffee familiar from an early age, but global experience expanded the vision. Time spent in the United States — including Chicago, San Francisco, and the DMV area — offered a firsthand understanding of how international buyers evaluate quality, consistency, communication, and professionalism.',
|
||||
'Returning to Ethiopia brought that perspective back to origin. It also created the opportunity to learn closely from a brother already active in coffee exporting, translating heritage into disciplined export practice, relationship building, and an appreciation for what buyers genuinely need from a trusted supplier.',
|
||||
'Today the business stands at the intersection of culture, global exposure, and commercial responsibility: honoring Ethiopian roots while delivering premium coffee with clear communication, traceability, and long-term partnership in mind.',
|
||||
];
|
||||
|
||||
export const philosophyPillars = [
|
||||
{
|
||||
title: 'Altitude',
|
||||
description: 'High-altitude growing environments support clarity, floral aromatics, and refined complexity.',
|
||||
},
|
||||
{
|
||||
title: 'Soil',
|
||||
description: 'Rich natural soils nourish coffee trees slowly and help create layered flavor expression.',
|
||||
},
|
||||
{
|
||||
title: 'Climate',
|
||||
description: 'Balanced rainfall, sunlight, and cool nights allow cherries to mature with patience.',
|
||||
},
|
||||
{
|
||||
title: 'Human Craftsmanship',
|
||||
description: 'Excellence depends on farmers, processors, cuppers, and exporters applying skill at every step.',
|
||||
},
|
||||
];
|
||||
|
||||
export const values = [
|
||||
{
|
||||
title: 'Integrity',
|
||||
description: 'We communicate honestly, document carefully, and build trust through dependable execution.',
|
||||
},
|
||||
{
|
||||
title: 'Culture & Heritage',
|
||||
description: 'Our work is grounded in Ethiopian coffee tradition and respect for origin.',
|
||||
},
|
||||
{
|
||||
title: 'Quality Excellence',
|
||||
description: 'We pursue careful selection, disciplined processing, and export readiness without compromise.',
|
||||
},
|
||||
{
|
||||
title: 'Partnership',
|
||||
description: 'We value long-term relationships with farmers, washing stations, and international buyers.',
|
||||
},
|
||||
{
|
||||
title: 'Responsibility',
|
||||
description: 'Commercial growth should strengthen communities and support ethical business practices.',
|
||||
},
|
||||
{
|
||||
title: 'Sustainability',
|
||||
description: 'Responsible sourcing protects both the environment and the future of coffee farming.',
|
||||
},
|
||||
];
|
||||
|
||||
export const principles = [
|
||||
'Direct sourcing',
|
||||
'Full traceability',
|
||||
'Compliance with international standards',
|
||||
'Long-term relationships',
|
||||
'Continuous improvement',
|
||||
];
|
||||
|
||||
export const origins = [
|
||||
{
|
||||
name: 'Yirgacheffe',
|
||||
profile: 'Floral, citrus, bright acidity',
|
||||
altitude: '1900–2200 MASL',
|
||||
rainfall: 'Balanced seasonal rainfall',
|
||||
image:
|
||||
'https://images.unsplash.com/photo-1461988320302-91bde64fc8e4?auto=format&fit=crop&w=900&q=80',
|
||||
},
|
||||
{
|
||||
name: 'Guji',
|
||||
profile: 'Fruity, vibrant sweetness',
|
||||
altitude: '1800–2200 MASL',
|
||||
rainfall: 'Reliable mountain rainfall',
|
||||
image:
|
||||
'https://images.unsplash.com/photo-1509042239860-f550ce710b93?auto=format&fit=crop&w=900&q=80',
|
||||
},
|
||||
{
|
||||
name: 'Sidama',
|
||||
profile: 'Balanced, complex flavor',
|
||||
altitude: '1700–2200 MASL',
|
||||
rainfall: 'Rich rainfall with cool nights',
|
||||
image:
|
||||
'https://images.unsplash.com/photo-1442512595331-e89e73853f31?auto=format&fit=crop&w=900&q=80',
|
||||
},
|
||||
{
|
||||
name: 'Harrar',
|
||||
profile: 'Bold, winey, spicy',
|
||||
altitude: '1500–2100 MASL',
|
||||
rainfall: 'Drier climate with intense character',
|
||||
image:
|
||||
'https://images.unsplash.com/photo-1455470956270-4cbb357f60f6?auto=format&fit=crop&w=900&q=80',
|
||||
},
|
||||
{
|
||||
name: 'Limu & Jimma',
|
||||
profile: 'Chocolatey, smooth',
|
||||
altitude: '1400–2100 MASL',
|
||||
rainfall: 'Steady seasonal rainfall',
|
||||
image:
|
||||
'https://images.unsplash.com/photo-1494314671902-399b18174975?auto=format&fit=crop&w=900&q=80',
|
||||
},
|
||||
];
|
||||
|
||||
export const qualityProcesses = [
|
||||
{
|
||||
title: 'Washed Process',
|
||||
highlight: 'Clean, bright coffee profile',
|
||||
steps: ['Selective harvesting', 'Pulping', 'Fermentation', 'Washing', 'Drying', 'Milling'],
|
||||
},
|
||||
{
|
||||
title: 'Natural Process',
|
||||
highlight: 'Sun-dried cherries develop fruity, bold flavors with patient drying.',
|
||||
steps: ['Selective harvesting', 'Whole cherry drying', 'Slow turning', 'Moisture stabilization', 'Hulling'],
|
||||
},
|
||||
{
|
||||
title: 'Honey Process',
|
||||
highlight: 'A semi-washed style balancing sweetness, texture, and acidity.',
|
||||
steps: ['Selective harvesting', 'Depulping', 'Mucilage retention', 'Controlled drying', 'Milling'],
|
||||
},
|
||||
];
|
||||
|
||||
export const qualityMetrics = [
|
||||
'Moisture control: 10–12%',
|
||||
'Cupping evaluation of aroma',
|
||||
'Body and mouthfeel review',
|
||||
'Acidity balance and clarity',
|
||||
'Aftertaste consistency',
|
||||
];
|
||||
|
||||
export const exportServices = [
|
||||
'FOB Djibouti export system managed with professional coordination',
|
||||
'Documentation handled internally for smoother buyer communication',
|
||||
'Reliable logistics chain from Addis Ababa to Djibouti and onward to global markets',
|
||||
'Flexible order volumes for specialty buyers, roasters, and wholesale partners',
|
||||
'Timely delivery expectations supported by strong communication and planning',
|
||||
];
|
||||
|
||||
export const sustainabilityPoints = [
|
||||
'Fair payment practices that respect producer effort and quality',
|
||||
'Ethical sourcing grounded in long-term supplier relationships',
|
||||
'Environmental responsibility in cultivation and processing decisions',
|
||||
'Community development engagement where coffee value begins',
|
||||
];
|
||||
|
||||
export const testimonials = [
|
||||
{
|
||||
quote:
|
||||
'A disciplined, origin-driven Ethiopian exporter with the kind of communication international buyers appreciate.',
|
||||
author: 'Testimonial placeholder — Specialty roasting partner',
|
||||
},
|
||||
{
|
||||
quote:
|
||||
'The combination of traceability, cup quality, and professionalism signals strong export potential.',
|
||||
author: 'Testimonial placeholder — Green coffee importer',
|
||||
},
|
||||
];
|
||||
|
||||
export const buyerTypes = [
|
||||
{ value: 'roaster', label: 'Roaster' },
|
||||
{ value: 'importer', label: 'Importer' },
|
||||
{ value: 'distributor', label: 'Distributor' },
|
||||
{ value: 'specialty_buyer', label: 'Specialty buyer' },
|
||||
{ value: 'wholesaler', label: 'Wholesaler' },
|
||||
{ value: 'other', label: 'Other' },
|
||||
];
|
||||
|
||||
export const inquiryTypes = [
|
||||
{ value: 'request_samples', label: 'Request Samples' },
|
||||
{ value: 'get_a_quote', label: 'Get a Quote' },
|
||||
{ value: 'partner_with_us', label: 'Partner With Us' },
|
||||
{ value: 'contact_message', label: 'General Inquiry' },
|
||||
];
|
||||
|
||||
export const incoterms = [
|
||||
{ value: 'fob', label: 'FOB' },
|
||||
{ value: 'cif', label: 'CIF' },
|
||||
{ value: 'cfr', label: 'CFR' },
|
||||
{ value: 'exw', label: 'EXW' },
|
||||
{ value: 'dap', label: 'DAP' },
|
||||
{ value: 'other', label: 'Other' },
|
||||
];
|
||||
|
||||
export const contactMethods = [
|
||||
{ value: 'email', label: 'Email' },
|
||||
{ value: 'phone', label: 'Phone' },
|
||||
{ value: 'whatsapp', label: 'WhatsApp' },
|
||||
];
|
||||
@ -62,3 +62,62 @@ font-family: 'Ubuntu', sans-serif !important;
|
||||
.introjs-prevbutton{
|
||||
@apply bg-transparent border border-pastelBrownTheme-buttonColor text-pastelBrownTheme-buttonColor !important;
|
||||
}
|
||||
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Playfair+Display:wght@600;700;800&display=swap');
|
||||
|
||||
:root {
|
||||
--coffee-brown: #1F3A2D;
|
||||
--coffee-beige: #F4F1E6;
|
||||
--coffee-gold: #9FB06F;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.font-brand-display {
|
||||
font-family: 'Playfair Display', Georgia, serif;
|
||||
}
|
||||
|
||||
.font-brand-body {
|
||||
font-family: 'Inter', 'Ubuntu', sans-serif;
|
||||
}
|
||||
|
||||
.coffee-input {
|
||||
width: 100%;
|
||||
height: 3.25rem;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid rgba(31, 58, 45, 0.14);
|
||||
background: #F8FAF4;
|
||||
padding: 0 1rem;
|
||||
font-size: 0.95rem;
|
||||
color: #22332B;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.coffee-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(159, 176, 111, 0.82);
|
||||
box-shadow: 0 0 0 3px rgba(159, 176, 111, 0.18);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.coffee-input::placeholder {
|
||||
color: #869488;
|
||||
}
|
||||
|
||||
@keyframes coffeeFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(14px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.coffee-fade-in {
|
||||
animation: coffeeFadeIn 0.8s ease both;
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React, { ReactNode, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import React, { ReactNode, useEffect, useState } from 'react'
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||
import menuAside from '../menuAside'
|
||||
|
||||
58
frontend/src/pages/about.tsx
Normal file
58
frontend/src/pages/about.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import MarketingLayout from '../components/marketing/MarketingLayout';
|
||||
import MarketingPageHero from '../components/marketing/MarketingPageHero';
|
||||
import MarketingSeo from '../components/marketing/MarketingSeo';
|
||||
import MarketingSection from '../components/marketing/MarketingSection';
|
||||
import { founderStory } from '../components/marketing/marketingData';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<>
|
||||
<MarketingSeo
|
||||
title="About Alem Desta Coffee Export"
|
||||
description="Founder story, Ethiopian coffee heritage, U.S. market exposure, and the roots behind Alem Desta Coffee Export."
|
||||
/>
|
||||
<MarketingLayout>
|
||||
<MarketingPageHero
|
||||
eyebrow="About us"
|
||||
title="A heritage-led coffee export business shaped by global perspective"
|
||||
description="Our story combines Ethiopian coffee culture, family tradition, U.S. market exposure, and practical export learning grounded in real relationships."
|
||||
image="https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?auto=format&fit=crop&w=1400&q=80"
|
||||
/>
|
||||
|
||||
<MarketingSection
|
||||
eyebrow="Founder story"
|
||||
title="From Ethiopian coffee culture to global buyer understanding"
|
||||
description="The company story is personal, emotional, and practical — exactly the mix international buyers often trust most when choosing who to work with."
|
||||
>
|
||||
<div className="grid gap-6 lg:grid-cols-[0.8fr_1.2fr]">
|
||||
<div className="rounded-[2rem] bg-[#1F3A2D] p-8 text-white shadow-[0_24px_80px_rgba(20,38,29,0.14)]">
|
||||
<p className="text-sm uppercase tracking-[0.32em] text-[#B7C78B]">What shapes the company</p>
|
||||
<ul className="mt-6 space-y-4 text-sm leading-7 text-white/78">
|
||||
<li>• Ethiopian coffee culture upbringing</li>
|
||||
<li>• Family coffee tradition</li>
|
||||
<li>• U.S. experience in Chicago, San Francisco, and the DMV</li>
|
||||
<li>• Return to Ethiopia with renewed business purpose</li>
|
||||
<li>• Export learning through close family mentorship</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{founderStory.map((paragraph) => (
|
||||
<div key={paragraph} className="rounded-[1.75rem] border border-[#1F3A2D]/8 bg-white p-7 shadow-[0_18px_60px_rgba(20,38,29,0.06)]">
|
||||
<p className="text-base leading-8 text-[#556257]">{paragraph}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</MarketingSection>
|
||||
</MarketingLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
AboutPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
55
frontend/src/pages/coffee-origins.tsx
Normal file
55
frontend/src/pages/coffee-origins.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import MarketingLayout from '../components/marketing/MarketingLayout';
|
||||
import MarketingPageHero from '../components/marketing/MarketingPageHero';
|
||||
import MarketingSeo from '../components/marketing/MarketingSeo';
|
||||
import MarketingSection from '../components/marketing/MarketingSection';
|
||||
import { origins } from '../components/marketing/marketingData';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
|
||||
export default function CoffeeOriginsPage() {
|
||||
return (
|
||||
<>
|
||||
<MarketingSeo
|
||||
title="Coffee Origins"
|
||||
description="Explore Ethiopian coffee regions including Yirgacheffe, Guji, Sidama, Harrar, Limu, and Jimma."
|
||||
/>
|
||||
<MarketingLayout>
|
||||
<MarketingPageHero
|
||||
eyebrow="Coffee origins"
|
||||
title="Regional character that gives Ethiopian coffee its global distinction"
|
||||
description="Each origin tells a different flavor story, giving international buyers a practical way to align sourcing decisions with roast goals and customer preferences."
|
||||
image="https://images.unsplash.com/photo-1455470956270-4cbb357f60f6?auto=format&fit=crop&w=1400&q=80"
|
||||
/>
|
||||
|
||||
<MarketingSection
|
||||
eyebrow="Origin portfolio"
|
||||
title="A curated overview of key coffee-producing regions"
|
||||
description="These region cards provide a premium, buyer-friendly snapshot of profile, altitude, and growing context."
|
||||
>
|
||||
<div className="grid gap-6 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{origins.map((item) => (
|
||||
<article key={item.name} className="overflow-hidden rounded-[2rem] bg-white shadow-[0_24px_70px_rgba(20,38,29,0.08)]">
|
||||
<div className="relative">
|
||||
<img className="h-64 w-full object-cover saturate-[0.9]" loading="lazy" src={item.image} alt={item.name} />
|
||||
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(18,38,29,0.05),rgba(18,38,29,0),rgba(18,38,29,0.25))]" />
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<p className="text-sm uppercase tracking-[0.28em] text-[#9FB06F]">{item.altitude}</p>
|
||||
<h2 className="font-brand-display mt-3 text-3xl text-[#1F3A2D]">{item.name}</h2>
|
||||
<p className="mt-4 text-sm leading-7 text-[#556257]">{item.profile}</p>
|
||||
<div className="mt-5 rounded-2xl bg-[#F4F1E6] px-4 py-3 text-sm text-[#556257]">Rainfall: {item.rainfall}</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</MarketingSection>
|
||||
</MarketingLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
CoffeeOriginsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
55
frontend/src/pages/coffee-philosophy.tsx
Normal file
55
frontend/src/pages/coffee-philosophy.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import { mdiTerrain, mdiWeatherPartlyCloudy, mdiSprout, mdiHumanGreeting } from '@mdi/js';
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
import MarketingLayout from '../components/marketing/MarketingLayout';
|
||||
import MarketingPageHero from '../components/marketing/MarketingPageHero';
|
||||
import MarketingSeo from '../components/marketing/MarketingSeo';
|
||||
import MarketingSection from '../components/marketing/MarketingSection';
|
||||
import { philosophyPillars } from '../components/marketing/marketingData';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
|
||||
const icons = [mdiTerrain, mdiSprout, mdiWeatherPartlyCloudy, mdiHumanGreeting];
|
||||
|
||||
export default function CoffeePhilosophyPage() {
|
||||
return (
|
||||
<>
|
||||
<MarketingSeo
|
||||
title="Coffee Philosophy"
|
||||
description="Coffee excellence begins at origin — patience, altitude, climate, soil, and craftsmanship define premium Ethiopian coffee."
|
||||
/>
|
||||
<MarketingLayout>
|
||||
<MarketingPageHero
|
||||
eyebrow="Coffee philosophy"
|
||||
title="Coffee Excellence Begins at Origin"
|
||||
description="Coffee is not industrial. It is crafted — by land, weather, skilled hands, and the patience required to let quality mature naturally."
|
||||
image="https://images.unsplash.com/photo-1509042239860-f550ce710b93?auto=format&fit=crop&w=1400&q=80"
|
||||
/>
|
||||
|
||||
<MarketingSection
|
||||
eyebrow="Our belief"
|
||||
title="Great coffee is the result of nature and human discipline working together"
|
||||
description="This page articulates a premium sourcing philosophy buyers can feel: Ethiopian coffee is a crafted product, not a commodity shortcut."
|
||||
centered
|
||||
>
|
||||
<div className="grid gap-6 md:grid-cols-2 xl:grid-cols-4">
|
||||
{philosophyPillars.map((item, index) => (
|
||||
<div key={item.title} className="rounded-[1.75rem] border border-[#1F3A2D]/8 bg-white p-7 shadow-[0_20px_65px_rgba(20,38,29,0.06)]">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[#F4F1E6] text-[#1F3A2D]">
|
||||
<BaseIcon path={icons[index]} />
|
||||
</div>
|
||||
<h3 className="font-brand-display mt-5 text-2xl text-[#1F3A2D]">{item.title}</h3>
|
||||
<p className="mt-4 text-sm leading-7 text-[#556257]">{item.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</MarketingSection>
|
||||
</MarketingLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
CoffeePhilosophyPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
102
frontend/src/pages/contact.tsx
Normal file
102
frontend/src/pages/contact.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import { mdiEmailOutline, mdiMapMarkerOutline, mdiPhoneOutline } from '@mdi/js';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
import InquiryForm from '../components/marketing/InquiryForm';
|
||||
import MarketingButton from '../components/marketing/MarketingButton';
|
||||
import MarketingLayout from '../components/marketing/MarketingLayout';
|
||||
import MarketingPageHero from '../components/marketing/MarketingPageHero';
|
||||
import MarketingSeo from '../components/marketing/MarketingSeo';
|
||||
import MarketingSection from '../components/marketing/MarketingSection';
|
||||
import { brand } from '../components/marketing/marketingData';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
|
||||
const inquiryLinks = [
|
||||
{ href: '/contact?type=request_samples', label: 'Request Samples' },
|
||||
{ href: '/contact?type=get_a_quote', label: 'Get a Quote' },
|
||||
{ href: '/contact?type=partner_with_us', label: 'Partner With Us' },
|
||||
];
|
||||
|
||||
export default function ContactPage() {
|
||||
const router = useRouter();
|
||||
const initialType = typeof router.query.type === 'string' ? router.query.type : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MarketingSeo
|
||||
title="Contact"
|
||||
description="Contact Alem Desta Coffee Export in Addis Ababa and submit sample requests, quote requests, or partnership inquiries."
|
||||
/>
|
||||
<MarketingLayout>
|
||||
<MarketingPageHero
|
||||
eyebrow="Contact"
|
||||
title="Start a confident buying conversation"
|
||||
description="The contact experience is designed as a real workflow: choose your inquiry type, submit details, and route the request directly into the admin inquiry queue."
|
||||
image="https://images.unsplash.com/photo-1494314671902-399b18174975?auto=format&fit=crop&w=1400&q=80"
|
||||
/>
|
||||
|
||||
<MarketingSection
|
||||
eyebrow="How to engage"
|
||||
title="Choose the right buyer action and share your needs"
|
||||
description="Sample requests, quotes, and partnership conversations all flow through one elegant form so your team can respond quickly and consistently."
|
||||
>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{inquiryLinks.map((item) => (
|
||||
<MarketingButton key={item.href} href={item.href} variant={initialType === item.href.split('=')[1] ? 'primary' : 'ghost'}>
|
||||
{item.label}
|
||||
</MarketingButton>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-10 grid gap-8 lg:grid-cols-[0.75fr_1.25fr]">
|
||||
<div className="space-y-5">
|
||||
<div className="rounded-[1.75rem] border border-[#1F3A2D]/8 bg-white p-6 shadow-[0_20px_65px_rgba(20,38,29,0.06)]">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[#F4F1E6] text-[#1F3A2D]">
|
||||
<BaseIcon path={mdiMapMarkerOutline} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.28em] text-[#9FB06F]">Location</p>
|
||||
<p className="mt-3 text-sm leading-7 text-[#556257]">{brand.address}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[1.75rem] border border-[#1F3A2D]/8 bg-white p-6 shadow-[0_20px_65px_rgba(20,38,29,0.06)]">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[#F4F1E6] text-[#1F3A2D]">
|
||||
<BaseIcon path={mdiEmailOutline} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.28em] text-[#9FB06F]">Email</p>
|
||||
<p className="mt-3 text-sm leading-7 text-[#556257]">{brand.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[1.75rem] border border-[#1F3A2D]/8 bg-white p-6 shadow-[0_20px_65px_rgba(20,38,29,0.06)]">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[#F4F1E6] text-[#1F3A2D]">
|
||||
<BaseIcon path={mdiPhoneOutline} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.28em] text-[#9FB06F]">Phone</p>
|
||||
<div className="mt-3 space-y-1 text-sm leading-7 text-[#556257]">
|
||||
{brand.phones.map((phone) => (
|
||||
<p key={phone}>{phone}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<InquiryForm initialInquiryType={initialType} />
|
||||
</div>
|
||||
</MarketingSection>
|
||||
</MarketingLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ContactPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
119
frontend/src/pages/export-services.tsx
Normal file
119
frontend/src/pages/export-services.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import {
|
||||
mdiCompassOutline,
|
||||
mdiLeaf,
|
||||
mdiShieldCheckOutline,
|
||||
mdiTruckFastOutline,
|
||||
} from '@mdi/js';
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
import MarketingButton from '../components/marketing/MarketingButton';
|
||||
import MarketingLayout from '../components/marketing/MarketingLayout';
|
||||
import MarketingPageHero from '../components/marketing/MarketingPageHero';
|
||||
import MarketingSeo from '../components/marketing/MarketingSeo';
|
||||
import MarketingSection from '../components/marketing/MarketingSection';
|
||||
import { exportServiceItems } from '../components/marketing/marketingData';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
|
||||
const iconByHref = {
|
||||
'/coffee-origins': mdiCompassOutline,
|
||||
'/quality-control': mdiShieldCheckOutline,
|
||||
'/sustainability': mdiLeaf,
|
||||
'/logistics-export': mdiTruckFastOutline,
|
||||
};
|
||||
|
||||
const workflowSteps = [
|
||||
{
|
||||
title: 'Origin matching',
|
||||
description:
|
||||
'We align buyer taste goals, processing preferences, and market needs with the most suitable Ethiopian origins.',
|
||||
},
|
||||
{
|
||||
title: 'Quality verification',
|
||||
description:
|
||||
'Lots move through disciplined selection, moisture review, and cupping evaluation before they are prepared for export.',
|
||||
},
|
||||
{
|
||||
title: 'Shipment coordination',
|
||||
description:
|
||||
'Documentation, communication, and logistics are handled with clarity from Addis Ababa through Djibouti to destination.',
|
||||
},
|
||||
];
|
||||
|
||||
export default function ExportServicesPage() {
|
||||
return (
|
||||
<>
|
||||
<MarketingSeo
|
||||
title="Export Services"
|
||||
description="Explore Alem Desta Coffee Export's service hub covering coffee origins, quality control, sustainability, and logistics coordination for global buyers."
|
||||
/>
|
||||
<MarketingLayout>
|
||||
<MarketingPageHero
|
||||
eyebrow="Export services"
|
||||
title="A premium export hub designed for serious international coffee buyers"
|
||||
description="The Export Services page brings every key decision area into one elegant overview — origin selection, quality assurance, sustainability expectations, and shipment coordination."
|
||||
image="https://images.unsplash.com/photo-1520607162513-77705c0f0d4a?auto=format&fit=crop&w=1400&q=80"
|
||||
/>
|
||||
|
||||
<MarketingSection
|
||||
eyebrow="Service hub"
|
||||
title="Explore the four pillars behind our export offer"
|
||||
description="Rather than overcrowding the main navigation, the website groups our detailed export capabilities here in one clear buyer-focused hub."
|
||||
>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{exportServiceItems.map((item) => (
|
||||
<article
|
||||
key={item.href}
|
||||
className="rounded-[2rem] border border-[#1F3A2D]/8 bg-white p-7 shadow-[0_20px_65px_rgba(20,38,29,0.06)]"
|
||||
>
|
||||
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-[#F4F1E6] text-[#1F3A2D]">
|
||||
<BaseIcon path={iconByHref[item.href as keyof typeof iconByHref]} />
|
||||
</div>
|
||||
<h2 className="font-brand-display mt-6 text-3xl text-[#1F3A2D]">{item.label}</h2>
|
||||
<p className="mt-4 text-sm leading-7 text-[#556257]">{item.description}</p>
|
||||
<div className="mt-6">
|
||||
<MarketingButton href={item.href} variant="ghost">
|
||||
View details
|
||||
</MarketingButton>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</MarketingSection>
|
||||
|
||||
<MarketingSection
|
||||
eyebrow="Buyer workflow"
|
||||
title="How the export relationship is structured"
|
||||
description="International buyers need more than storytelling. They need a process that feels organized, transparent, and commercially dependable."
|
||||
className="bg-[#F8FAF4]"
|
||||
>
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{workflowSteps.map((step, index) => (
|
||||
<div
|
||||
key={step.title}
|
||||
className="rounded-[1.75rem] border border-[#1F3A2D]/8 bg-white p-7 shadow-[0_18px_55px_rgba(20,38,29,0.05)]"
|
||||
>
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-full bg-[#1F3A2D] text-sm font-semibold text-[#F4F1E6]">
|
||||
{index + 1}
|
||||
</div>
|
||||
<h3 className="font-brand-display mt-6 text-2xl text-[#1F3A2D]">{step.title}</h3>
|
||||
<p className="mt-4 text-sm leading-7 text-[#556257]">{step.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-10 flex flex-col gap-4 sm:flex-row">
|
||||
<MarketingButton href="/contact?type=request_samples">Request Samples</MarketingButton>
|
||||
<MarketingButton href="/contact?type=get_a_quote" variant="ghost">
|
||||
Request a tailored quote
|
||||
</MarketingButton>
|
||||
</div>
|
||||
</MarketingSection>
|
||||
</MarketingLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ExportServicesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
@ -1,166 +1,258 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
mdiEarth,
|
||||
mdiLeaf,
|
||||
mdiMapMarker,
|
||||
mdiShieldCheck,
|
||||
mdiTruckFastOutline,
|
||||
} from '@mdi/js';
|
||||
import type { ReactElement } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import CardBox from '../components/CardBox';
|
||||
import SectionFullScreen from '../components/SectionFullScreen';
|
||||
import React from 'react';
|
||||
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
import MarketingButton from '../components/marketing/MarketingButton';
|
||||
import MarketingLayout from '../components/marketing/MarketingLayout';
|
||||
import MarketingSection from '../components/marketing/MarketingSection';
|
||||
import MarketingSeo from '../components/marketing/MarketingSeo';
|
||||
import NewsletterForm from '../components/marketing/NewsletterForm';
|
||||
import {
|
||||
brand,
|
||||
exportServices,
|
||||
featureHighlights,
|
||||
galleryItems,
|
||||
origins,
|
||||
testimonials,
|
||||
} from '../components/marketing/marketingData';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import { getPageTitle } from '../config';
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
||||
|
||||
const statItems = [
|
||||
{ label: 'Origin-driven sourcing', value: 'Ethiopia first' },
|
||||
{ label: 'Export route', value: 'Addis Ababa → Djibouti → Global markets' },
|
||||
{ label: 'Buyer focus', value: 'Roasters, importers, distributors, wholesalers' },
|
||||
];
|
||||
|
||||
export default function Starter() {
|
||||
const [illustrationImage, setIllustrationImage] = useState({
|
||||
src: undefined,
|
||||
photographer: undefined,
|
||||
photographer_url: undefined,
|
||||
})
|
||||
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
|
||||
const [contentType, setContentType] = useState('video');
|
||||
const [contentPosition, setContentPosition] = useState('right');
|
||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||
|
||||
const title = 'Alem Desta Coffee Export Site'
|
||||
|
||||
// Fetch Pexels image/video
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const image = await getPexelsImage();
|
||||
const video = await getPexelsVideo();
|
||||
setIllustrationImage(image);
|
||||
setIllustrationVideo(video);
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const imageBlock = (image) => (
|
||||
<div
|
||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
||||
style={{
|
||||
backgroundImage: `${
|
||||
image
|
||||
? `url(${image?.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
>
|
||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={image?.photographer_url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Photo by {image?.photographer} on Pexels
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const videoBlock = (video) => {
|
||||
if (video?.video_files?.length > 0) {
|
||||
return (
|
||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
||||
<video
|
||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
>
|
||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={video?.user?.url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Video by {video.user.name} on Pexels
|
||||
</a>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
};
|
||||
const trustPillars = [
|
||||
{
|
||||
icon: mdiShieldCheck,
|
||||
title: 'Trust & transparency',
|
||||
description: 'Clear communication, honest documentation, and disciplined follow-through for international buyers.',
|
||||
},
|
||||
{
|
||||
icon: mdiMapMarker,
|
||||
title: 'Origin expertise',
|
||||
description: 'Regional understanding supports lot selection aligned with buyer taste profiles and sourcing goals.',
|
||||
},
|
||||
{
|
||||
icon: mdiTruckFastOutline,
|
||||
title: 'Export reliability',
|
||||
description: 'A structured logistics chain supports timely movement from origin to destination.',
|
||||
},
|
||||
{
|
||||
icon: mdiLeaf,
|
||||
title: 'Responsible sourcing',
|
||||
description: 'Sustainability is treated as a business responsibility, not a campaign message.',
|
||||
},
|
||||
];
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
contentPosition === 'background'
|
||||
? {
|
||||
backgroundImage: `${
|
||||
illustrationImage
|
||||
? `url(${illustrationImage.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<Head>
|
||||
<title>{getPageTitle('Starter Page')}</title>
|
||||
</Head>
|
||||
<>
|
||||
<MarketingSeo
|
||||
title="Ethiopian Premium Coffee Export"
|
||||
description="Premium Ethiopian coffee exporter connecting origin, traceability, and global buyer confidence with elegant, professional service."
|
||||
/>
|
||||
<MarketingLayout>
|
||||
<section className="relative -mt-[88px] overflow-hidden bg-[#16271F] px-5 pb-16 pt-36 text-white lg:px-8 lg:pb-24 lg:pt-44">
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
className="h-full w-full scale-[1.02] object-cover opacity-32 saturate-[0.82]"
|
||||
loading="eager"
|
||||
src="https://images.unsplash.com/photo-1447933601403-0c6688de566e?auto=format&fit=crop&w=1600&q=80"
|
||||
alt="Ethiopian coffee farm"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-[linear-gradient(135deg,rgba(18,38,29,0.88),rgba(31,58,45,0.72),rgba(18,38,29,0.9))]" />
|
||||
</div>
|
||||
|
||||
<SectionFullScreen bg='violet'>
|
||||
<div
|
||||
className={`flex ${
|
||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
||||
} min-h-screen w-full`}
|
||||
>
|
||||
{contentType === 'image' && contentPosition !== 'background'
|
||||
? imageBlock(illustrationImage)
|
||||
: null}
|
||||
{contentType === 'video' && contentPosition !== 'background'
|
||||
? videoBlock(illustrationVideo)
|
||||
: null}
|
||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
||||
<CardBoxComponentTitle title="Welcome to your Alem Desta Coffee Export Site app!"/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
||||
<p className='text-center text-gray-500'>For guides and documentation please check
|
||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
||||
<div className="coffee-fade-in relative mx-auto grid max-w-7xl gap-10 lg:grid-cols-[1.05fr_0.95fr] lg:items-end">
|
||||
<div>
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.38em] text-[#B7C78B]">Specialty green coffee export</p>
|
||||
<h1 className="font-brand-display mt-6 max-w-4xl text-5xl leading-[1.05] text-[#F4F1E6] md:text-7xl">
|
||||
Ethiopian Premium Coffee – Sourced with Experience, Exported with Integrity
|
||||
</h1>
|
||||
<p className="mt-6 max-w-3xl text-lg leading-8 text-white/78">
|
||||
Achieving premium coffee excellence from its origin.
|
||||
</p>
|
||||
<p className="mt-6 max-w-3xl text-base leading-8 text-white/72">
|
||||
{brand.name} bridges Ethiopian premium coffee producers with global buyers, focusing on traceability,
|
||||
single-origin green coffee sourcing, export-ready preparation, and uncompromising quality.
|
||||
</p>
|
||||
<div className="mt-10 flex flex-col gap-4 sm:flex-row">
|
||||
<MarketingButton href="/contact?type=request_samples">Request Samples</MarketingButton>
|
||||
<MarketingButton href="/contact?type=get_a_quote" variant="secondary">
|
||||
Get a Quote
|
||||
</MarketingButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
href='/login'
|
||||
label='Login'
|
||||
color='info'
|
||||
className='w-full'
|
||||
/>
|
||||
<div className="rounded-[2rem] border border-white/10 bg-white/10 p-6 backdrop-blur-md shadow-[0_30px_80px_rgba(0,0,0,0.25)] md:p-8">
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.28em] text-[#B7C78B]">Why buyers trust us</p>
|
||||
<div className="mt-8 space-y-5">
|
||||
{statItems.map((item) => (
|
||||
<div key={item.label} className="border-b border-white/10 pb-5 last:border-b-0 last:pb-0">
|
||||
<p className="text-sm uppercase tracking-[0.28em] text-white/48">{item.label}</p>
|
||||
<p className="mt-2 text-lg font-medium text-white">{item.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</BaseButtons>
|
||||
</CardBox>
|
||||
</div>
|
||||
</div>
|
||||
</SectionFullScreen>
|
||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</div>
|
||||
<MarketingSection
|
||||
eyebrow="Immediate confidence"
|
||||
title="A refined sourcing experience for global coffee buyers"
|
||||
description="The first impression international buyers need is clarity: where coffee comes from, how quality is protected, and whether export communication will be dependable. This website presents that trust story with elegance and precision."
|
||||
>
|
||||
<div className="grid gap-6 md:grid-cols-2 xl:grid-cols-4">
|
||||
{trustPillars.map((item) => (
|
||||
<div key={item.title} className="rounded-[1.75rem] border border-[#1F3A2D]/8 bg-white p-6 shadow-[0_20px_65px_rgba(20,38,29,0.06)]">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[#F4F1E6] text-[#1F3A2D]">
|
||||
<BaseIcon path={item.icon} />
|
||||
</div>
|
||||
<h3 className="font-brand-display mt-5 text-2xl text-[#1F3A2D]">{item.title}</h3>
|
||||
<p className="mt-3 text-sm leading-7 text-[#556257]">{item.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</MarketingSection>
|
||||
|
||||
</div>
|
||||
<MarketingSection
|
||||
eyebrow="Feature highlights"
|
||||
title="Built around traceability, premium handling, and long-term export partnerships"
|
||||
description="Each highlight below mirrors the core buying questions of roasters, green coffee importers, and wholesale partners evaluating a supplier relationship."
|
||||
centered
|
||||
className="bg-[#F8FAF4]"
|
||||
>
|
||||
<div className="grid gap-6 lg:grid-cols-2 xl:grid-cols-4">
|
||||
{featureHighlights.map((item) => (
|
||||
<div key={item.title} className="rounded-[1.75rem] border border-[#1F3A2D]/8 bg-white p-7 shadow-[0_20px_70px_rgba(20,38,29,0.07)]">
|
||||
<h3 className="font-brand-display text-2xl text-[#1F3A2D]">{item.title}</h3>
|
||||
<p className="mt-4 text-sm leading-7 text-[#556257]">{item.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</MarketingSection>
|
||||
|
||||
<MarketingSection
|
||||
eyebrow="Visual origin story"
|
||||
title="From farms to cupping tables to export readiness"
|
||||
description="The buyer journey starts with visual confidence: clean origin imagery, disciplined processing, and a clear sense of how coffee moves professionally toward shipment."
|
||||
>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{galleryItems.map((item) => (
|
||||
<article key={item.title} className="group overflow-hidden rounded-[2rem] bg-white shadow-[0_24px_70px_rgba(20,38,29,0.08)]">
|
||||
<div className="relative h-72 overflow-hidden">
|
||||
<img
|
||||
className="h-full w-full object-cover saturate-[0.88] transition duration-700 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
src={item.image}
|
||||
alt={item.title}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(18,38,29,0.08),rgba(18,38,29,0),rgba(18,38,29,0.28))]" />
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<h3 className="font-brand-display text-2xl text-[#1F3A2D]">{item.title}</h3>
|
||||
<p className="mt-3 text-sm leading-7 text-[#556257]">{item.caption}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</MarketingSection>
|
||||
|
||||
<MarketingSection
|
||||
eyebrow="Origin snapshots"
|
||||
title="Distinctive Ethiopian regions, each with its own flavor narrative"
|
||||
description="A quick regional preview gives buyers a sense of sourcing depth before they request samples or a tailored quote."
|
||||
className="bg-[#F4F1E6]/45"
|
||||
>
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{origins.slice(0, 3).map((item) => (
|
||||
<div key={item.name} className="overflow-hidden rounded-[2rem] bg-white shadow-[0_24px_70px_rgba(20,38,29,0.08)]">
|
||||
<div className="relative">
|
||||
<img className="h-56 w-full object-cover saturate-[0.9]" loading="lazy" src={item.image} alt={item.name} />
|
||||
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(18,38,29,0.05),rgba(18,38,29,0),rgba(18,38,29,0.24))]" />
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<p className="text-sm uppercase tracking-[0.28em] text-[#9FB06F]">{item.altitude}</p>
|
||||
<h3 className="font-brand-display mt-3 text-2xl text-[#1F3A2D]">{item.name}</h3>
|
||||
<p className="mt-3 text-sm leading-7 text-[#556257]">{item.profile}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<MarketingButton href="/coffee-origins" variant="ghost">
|
||||
Explore all origins
|
||||
</MarketingButton>
|
||||
</div>
|
||||
</MarketingSection>
|
||||
|
||||
<MarketingSection
|
||||
eyebrow="Export capability"
|
||||
title="Professional export services built for international buyers"
|
||||
description="Reliable logistics, internal documentation handling, and strong communication help create a premium corporate buying experience."
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{exportServices.slice(0, 4).map((item) => (
|
||||
<div key={item} className="flex gap-4 rounded-[1.5rem] border border-[#1F3A2D]/8 bg-white p-5 shadow-[0_18px_60px_rgba(20,38,29,0.06)]">
|
||||
<div className="mt-1 text-[#9FB06F]">
|
||||
<BaseIcon path={mdiEarth} />
|
||||
</div>
|
||||
<p className="text-sm leading-7 text-[#556257]">{item}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</MarketingSection>
|
||||
|
||||
<MarketingSection
|
||||
eyebrow="Testimonials"
|
||||
title="Commercial credibility, with room to grow into buyer proof"
|
||||
description="These premium placeholders establish the intended testimonial zone so future buyer feedback can be added without redesigning the site."
|
||||
centered
|
||||
className="bg-[#F8FAF4]"
|
||||
>
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{testimonials.map((item) => (
|
||||
<blockquote key={item.author} className="rounded-[1.75rem] border border-[#1F3A2D]/8 bg-white p-7 shadow-[0_18px_60px_rgba(20,38,29,0.06)]">
|
||||
<p className="font-brand-display text-2xl leading-10 text-[#1F3A2D]">“{item.quote}”</p>
|
||||
<footer className="mt-6 text-sm font-medium uppercase tracking-[0.24em] text-[#72816F]">{item.author}</footer>
|
||||
</blockquote>
|
||||
))}
|
||||
</div>
|
||||
</MarketingSection>
|
||||
|
||||
<MarketingSection
|
||||
eyebrow="Stay informed"
|
||||
title="Subscribe for sourcing updates"
|
||||
description="Newsletter sign-up is already connected to the admin subscriber list, giving your team a real conversion path from day one."
|
||||
className="bg-[#1F3A2D] text-white"
|
||||
>
|
||||
<div className="grid gap-10 lg:grid-cols-[1fr_0.9fr] lg:items-center">
|
||||
<div>
|
||||
<h3 className="font-brand-display text-4xl text-[#F4F1E6]">Receive buyer-focused green coffee updates</h3>
|
||||
<p className="mt-5 max-w-2xl text-base leading-8 text-white/72">
|
||||
Join the list for availability updates, new origin highlights, and company announcements shaped for professional buyers.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[2rem] border border-white/10 bg-white/10 p-6 backdrop-blur-md">
|
||||
<NewsletterForm source="home_cta" />
|
||||
</div>
|
||||
</div>
|
||||
</MarketingSection>
|
||||
</MarketingLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||
HomePage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
|
||||
|
||||
109
frontend/src/pages/logistics-export.tsx
Normal file
109
frontend/src/pages/logistics-export.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import { mdiFileDocumentOutline, mdiMessageTextFastOutline, mdiPackageVariantClosed, mdiTruckFastOutline } from '@mdi/js';
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
import MarketingLayout from '../components/marketing/MarketingLayout';
|
||||
import MarketingPageHero from '../components/marketing/MarketingPageHero';
|
||||
import MarketingSeo from '../components/marketing/MarketingSeo';
|
||||
import MarketingSection from '../components/marketing/MarketingSection';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
|
||||
const logisticsPillars = [
|
||||
{
|
||||
icon: mdiFileDocumentOutline,
|
||||
title: 'Documentation handling',
|
||||
description:
|
||||
'Export paperwork is managed with care to support smoother communication and reduce uncertainty for international buyers.',
|
||||
},
|
||||
{
|
||||
icon: mdiPackageVariantClosed,
|
||||
title: 'Shipment preparation',
|
||||
description:
|
||||
'Lots are prepared for export with disciplined packaging, timing coordination, and readiness checks before dispatch.',
|
||||
},
|
||||
{
|
||||
icon: mdiTruckFastOutline,
|
||||
title: 'Route coordination',
|
||||
description:
|
||||
'The logistics chain is organized from Addis Ababa to Djibouti and onward to destination markets with responsive updates.',
|
||||
},
|
||||
{
|
||||
icon: mdiMessageTextFastOutline,
|
||||
title: 'Buyer communication',
|
||||
description:
|
||||
'Prompt communication keeps importers, roasters, and distributors informed throughout the export workflow.',
|
||||
},
|
||||
];
|
||||
|
||||
const timeline = [
|
||||
'Lot confirmation and shipment planning',
|
||||
'Internal document preparation and export coordination',
|
||||
'Movement from origin handling points toward Djibouti',
|
||||
'Final export dispatch with clear buyer communication',
|
||||
];
|
||||
|
||||
export default function LogisticsExportPage() {
|
||||
return (
|
||||
<>
|
||||
<MarketingSeo
|
||||
title="Logistics & Export"
|
||||
description="Learn how Alem Desta Coffee Export coordinates documentation, shipment preparation, and export communication from Ethiopia to global markets."
|
||||
/>
|
||||
<MarketingLayout>
|
||||
<MarketingPageHero
|
||||
eyebrow="Logistics & export"
|
||||
title="Export coordination that feels disciplined, responsive, and globally ready"
|
||||
description="For premium buyers, logistics is part of trust. We present the export chain with the same clarity and professionalism expected from the coffee itself."
|
||||
image="https://images.unsplash.com/photo-1520607162513-77705c0f0d4a?auto=format&fit=crop&w=1400&q=80"
|
||||
/>
|
||||
|
||||
<MarketingSection
|
||||
eyebrow="Operational confidence"
|
||||
title="The logistics side of the business is built to reduce friction"
|
||||
description="This page gives buyers a concise view of how coffee moves from Ethiopian origin into an organized export workflow."
|
||||
>
|
||||
<div className="grid gap-6 md:grid-cols-2 xl:grid-cols-4">
|
||||
{logisticsPillars.map((item) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className="rounded-[1.75rem] border border-[#1F3A2D]/8 bg-white p-6 shadow-[0_20px_65px_rgba(20,38,29,0.06)]"
|
||||
>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[#F4F1E6] text-[#1F3A2D]">
|
||||
<BaseIcon path={item.icon} />
|
||||
</div>
|
||||
<h2 className="font-brand-display mt-5 text-2xl text-[#1F3A2D]">{item.title}</h2>
|
||||
<p className="mt-4 text-sm leading-7 text-[#556257]">{item.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</MarketingSection>
|
||||
|
||||
<MarketingSection
|
||||
eyebrow="Export flow"
|
||||
title="A simple view of how the shipment process is organized"
|
||||
description="The sequence below reinforces a premium message: disciplined coffee handling, timely coordination, and dependable communication."
|
||||
className="bg-[#F8FAF4]"
|
||||
>
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{timeline.map((step, index) => (
|
||||
<div
|
||||
key={step}
|
||||
className="flex items-center gap-4 rounded-[1.5rem] border border-[#1F3A2D]/8 bg-white px-5 py-4 shadow-[0_18px_55px_rgba(20,38,29,0.05)]"
|
||||
>
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-[#1F3A2D] text-sm font-semibold text-[#F4F1E6]">
|
||||
{index + 1}
|
||||
</div>
|
||||
<p className="text-sm leading-7 text-[#556257]">{step}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</MarketingSection>
|
||||
</MarketingLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
LogisticsExportPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
79
frontend/src/pages/quality-control.tsx
Normal file
79
frontend/src/pages/quality-control.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import { mdiChartWaterfall, mdiCupOutline, mdiWaterOutline } from '@mdi/js';
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
import MarketingLayout from '../components/marketing/MarketingLayout';
|
||||
import MarketingPageHero from '../components/marketing/MarketingPageHero';
|
||||
import MarketingSeo from '../components/marketing/MarketingSeo';
|
||||
import MarketingSection from '../components/marketing/MarketingSection';
|
||||
import { qualityMetrics, qualityProcesses } from '../components/marketing/marketingData';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
|
||||
const processIcons = [mdiWaterOutline, mdiChartWaterfall, mdiCupOutline];
|
||||
|
||||
export default function QualityControlPage() {
|
||||
return (
|
||||
<>
|
||||
<MarketingSeo
|
||||
title="Quality Control"
|
||||
description="Washed, natural, and honey processing workflows with moisture control and cupping evaluation standards."
|
||||
/>
|
||||
<MarketingLayout>
|
||||
<MarketingPageHero
|
||||
eyebrow="Quality control"
|
||||
title="Structured processing and disciplined quality review"
|
||||
description="This page turns processing detail into buyer confidence by showing how washed, natural, and honey methods are handled and evaluated."
|
||||
image="https://images.unsplash.com/photo-1511920170033-f8396924c348?auto=format&fit=crop&w=1400&q=80"
|
||||
/>
|
||||
|
||||
<MarketingSection
|
||||
eyebrow="Processing systems"
|
||||
title="Three process pathways, each explained with clarity"
|
||||
description="The goal is simple: make process detail easy to read, visually elegant, and commercially reassuring."
|
||||
>
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{qualityProcesses.map((item, index) => (
|
||||
<div key={item.title} className="rounded-[1.85rem] border border-[#1F3A2D]/8 bg-white p-7 shadow-[0_24px_70px_rgba(20,38,29,0.06)]">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[#F4F1E6] text-[#1F3A2D]">
|
||||
<BaseIcon path={processIcons[index]} />
|
||||
</div>
|
||||
<h3 className="font-brand-display mt-5 text-3xl text-[#1F3A2D]">{item.title}</h3>
|
||||
<p className="mt-4 text-sm leading-7 text-[#556257]">{item.highlight}</p>
|
||||
<div className="mt-6 space-y-3">
|
||||
{item.steps.map((step, stepIndex) => (
|
||||
<div key={step} className="flex items-center gap-3 rounded-2xl bg-[#EEF3E7] px-4 py-3 text-sm text-[#4F5E53]">
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-[#1F3A2D] text-xs font-semibold text-[#F4F1E6]">
|
||||
{stepIndex + 1}
|
||||
</span>
|
||||
{step}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</MarketingSection>
|
||||
|
||||
<MarketingSection
|
||||
eyebrow="Quality system"
|
||||
title="Measured, tasted, and reviewed before export"
|
||||
description="Moisture control and cupping evaluation help translate process discipline into export confidence."
|
||||
className="bg-[#F8FAF4]"
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{qualityMetrics.map((item) => (
|
||||
<div key={item} className="rounded-[1.5rem] border border-[#1F3A2D]/8 bg-white px-5 py-4 text-sm leading-7 text-[#556257] shadow-[0_18px_55px_rgba(20,38,29,0.05)]">
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</MarketingSection>
|
||||
</MarketingLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
QualityControlPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
47
frontend/src/pages/sustainability.tsx
Normal file
47
frontend/src/pages/sustainability.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import MarketingLayout from '../components/marketing/MarketingLayout';
|
||||
import MarketingPageHero from '../components/marketing/MarketingPageHero';
|
||||
import MarketingSeo from '../components/marketing/MarketingSeo';
|
||||
import MarketingSection from '../components/marketing/MarketingSection';
|
||||
import { sustainabilityPoints } from '../components/marketing/marketingData';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
|
||||
export default function SustainabilityPage() {
|
||||
return (
|
||||
<>
|
||||
<MarketingSeo
|
||||
title="Sustainability"
|
||||
description="Fair payment, ethical sourcing, environmental care, and community development framed as a real business responsibility."
|
||||
/>
|
||||
<MarketingLayout>
|
||||
<MarketingPageHero
|
||||
eyebrow="Sustainability"
|
||||
title="Sustainability is our responsibility, not a marketing claim"
|
||||
description="This message anchors the brand in seriousness and credibility, especially for buyers evaluating long-term sourcing relationships."
|
||||
image="https://images.unsplash.com/photo-1461988320302-91bde64fc8e4?auto=format&fit=crop&w=1400&q=80"
|
||||
/>
|
||||
|
||||
<MarketingSection
|
||||
eyebrow="Responsible sourcing"
|
||||
title="A business approach that respects people, land, and long-term supply"
|
||||
description="Sustainability should be visible in sourcing behavior, not only in headlines."
|
||||
centered
|
||||
>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{sustainabilityPoints.map((item) => (
|
||||
<div key={item} className="rounded-[1.75rem] border border-[#1F3A2D]/8 bg-white p-7 text-sm leading-7 text-[#556257] shadow-[0_20px_65px_rgba(20,38,29,0.06)]">
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</MarketingSection>
|
||||
</MarketingLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
SustainabilityPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
72
frontend/src/pages/vision-mission-values.tsx
Normal file
72
frontend/src/pages/vision-mission-values.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { mdiHandshakeOutline, mdiLeaf, mdiMedalOutline, mdiScaleBalance, mdiSeedOutline, mdiShieldCheck } from '@mdi/js';
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
import MarketingLayout from '../components/marketing/MarketingLayout';
|
||||
import MarketingPageHero from '../components/marketing/MarketingPageHero';
|
||||
import MarketingSeo from '../components/marketing/MarketingSeo';
|
||||
import MarketingSection from '../components/marketing/MarketingSection';
|
||||
import { principles, values } from '../components/marketing/marketingData';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
|
||||
const icons = [mdiShieldCheck, mdiSeedOutline, mdiMedalOutline, mdiHandshakeOutline, mdiScaleBalance, mdiLeaf];
|
||||
|
||||
export default function VisionMissionValuesPage() {
|
||||
return (
|
||||
<>
|
||||
<MarketingSeo
|
||||
title="Vision Mission and Values"
|
||||
description="Vision, mission, values, and business principles that define Alem Desta Coffee Export."
|
||||
/>
|
||||
<MarketingLayout>
|
||||
<MarketingPageHero
|
||||
eyebrow="Vision, mission & values"
|
||||
title="A long-term export business built on trust, quality, and partnership"
|
||||
description="International buyers need more than attractive language. They need a clear statement of vision, mission, principles, and values that signal operational maturity."
|
||||
image="https://images.unsplash.com/photo-1442512595331-e89e73853f31?auto=format&fit=crop&w=1400&q=80"
|
||||
/>
|
||||
|
||||
<MarketingSection title="Vision" description="To become a globally trusted Ethiopian coffee exporter known for quality, traceability, and long-term partnerships." />
|
||||
<MarketingSection title="Mission" description="To export premium Ethiopian coffee with strict quality control, transparency, and sustainable partnerships." className="pt-0" />
|
||||
|
||||
<MarketingSection
|
||||
eyebrow="Core values"
|
||||
title="The values that guide how we source, communicate, and deliver"
|
||||
centered
|
||||
className="bg-[#F8FAF4]"
|
||||
>
|
||||
<div className="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
{values.map((item, index) => (
|
||||
<div key={item.title} className="rounded-[1.75rem] border border-[#1F3A2D]/8 bg-white p-7 shadow-[0_20px_65px_rgba(20,38,29,0.06)]">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[#F4F1E6] text-[#1F3A2D]">
|
||||
<BaseIcon path={icons[index]} />
|
||||
</div>
|
||||
<h3 className="font-brand-display mt-5 text-2xl text-[#1F3A2D]">{item.title}</h3>
|
||||
<p className="mt-4 text-sm leading-7 text-[#556257]">{item.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</MarketingSection>
|
||||
|
||||
<MarketingSection
|
||||
eyebrow="Principles"
|
||||
title="Execution principles buyers can recognize immediately"
|
||||
description="These principles turn brand values into supplier behavior."
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{principles.map((item) => (
|
||||
<div key={item} className="rounded-[1.5rem] border border-[#1F3A2D]/8 bg-white px-5 py-4 text-sm font-medium text-[#1F3A2D] shadow-[0_18px_60px_rgba(20,38,29,0.05)]">
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</MarketingSection>
|
||||
</MarketingLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
VisionMissionValuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user