mailing
All checks were successful
Build & Deploy KLZ Cables / deploy (push) Successful in 5m8s

This commit is contained in:
2026-01-25 13:36:25 +01:00
parent ae1e0ad8a9
commit a0cfa8ef62
9 changed files with 3598 additions and 66 deletions

8
.env
View File

@@ -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

View File

@@ -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
View 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
View 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>
);
}

View File

@@ -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') {

View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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",