Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Failing after 2m15s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
212 lines
9.7 KiB
TypeScript
212 lines
9.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useRef, useEffect } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { useTranslations, useLocale } from 'next-intl';
|
|
import { cn } from '@/components/ui/utils';
|
|
import { requestBrochureAction } from '@/app/actions/brochure';
|
|
import { useAnalytics } from './analytics/useAnalytics';
|
|
import { AnalyticsEvents } from './analytics/analytics-events';
|
|
|
|
interface BrochureModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export default function BrochureModal({ isOpen, onClose }: BrochureModalProps) {
|
|
const t = useTranslations('Brochure');
|
|
const locale = useLocale();
|
|
const { trackEvent } = useAnalytics();
|
|
const formRef = useRef<HTMLFormElement>(null);
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
const [state, setState] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
|
|
const [brochureUrl, setBrochureUrl] = useState<string | null>(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');
|
|
}
|
|
};
|
|
|
|
const handleClose = () => {
|
|
setState('idle');
|
|
setBrochureUrl(null);
|
|
setErrorMsg('');
|
|
onClose();
|
|
};
|
|
|
|
if (!mounted || !isOpen) return null;
|
|
|
|
const modal = (
|
|
<div
|
|
className="fixed inset-0 z-[9999] flex items-center justify-center p-4"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
>
|
|
{/* Backdrop */}
|
|
<div
|
|
className="absolute inset-0 bg-black/70 backdrop-blur-sm"
|
|
onClick={handleClose}
|
|
aria-hidden="true"
|
|
/>
|
|
|
|
{/* Modal Panel */}
|
|
<div className="relative z-10 w-full max-w-md rounded-[28px] bg-[#000d26] border border-white/10 shadow-[0_40px_80px_rgba(0,0,0,0.6)] overflow-hidden">
|
|
{/* Accent bar at top */}
|
|
<div className="h-1 w-full bg-gradient-to-r from-[#82ed20] via-[#5cb516] to-[#82ed20]" />
|
|
|
|
{/* Close Button */}
|
|
<button
|
|
type="button"
|
|
onClick={handleClose}
|
|
className="absolute top-4 right-4 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-white/5 text-white/40 hover:bg-white/10 hover:text-white transition-colors"
|
|
aria-label={t('close')}
|
|
>
|
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
|
|
<div className="p-8 pt-7">
|
|
{/* Icon + Header */}
|
|
<div className="mb-7">
|
|
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[#82ed20]/10 border border-[#82ed20]/20 mb-4">
|
|
<svg className="h-6 w-6 text-[#82ed20]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
|
|
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
|
</svg>
|
|
</div>
|
|
<h2 className="text-2xl font-black text-white uppercase tracking-tight leading-none mb-2">
|
|
{t('title')}
|
|
</h2>
|
|
<p className="text-sm text-white/50 leading-relaxed">
|
|
{t('subtitle')}
|
|
</p>
|
|
</div>
|
|
|
|
{state === 'success' && brochureUrl ? (
|
|
<div>
|
|
<div className="flex items-center gap-3 mb-6 p-4 rounded-2xl bg-[#82ed20]/10 border border-[#82ed20]/20">
|
|
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-[#82ed20]/20">
|
|
<svg className="h-5 w-5 text-[#82ed20]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-bold text-[#82ed20]">{t('successTitle')}</p>
|
|
<p className="text-xs text-white/50 mt-0.5">{t('successDesc')}</p>
|
|
</div>
|
|
</div>
|
|
<a
|
|
href={brochureUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center justify-center gap-3 w-full py-4 px-6 rounded-2xl bg-[#82ed20] hover:bg-[#6dd318] text-[#000d26] font-black text-sm uppercase tracking-widest transition-colors"
|
|
>
|
|
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
{t('download')}
|
|
</a>
|
|
</div>
|
|
) : (
|
|
<form ref={formRef} onSubmit={handleSubmit}>
|
|
<div className="mb-5">
|
|
<label
|
|
htmlFor="brochure-email"
|
|
className="block text-[10px] font-black uppercase tracking-[0.2em] text-white/40 mb-2"
|
|
>
|
|
{t('emailLabel')}
|
|
</label>
|
|
<input
|
|
id="brochure-email"
|
|
name="email"
|
|
type="email"
|
|
required
|
|
autoComplete="email"
|
|
placeholder={t('emailPlaceholder')}
|
|
className="w-full rounded-xl bg-white/5 border border-white/10 px-4 py-3.5 text-white placeholder:text-white/20 text-sm font-medium focus:outline-none focus:border-[#82ed20]/40 transition-colors"
|
|
disabled={state === 'submitting'}
|
|
/>
|
|
</div>
|
|
|
|
{state === 'error' && errorMsg && (
|
|
<p className="text-red-400 text-xs mb-4 font-medium">{errorMsg}</p>
|
|
)}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={state === 'submitting'}
|
|
className={cn(
|
|
'w-full py-4 px-6 rounded-2xl font-black text-sm uppercase tracking-widest transition-colors',
|
|
state === 'submitting'
|
|
? 'bg-white/10 text-white/40 cursor-wait'
|
|
: 'bg-[#82ed20] hover:bg-[#6dd318] text-[#000d26]',
|
|
)}
|
|
>
|
|
{state === 'submitting' ? t('submitting') : t('submit')}
|
|
</button>
|
|
|
|
<p className="mt-4 text-[10px] text-white/25 text-center leading-relaxed">
|
|
{t('privacyNote')}
|
|
</p>
|
|
</form>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return createPortal(modal, document.body);
|
|
}
|