This commit is contained in:
8
.env
8
.env
@@ -10,3 +10,11 @@ NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
|
||||
# GlitchTip (Sentry protocol)
|
||||
SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@errors.infra.mintel.me/1
|
||||
NEXT_PUBLIC_SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@klz-cables.com/errors/1
|
||||
|
||||
# SMTP Configuration
|
||||
MAIL_HOST=smtp.eu.mailgun.org
|
||||
MAIL_PORT=587
|
||||
MAIL_USERNAME=postmaster@mg.mintel.me
|
||||
MAIL_PASSWORD=4592fcb94599ee1a45b4ac2386fd0a64-102c75d8-ca2870e6
|
||||
MAIL_FROM=KLZ Cables <postmaster@mg.mintel.me>
|
||||
MAIL_RECIPIENTS=marc@cablecreations.de,info@klz-cables.com
|
||||
|
||||
@@ -3,7 +3,8 @@ import { getTranslations } from 'next-intl/server';
|
||||
import { Metadata } from 'next';
|
||||
import JsonLd from '@/components/JsonLd';
|
||||
import { getBreadcrumbSchema, SITE_URL, LOGO_URL } from '@/lib/schema';
|
||||
import { Section, Container, Button, Heading, Card, Input, Textarea, Label } from '@/components/ui';
|
||||
import { Section, Container, Heading } from '@/components/ui';
|
||||
import ContactForm from '@/components/ContactForm';
|
||||
|
||||
interface ContactPageProps {
|
||||
params: {
|
||||
@@ -184,54 +185,7 @@ export default function ContactPage() {
|
||||
|
||||
{/* Contact Form */}
|
||||
<div className="lg:col-span-7">
|
||||
<Card className="p-6 md:p-12 rounded-2xl md:rounded-[40px] border-none shadow-2xl animate-slide-up">
|
||||
<Heading level={3} subtitle={t('form.subtitle')} className="mb-6 md:mb-10">
|
||||
{t('form.title')}
|
||||
</Heading>
|
||||
<form className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-8">
|
||||
<div className="space-y-1 md:space-y-2">
|
||||
<Label htmlFor="name">{t('form.name')}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
autoComplete="name"
|
||||
enterKeyHint="next"
|
||||
placeholder={t('form.namePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1 md:space-y-2">
|
||||
<Label htmlFor="email">{t('form.email')}</Label>
|
||||
<Input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
autoComplete="email"
|
||||
inputMode="email"
|
||||
enterKeyHint="next"
|
||||
placeholder={t('form.emailPlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2 space-y-1 md:space-y-2">
|
||||
<Label htmlFor="message">{t('form.message')}</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
name="message"
|
||||
rows={4}
|
||||
enterKeyHint="send"
|
||||
placeholder={t('form.messagePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2 pt-2 md:pt-4">
|
||||
<Button type="submit" variant="saturated" size="lg" className="w-full shadow-xl shadow-saturated/20 md:h-16 md:px-10 md:text-xl active:scale-[0.98] transition-transform">
|
||||
{t('form.submit')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
<ContactForm />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
34
app/actions/contact.ts
Normal file
34
app/actions/contact.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
"use server";
|
||||
|
||||
import { sendEmail } from "@/lib/mail/mailer";
|
||||
import ContactEmail from "@/components/emails/ContactEmail";
|
||||
import React from "react";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
export async function sendContactFormAction(formData: FormData) {
|
||||
const name = formData.get("name") as string;
|
||||
const email = formData.get("email") as string;
|
||||
const message = formData.get("message") as string;
|
||||
const productName = formData.get("productName") as string | null;
|
||||
|
||||
if (!name || !email || !message) {
|
||||
return { success: false, error: "Missing required fields" };
|
||||
}
|
||||
|
||||
const subject = productName
|
||||
? `Product Inquiry: ${productName}`
|
||||
: "New Contact Form Submission";
|
||||
|
||||
const result = await sendEmail({
|
||||
subject,
|
||||
template: React.createElement(ContactEmail, {
|
||||
name,
|
||||
email,
|
||||
message,
|
||||
productName: productName || undefined,
|
||||
subject,
|
||||
}),
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
128
components/ContactForm.tsx
Normal file
128
components/ContactForm.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Button, Heading, Card, Input, Textarea, Label } from '@/components/ui';
|
||||
import { sendContactFormAction } from '@/app/actions/contact';
|
||||
import { useAnalytics } from '@/components/analytics/useAnalytics';
|
||||
|
||||
export default function ContactForm() {
|
||||
const t = useTranslations('Contact');
|
||||
const { trackEvent } = useAnalytics();
|
||||
const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
setStatus('submitting');
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
try {
|
||||
const result = await sendContactFormAction(formData);
|
||||
if (result.success) {
|
||||
trackEvent('contact_form_submission', {
|
||||
form_type: 'general',
|
||||
email: formData.get('email') as string,
|
||||
});
|
||||
setStatus('success');
|
||||
(e.target as HTMLFormElement).reset();
|
||||
} else {
|
||||
setStatus('error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
setStatus('error');
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'success') {
|
||||
return (
|
||||
<Card className="p-6 md:p-12 rounded-2xl md:rounded-[40px] border-none shadow-2xl text-center">
|
||||
<div className="w-20 h-20 bg-accent rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg shadow-accent/20">
|
||||
<svg className="w-10 h-10 text-primary-dark" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<Heading level={3} className="mb-4">
|
||||
{t('form.successTitle') || 'Message Sent!'}
|
||||
</Heading>
|
||||
<p className="text-text-secondary text-lg mb-8">
|
||||
{t('form.successDesc') || 'Thank you for your message. We will get back to you as soon as possible.'}
|
||||
</p>
|
||||
<Button onClick={() => setStatus('idle')} variant="saturated">
|
||||
{t('form.sendAnother') || 'Send another message'}
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-6 md:p-12 rounded-2xl md:rounded-[40px] border-none shadow-2xl animate-slide-up">
|
||||
<Heading level={3} subtitle={t('form.subtitle')} className="mb-6 md:mb-10">
|
||||
{t('form.title')}
|
||||
</Heading>
|
||||
<form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-8">
|
||||
<div className="space-y-1 md:space-y-2">
|
||||
<Label htmlFor="name">{t('form.name')}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
autoComplete="name"
|
||||
enterKeyHint="next"
|
||||
placeholder={t('form.namePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1 md:space-y-2">
|
||||
<Label htmlFor="email">{t('form.email')}</Label>
|
||||
<Input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
autoComplete="email"
|
||||
inputMode="email"
|
||||
enterKeyHint="next"
|
||||
placeholder={t('form.emailPlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2 space-y-1 md:space-y-2">
|
||||
<Label htmlFor="message">{t('form.message')}</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
name="message"
|
||||
rows={4}
|
||||
enterKeyHint="send"
|
||||
placeholder={t('form.messagePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{status === 'error' && (
|
||||
<div className="md:col-span-2 text-red-500 text-sm font-bold">
|
||||
{t('form.error') || 'An error occurred. Please try again later.'}
|
||||
</div>
|
||||
)}
|
||||
<div className="md:col-span-2 pt-2 md:pt-4">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="saturated"
|
||||
size="lg"
|
||||
disabled={status === 'submitting'}
|
||||
className="w-full shadow-xl shadow-saturated/20 md:h-16 md:px-10 md:text-xl active:scale-[0.98] transition-transform"
|
||||
>
|
||||
{status === 'submitting' ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<svg className="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{t('form.submitting') || 'Sending...'}
|
||||
</span>
|
||||
) : t('form.submit')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Input, Textarea, Button } from '@/components/ui';
|
||||
import { sendContactFormAction } from '@/app/actions/contact';
|
||||
import { useAnalytics } from '@/components/analytics/useAnalytics';
|
||||
|
||||
interface RequestQuoteFormProps {
|
||||
productName: string;
|
||||
@@ -10,6 +12,7 @@ interface RequestQuoteFormProps {
|
||||
|
||||
export default function RequestQuoteForm({ productName }: RequestQuoteFormProps) {
|
||||
const t = useTranslations('Products.form');
|
||||
const { trackEvent } = useAnalytics();
|
||||
const [email, setEmail] = useState('');
|
||||
const [request, setRequest] = useState('');
|
||||
const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
|
||||
@@ -18,15 +21,30 @@ export default function RequestQuoteForm({ productName }: RequestQuoteFormProps)
|
||||
e.preventDefault();
|
||||
setStatus('submitting');
|
||||
|
||||
// Simulate API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const formData = new FormData();
|
||||
formData.append('name', 'Product Inquiry'); // Default name for product inquiries
|
||||
formData.append('email', email);
|
||||
formData.append('message', request);
|
||||
formData.append('productName', productName);
|
||||
|
||||
// Here you would typically send the data to your backend
|
||||
console.log('Form submitted:', { productName, email, request });
|
||||
|
||||
setStatus('success');
|
||||
setEmail('');
|
||||
setRequest('');
|
||||
try {
|
||||
const result = await sendContactFormAction(formData);
|
||||
if (result.success) {
|
||||
trackEvent('contact_form_submission', {
|
||||
form_type: 'product_quote',
|
||||
product_name: productName,
|
||||
email: email,
|
||||
});
|
||||
setStatus('success');
|
||||
setEmail('');
|
||||
setRequest('');
|
||||
} else {
|
||||
setStatus('error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
setStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
if (status === 'success') {
|
||||
|
||||
115
components/emails/ContactEmail.tsx
Normal file
115
components/emails/ContactEmail.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
Body,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Hr,
|
||||
Html,
|
||||
Preview,
|
||||
Section,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
interface ContactEmailProps {
|
||||
name: string;
|
||||
email: string;
|
||||
message: string;
|
||||
subject?: string;
|
||||
productName?: string;
|
||||
}
|
||||
|
||||
export const ContactEmail = ({
|
||||
name,
|
||||
email,
|
||||
message,
|
||||
subject = "New Contact Form Submission",
|
||||
productName,
|
||||
}: ContactEmailProps) => (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{subject}</Preview>
|
||||
<Body style={main}>
|
||||
<Container style={container}>
|
||||
<Heading style={h1}>{subject}</Heading>
|
||||
{productName && (
|
||||
<Text style={text}>
|
||||
<strong>Product Inquiry:</strong> {productName}
|
||||
</Text>
|
||||
)}
|
||||
<Section style={section}>
|
||||
<Text style={text}>
|
||||
<strong>Name:</strong> {name}
|
||||
</Text>
|
||||
<Text style={text}>
|
||||
<strong>Email:</strong> {email}
|
||||
</Text>
|
||||
<Hr style={hr} />
|
||||
<Text style={text}>
|
||||
<strong>Message:</strong>
|
||||
</Text>
|
||||
<Text style={messageText}>{message}</Text>
|
||||
</Section>
|
||||
<Hr style={hr} />
|
||||
<Text style={footer}>
|
||||
This email was sent from the contact form on klz-cables.com
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
|
||||
export default ContactEmail;
|
||||
|
||||
const main = {
|
||||
backgroundColor: "#f6f9fc",
|
||||
fontFamily:
|
||||
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
|
||||
};
|
||||
|
||||
const container = {
|
||||
backgroundColor: "#ffffff",
|
||||
margin: "0 auto",
|
||||
padding: "20px 0 48px",
|
||||
marginBottom: "64px",
|
||||
};
|
||||
|
||||
const section = {
|
||||
padding: "0 48px",
|
||||
};
|
||||
|
||||
const h1 = {
|
||||
color: "#333",
|
||||
fontSize: "24px",
|
||||
fontWeight: "bold",
|
||||
padding: "0 48px",
|
||||
margin: "30px 0",
|
||||
};
|
||||
|
||||
const text = {
|
||||
color: "#333",
|
||||
fontSize: "16px",
|
||||
lineHeight: "24px",
|
||||
textAlign: "left" as const,
|
||||
};
|
||||
|
||||
const messageText = {
|
||||
...text,
|
||||
backgroundColor: "#f4f4f4",
|
||||
padding: "15px",
|
||||
borderRadius: "4px",
|
||||
whiteSpace: "pre-wrap" as const,
|
||||
};
|
||||
|
||||
const hr = {
|
||||
borderColor: "#e6ebf1",
|
||||
margin: "20px 0",
|
||||
};
|
||||
|
||||
const footer = {
|
||||
color: "#8898aa",
|
||||
fontSize: "12px",
|
||||
lineHeight: "16px",
|
||||
textAlign: "center" as const,
|
||||
marginTop: "20px",
|
||||
};
|
||||
41
lib/mail/mailer.ts
Normal file
41
lib/mail/mailer.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import nodemailer from "nodemailer";
|
||||
import { render } from "@react-email/components";
|
||||
import { ReactElement } from "react";
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.MAIL_HOST,
|
||||
port: Number(process.env.MAIL_PORT),
|
||||
secure: Number(process.env.MAIL_PORT) === 465,
|
||||
auth: {
|
||||
user: process.env.MAIL_USERNAME,
|
||||
pass: process.env.MAIL_PASSWORD,
|
||||
},
|
||||
});
|
||||
|
||||
interface SendEmailOptions {
|
||||
to?: string | string[];
|
||||
subject: string;
|
||||
template: ReactElement;
|
||||
}
|
||||
|
||||
export async function sendEmail({ to, subject, template }: SendEmailOptions) {
|
||||
const html = await render(template);
|
||||
|
||||
const recipients = to || process.env.MAIL_RECIPIENTS?.split(",") || [];
|
||||
|
||||
const mailOptions = {
|
||||
from: process.env.MAIL_FROM,
|
||||
to: recipients,
|
||||
subject,
|
||||
html,
|
||||
};
|
||||
|
||||
try {
|
||||
const info = await transporter.sendMail(mailOptions);
|
||||
console.log("Email sent: %s", info.messageId);
|
||||
return { success: true, messageId: info.messageId };
|
||||
} catch (error) {
|
||||
console.error("Error sending email:", error);
|
||||
return { success: false, error };
|
||||
}
|
||||
}
|
||||
3248
package-lock.json
generated
3248
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@react-email/components": "^1.0.6",
|
||||
"@react-pdf/renderer": "^4.3.2",
|
||||
"@sentry/nextjs": "^8.55.0",
|
||||
"@swc/helpers": "^0.5.18",
|
||||
@@ -17,9 +18,11 @@
|
||||
"next-i18next": "^15.4.3",
|
||||
"next-intl": "^4.6.1",
|
||||
"next-mdx-remote": "^5.0.0",
|
||||
"nodemailer": "^7.0.12",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-email": "^5.2.5",
|
||||
"redis": "^4.7.1",
|
||||
"resend": "^3.5.0",
|
||||
"schema-dts": "^1.1.5",
|
||||
@@ -32,6 +35,7 @@
|
||||
"@tailwindcss/cli": "^4.1.18",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@types/node": "^22.19.3",
|
||||
"@types/nodemailer": "^7.0.5",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/sharp": "^0.31.1",
|
||||
|
||||
Reference in New Issue
Block a user