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)
|
# GlitchTip (Sentry protocol)
|
||||||
SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@errors.infra.mintel.me/1
|
SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@errors.infra.mintel.me/1
|
||||||
NEXT_PUBLIC_SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@klz-cables.com/errors/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 { Metadata } from 'next';
|
||||||
import JsonLd from '@/components/JsonLd';
|
import JsonLd from '@/components/JsonLd';
|
||||||
import { getBreadcrumbSchema, SITE_URL, LOGO_URL } from '@/lib/schema';
|
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 {
|
interface ContactPageProps {
|
||||||
params: {
|
params: {
|
||||||
@@ -184,54 +185,7 @@ export default function ContactPage() {
|
|||||||
|
|
||||||
{/* Contact Form */}
|
{/* Contact Form */}
|
||||||
<div className="lg:col-span-7">
|
<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">
|
<ContactForm />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</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 React, { useState } from 'react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { Input, Textarea, Button } from '@/components/ui';
|
import { Input, Textarea, Button } from '@/components/ui';
|
||||||
|
import { sendContactFormAction } from '@/app/actions/contact';
|
||||||
|
import { useAnalytics } from '@/components/analytics/useAnalytics';
|
||||||
|
|
||||||
interface RequestQuoteFormProps {
|
interface RequestQuoteFormProps {
|
||||||
productName: string;
|
productName: string;
|
||||||
@@ -10,6 +12,7 @@ interface RequestQuoteFormProps {
|
|||||||
|
|
||||||
export default function RequestQuoteForm({ productName }: RequestQuoteFormProps) {
|
export default function RequestQuoteForm({ productName }: RequestQuoteFormProps) {
|
||||||
const t = useTranslations('Products.form');
|
const t = useTranslations('Products.form');
|
||||||
|
const { trackEvent } = useAnalytics();
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [request, setRequest] = useState('');
|
const [request, setRequest] = useState('');
|
||||||
const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
|
const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
|
||||||
@@ -18,15 +21,30 @@ export default function RequestQuoteForm({ productName }: RequestQuoteFormProps)
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setStatus('submitting');
|
setStatus('submitting');
|
||||||
|
|
||||||
// Simulate API call
|
const formData = new FormData();
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
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
|
try {
|
||||||
console.log('Form submitted:', { productName, email, request });
|
const result = await sendContactFormAction(formData);
|
||||||
|
if (result.success) {
|
||||||
setStatus('success');
|
trackEvent('contact_form_submission', {
|
||||||
setEmail('');
|
form_type: 'product_quote',
|
||||||
setRequest('');
|
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') {
|
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": {
|
"dependencies": {
|
||||||
|
"@react-email/components": "^1.0.6",
|
||||||
"@react-pdf/renderer": "^4.3.2",
|
"@react-pdf/renderer": "^4.3.2",
|
||||||
"@sentry/nextjs": "^8.55.0",
|
"@sentry/nextjs": "^8.55.0",
|
||||||
"@swc/helpers": "^0.5.18",
|
"@swc/helpers": "^0.5.18",
|
||||||
@@ -17,9 +18,11 @@
|
|||||||
"next-i18next": "^15.4.3",
|
"next-i18next": "^15.4.3",
|
||||||
"next-intl": "^4.6.1",
|
"next-intl": "^4.6.1",
|
||||||
"next-mdx-remote": "^5.0.0",
|
"next-mdx-remote": "^5.0.0",
|
||||||
|
"nodemailer": "^7.0.12",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-email": "^5.2.5",
|
||||||
"redis": "^4.7.1",
|
"redis": "^4.7.1",
|
||||||
"resend": "^3.5.0",
|
"resend": "^3.5.0",
|
||||||
"schema-dts": "^1.1.5",
|
"schema-dts": "^1.1.5",
|
||||||
@@ -32,6 +35,7 @@
|
|||||||
"@tailwindcss/cli": "^4.1.18",
|
"@tailwindcss/cli": "^4.1.18",
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@types/node": "^22.19.3",
|
"@types/node": "^22.19.3",
|
||||||
|
"@types/nodemailer": "^7.0.5",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/sharp": "^0.31.1",
|
"@types/sharp": "^0.31.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user