diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index ee1367b2..f9e12ab8 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -7,10 +7,8 @@ import AnalyticsShell from '@/components/analytics/AnalyticsShell'; import { Metadata, Viewport } from 'next'; import { NextIntlClientProvider } from 'next-intl'; import { getMessages } from 'next-intl/server'; -import { Suspense } from 'react'; import '../../styles/globals.css'; import { SITE_URL } from '@/lib/schema'; -import { config } from '@/lib/config'; import FeedbackClientWrapper from '@/components/FeedbackClientWrapper'; import { setRequestLocale } from 'next-intl/server'; import { Inter } from 'next/font/google'; @@ -61,6 +59,7 @@ export const viewport: Viewport = { themeColor: '#001a4d', }; +import AutoBrochureModal from '@/components/AutoBrochureModal'; export default async function Layout(props: { children: React.ReactNode; params: Promise<{ locale: string }>; @@ -77,7 +76,7 @@ export default async function Layout(props: { let messages: Record = {}; try { messages = await getMessages(); - } catch (error) { + } catch { messages = {}; } @@ -161,6 +160,8 @@ export default async function Layout(props: { + + diff --git a/app/actions/brochure.ts b/app/actions/brochure.ts index b44a76ed..c5397624 100644 --- a/app/actions/brochure.ts +++ b/app/actions/brochure.ts @@ -3,76 +3,112 @@ import { getServerAppServices } from '@/lib/services/create-services.server'; export async function requestBrochureAction(formData: FormData) { - const services = getServerAppServices(); - const logger = services.logger.child({ action: 'requestBrochureAction' }); + const services = getServerAppServices(); + const logger = services.logger.child({ action: 'requestBrochureAction' }); - const { headers } = await import('next/headers'); - const requestHeaders = await headers(); + const { headers } = await import('next/headers'); + const requestHeaders = await headers(); - if ('setServerContext' in services.analytics) { - (services.analytics as any).setServerContext({ - userAgent: requestHeaders.get('user-agent') || undefined, - language: requestHeaders.get('accept-language')?.split(',')[0] || undefined, - referrer: requestHeaders.get('referer') || undefined, - ip: requestHeaders.get('x-forwarded-for')?.split(',')[0] || undefined, - }); - } + if ('setServerContext' in services.analytics) { + (services.analytics as any).setServerContext({ + userAgent: requestHeaders.get('user-agent') || undefined, + language: requestHeaders.get('accept-language')?.split(',')[0] || undefined, + referrer: requestHeaders.get('referer') || undefined, + ip: requestHeaders.get('x-forwarded-for')?.split(',')[0] || undefined, + }); + } - services.analytics.track('brochure-request-attempt'); + services.analytics.track('brochure-request-attempt'); - const email = formData.get('email') as string; - const locale = (formData.get('locale') as string) || 'en'; + const email = formData.get('email') as string; + const locale = (formData.get('locale') as string) || 'en'; - if (!email) { - logger.warn('Missing email in brochure request'); - return { success: false, error: 'Missing email address' }; - } + if (!email) { + logger.warn('Missing email in brochure request'); + return { success: false, error: 'Missing email address' }; + } - // Basic email validation - if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { - return { success: false, error: 'Invalid email address' }; - } + // Basic email validation + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + return { success: false, error: 'Invalid email address' }; + } - // 1. Save to CMS - try { - const { getPayload } = await import('payload'); - const configPromise = (await import('@payload-config')).default; - const payload = await getPayload({ config: configPromise }); + // 1. Save to CMS + try { + const { getPayload } = await import('payload'); + const configPromise = (await import('@payload-config')).default; + const payload = await getPayload({ config: configPromise }); - await payload.create({ - collection: 'form-submissions', - data: { - name: email.split('@')[0], - email, - message: `Brochure download request (${locale})`, - type: 'brochure_download' as any, - }, - }); - - logger.info('Successfully saved brochure request to Payload CMS', { email }); - } catch (error) { - logger.error('Failed to store brochure request in Payload CMS', { error }); - services.errors.captureException(error, { action: 'payload_store_brochure_request' }); - } - - // 2. Notify via Gotify - try { - await services.notifications.notify({ - title: 'πŸ“‘ Brochure Download Request', - message: `New brochure download request from ${email} (${locale})`, - priority: 3, - }); - } catch (error) { - logger.error('Failed to send notification', { error }); - } - - // 3. Track success - services.analytics.track('brochure-request-success', { - locale, + await payload.create({ + collection: 'form-submissions', + data: { + name: email.split('@')[0], + email, + message: `Brochure download request (${locale})`, + type: 'brochure_download' as any, + }, }); - // Return the brochure URL - const brochureUrl = `/brochure/klz-product-catalog-${locale}.pdf`; + logger.info('Successfully saved brochure request to Payload CMS', { email }); + } catch (error) { + logger.error('Failed to store brochure request in Payload CMS', { error }); + services.errors.captureException(error, { action: 'payload_store_brochure_request' }); + } - return { success: true, brochureUrl }; + // 2. Notify via Gotify + try { + await services.notifications.notify({ + title: 'πŸ“‘ Brochure Download Request', + message: `New brochure download request from ${email} (${locale})`, + priority: 3, + }); + } catch (error) { + logger.error('Failed to send notification', { error }); + } + + // 3. Send Brochure via Email + const brochureUrl = `https://klz-cables.com/brochure/klz-product-catalog-${locale}.pdf`; + + try { + const { sendEmail } = await import('@/lib/mail/mailer'); + const { render } = await import('@mintel/mail'); + const React = await import('react'); + const { BrochureDeliveryEmail } = await import('@/components/emails/BrochureDeliveryEmail'); + + const html = await render( + React.createElement(BrochureDeliveryEmail, { + email, + brochureUrl, + locale: locale as 'en' | 'de', + }), + ); + + const emailResult = await sendEmail({ + to: email, + subject: locale === 'de' ? 'Ihr KLZ Kabelkatalog' : 'Your KLZ Cable Catalog', + html, + }); + + if (emailResult.success) { + logger.info('Brochure email sent successfully', { email }); + } else { + logger.error('Failed to send brochure email', { error: emailResult.error, email }); + services.errors.captureException(new Error(`Brochure email failed: ${emailResult.error}`), { + action: 'requestBrochureAction_email', + email, + }); + return { success: false, error: 'Failed to send email. Please try again later.' }; + } + } catch (error) { + logger.error('Exception while sending brochure email', { error }); + return { success: false, error: 'Failed to send email. Please try again later.' }; + } + + // 4. Track success + services.analytics.track('brochure-request-success', { + locale, + delivery_method: 'email', + }); + + return { success: true }; } diff --git a/components/AutoBrochureModal.tsx b/components/AutoBrochureModal.tsx new file mode 100644 index 00000000..2622dd71 --- /dev/null +++ b/components/AutoBrochureModal.tsx @@ -0,0 +1,28 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import dynamic from 'next/dynamic'; + +const BrochureModal = dynamic(() => import('./BrochureModal'), { ssr: false }); + +export default function AutoBrochureModal() { + const [isOpen, setIsOpen] = useState(false); + + useEffect(() => { + // Check if user has already seen or interacted with the modal + const hasSeenModal = localStorage.getItem('klz_brochure_modal_seen'); + + if (!hasSeenModal) { + // Auto-open after 5 seconds to not interrupt immediate page load + const timer = setTimeout(() => { + setIsOpen(true); + // Mark as seen so it doesn't bother them again on next page load + localStorage.setItem('klz_brochure_modal_seen', 'true'); + }, 5000); + + return () => clearTimeout(timer); + } + }, []); + + return setIsOpen(false)} />; +} diff --git a/components/BrochureCTA.tsx b/components/BrochureCTA.tsx index 4901c008..f457d276 100644 --- a/components/BrochureCTA.tsx +++ b/components/BrochureCTA.tsx @@ -9,8 +9,8 @@ import { useAnalytics } from './analytics/useAnalytics'; import { AnalyticsEvents } from './analytics/analytics-events'; interface Props { - className?: string; - compact?: boolean; + className?: string; + compact?: boolean; } /** @@ -19,235 +19,440 @@ interface Props { * No direct download link is exposed anywhere. */ export default function BrochureCTA({ className, compact = false }: Props) { - const t = useTranslations('Brochure'); - const locale = useLocale(); - const { trackEvent } = useAnalytics(); - const formRef = useRef(null); + const t = useTranslations('Brochure'); + const locale = useLocale(); + const { trackEvent } = useAnalytics(); + const formRef = useRef(null); - const [open, setOpen] = useState(false); - const [mounted, setMounted] = useState(false); - const [phase, setPhase] = useState<'form' | 'loading' | 'success' | 'error'>('form'); - const [url, setUrl] = useState(''); - const [err, setErr] = useState(''); + const [open, setOpen] = useState(false); + const [mounted, setMounted] = useState(false); + const [phase, setPhase] = useState<'form' | 'loading' | 'success' | 'error'>('form'); + const [err, setErr] = useState(''); - useEffect(() => { setMounted(true); }, []); + useEffect(() => { + setMounted(true); + }, []); - useEffect(() => { - if (!open) return; - const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') closeModal(); }; - document.addEventListener('keydown', onKey); - document.body.style.overflow = 'hidden'; - return () => { - document.removeEventListener('keydown', onKey); - document.body.style.overflow = ''; - }; - }, [open]); + function openModal() { + setOpen(true); + } + function closeModal() { + setOpen(false); + setPhase('form'); + setErr(''); + } - function openModal() { setOpen(true); } - function closeModal() { - setOpen(false); - setPhase('form'); - setUrl(''); - setErr(''); + useEffect(() => { + if (!open) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') closeModal(); + }; + document.addEventListener('keydown', onKey); + document.body.style.overflow = 'hidden'; + return () => { + document.removeEventListener('keydown', onKey); + document.body.style.overflow = ''; + }; + }, [open]); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!formRef.current) return; + setPhase('loading'); + + const fd = new FormData(formRef.current); + fd.set('locale', locale); + + try { + const res = await requestBrochureAction(fd); + if (res.success) { + setPhase('success'); + trackEvent(AnalyticsEvents.DOWNLOAD, { + file_name: `klz-product-catalog-${locale}.pdf`, + file_type: 'brochure', + location: 'brochure_modal', + }); + } else { + setErr(res.error || 'Error'); + setPhase('error'); + } + } catch { + setErr('Network error'); + setPhase('error'); } + } - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - if (!formRef.current) return; - setPhase('loading'); + // ── Trigger Button ───────────────────────────────────────────────── + const trigger = ( +
+ +
+ ); - {/* Icon */} - - - - - - - {/* Labels */} - - PDF Katalog - - {t('ctaTitle')} - - - - {/* Arrow */} - - - - - - - - ); - - // ── Modal ────────────────────────────────────────────────────────── - const modal = mounted && open ? createPortal( -
+ // ── Modal ────────────────────────────────────────────────────────── + const modal = + mounted && open + ? createPortal( +
{/* Backdrop */}
{/* Panel */} -
+
+ {/* Green top bar */} +
- {/* Green top bar */} -
+ {/* Close */} + - {/* Close */} - - -
- {/* Header */} -
-
- - - -
-

- {t('title')} -

-

- {t('subtitle')} -

-
- - {phase === 'success' ? ( -
-
-
- - - -
-
-

{t('successTitle')}

-

{t('successDesc')}

-
-
- - - - - {t('download')} - -
- ) : ( -
- - - - {phase === 'error' && err && ( -

{err}

- )} - - - -

- {t('privacyNote')} -

-
- )} +
+

+ {t('title')} +

+

+ {t('subtitle')} +

-
-
, - document.body, - ) : null; - return ( - <> - {trigger} - {modal} - - ); + {phase === 'success' ? ( +
+
+
+ + + +
+
+

+ {locale === 'de' ? 'Erfolgreich gesendet' : 'Successfully sent'} +

+

+ {locale === 'de' + ? 'Bitte prΓΌfen Sie Ihren Posteingang.' + : 'Please check your inbox.'} +

+
+
+ +
+ ) : ( +
+ + + + {phase === 'error' && err && ( +

+ {err} +

+ )} + + + +

+ {t('privacyNote')} +

+
+ )} +
+
+
, + document.body, + ) + : null; + + return ( + <> + {trigger} + {modal} + + ); } diff --git a/components/BrochureModal.tsx b/components/BrochureModal.tsx index 1b3a0a32..266dc754 100644 --- a/components/BrochureModal.tsx +++ b/components/BrochureModal.tsx @@ -9,203 +9,216 @@ import { useAnalytics } from './analytics/useAnalytics'; import { AnalyticsEvents } from './analytics/analytics-events'; interface BrochureModalProps { - isOpen: boolean; - onClose: () => void; + isOpen: boolean; + onClose: () => void; } export default function BrochureModal({ isOpen, onClose }: BrochureModalProps) { - const t = useTranslations('Brochure'); - const locale = useLocale(); - const { trackEvent } = useAnalytics(); - const formRef = useRef(null); - const [mounted, setMounted] = useState(false); + const t = useTranslations('Brochure'); + const locale = useLocale(); + const { trackEvent } = useAnalytics(); + const formRef = useRef(null); + const [state, setState] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle'); + const [errorMsg, setErrorMsg] = useState(''); - const [state, setState] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle'); - const [brochureUrl, setBrochureUrl] = useState(null); - const [errorMsg, setErrorMsg] = useState(''); - - // Mount guard for SSR/portal - useEffect(() => { - setMounted(true); - }, []); - - // Close on escape + lock scroll - useEffect(() => { - const handleEsc = (e: KeyboardEvent) => { - if (e.key === 'Escape') onClose(); - }; - if (isOpen) { - document.addEventListener('keydown', handleEsc); - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = ''; - } - return () => { - document.removeEventListener('keydown', handleEsc); - document.body.style.overflow = ''; - }; - }, [isOpen, onClose]); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!formRef.current) return; - - setState('submitting'); - setErrorMsg(''); - - try { - const formData = new FormData(formRef.current); - formData.set('locale', locale); - - const result = await requestBrochureAction(formData); - - if (result.success && result.brochureUrl) { - setState('success'); - setBrochureUrl(result.brochureUrl); - trackEvent(AnalyticsEvents.DOWNLOAD, { - file_name: `klz-product-catalog-${locale}.pdf`, - file_type: 'brochure', - location: 'brochure_modal', - }); - } else { - setState('error'); - setErrorMsg(result.error || 'Something went wrong'); - } - } catch { - setState('error'); - setErrorMsg('Network error'); - } + // Close on escape + lock scroll + useEffect(() => { + const handleEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); }; - - const handleClose = () => { - setState('idle'); - setBrochureUrl(null); - setErrorMsg(''); - onClose(); + if (isOpen) { + document.addEventListener('keydown', handleEsc); + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + return () => { + document.removeEventListener('keydown', handleEsc); + document.body.style.overflow = ''; }; + }, [isOpen, onClose]); - if (!mounted || !isOpen) return null; + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!formRef.current) return; - const modal = ( -
{ + setState('idle'); + setErrorMsg(''); + onClose(); + }; + + if (!isOpen) return null; + + const modal = ( +
+ {/* Backdrop */} +