fix(forms): Naprawa formularza multisplit i dodanie reCAPTCHA v3 do wszystkich formularzy
This commit is contained in:
+12
-1
@@ -1,8 +1,8 @@
|
||||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { Resend } from "resend";
|
||||
import { verifyRecaptcha } from "@/lib/recaptcha";
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
@@ -19,6 +19,17 @@ const contactFormSchema = z.object({
|
||||
|
||||
export async function submitContactForm(prevState: any, formData: FormData) {
|
||||
try {
|
||||
if (process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY && process.env.RECAPTCHA_SECRET_KEY) {
|
||||
const token = formData.get('recaptchaToken') as string;
|
||||
if (!token) {
|
||||
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
|
||||
}
|
||||
const { success } = await verifyRecaptcha(token);
|
||||
if (!success) {
|
||||
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Walidacja danych z formularza
|
||||
const rawData = {
|
||||
name: formData.get("name"),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Header } from "@/components/layout/Header";
|
||||
import { TrustBar } from "@/components/layout/TrustBar";
|
||||
import { Footer } from "@/components/layout/Footer";
|
||||
import CookieBanner from '@/components/CookieBanner';
|
||||
import Script from 'next/script';
|
||||
import { CompareBar } from '@/components/shop/CompareBar';
|
||||
import { AnalyticsWrapper } from "@/components/ui/AnalyticsWrapper";
|
||||
import { CookieSettingsFloatingIcon } from "@/components/ui/CookieSettingsFloatingIcon";
|
||||
@@ -67,6 +68,12 @@ export default function RootLayout({
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
</head>
|
||||
<body className="min-h-screen flex flex-col bg-background text-foreground">
|
||||
{process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY && (
|
||||
<Script
|
||||
src={`https://www.google.com/recaptcha/api.js?render=${process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY}`}
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
)}
|
||||
<Header />
|
||||
<main className="flex-grow">{children}</main>
|
||||
<TrustBar />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { z } from 'zod';
|
||||
import { Resend } from 'resend';
|
||||
import { verifyRecaptcha } from '@/lib/recaptcha';
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
@@ -18,6 +19,17 @@ const orderSchema = z.object({
|
||||
|
||||
export async function submitOrderForm(prevState: any, formData: FormData) {
|
||||
try {
|
||||
if (process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY && process.env.RECAPTCHA_SECRET_KEY) {
|
||||
const token = formData.get('recaptchaToken') as string;
|
||||
if (!token) {
|
||||
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
|
||||
}
|
||||
const { success } = await verifyRecaptcha(token);
|
||||
if (!success) {
|
||||
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
|
||||
}
|
||||
}
|
||||
|
||||
const data = orderSchema.parse({
|
||||
name: formData.get('name'),
|
||||
phone: formData.get('phone'),
|
||||
|
||||
+15
-2
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useCartStore } from '@/lib/cart-store';
|
||||
import { useActionState, useEffect } from 'react';
|
||||
import { useActionState, useEffect, startTransition } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { submitOrderForm } from './actions';
|
||||
import { ArrowLeft, CheckCircle } from 'lucide-react';
|
||||
@@ -75,7 +75,20 @@ Wróć do sklepu
|
||||
</div>
|
||||
|
||||
{/* Formularz */}
|
||||
<form action={formAction} className="bg-white rounded-xl border border-border p-6 flex flex-col gap-4">
|
||||
<form onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
if (process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY) {
|
||||
const token = await (window as any).grecaptcha.execute(
|
||||
process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY,
|
||||
{ action: 'submit' }
|
||||
);
|
||||
formData.append('recaptchaToken', token);
|
||||
}
|
||||
startTransition(() => {
|
||||
formAction(formData);
|
||||
});
|
||||
}} className="bg-white rounded-xl border border-border p-6 flex flex-col gap-4">
|
||||
<h2 className="text-base font-bold text-primary font-heading">Twoje dane</h2>
|
||||
|
||||
{/* Hidden: produkty jako JSON */}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
import { Resend } from "resend";
|
||||
import { verifyRecaptcha } from "@/lib/recaptcha";
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
const multisplitFormSchema = z.object({
|
||||
name: z.string().min(2, "Imię musi mieć min. 2 znaki"),
|
||||
phone: z.string().min(9, "Wprowadź poprawny numer telefonu"),
|
||||
email: z.union([z.literal(''), z.string().email("Niepoprawny adres e-mail")]).optional(),
|
||||
city: z.string().min(1, "Wybierz miasto"),
|
||||
rooms: z.string().optional(),
|
||||
message: z.string().optional(),
|
||||
consent: z.boolean().refine((val) => val === true, "Zgoda RODO jest wymagana"),
|
||||
productName: z.string(),
|
||||
productSku: z.string(),
|
||||
});
|
||||
|
||||
export async function submitMultisplitForm(data: any) {
|
||||
try {
|
||||
if (process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY && process.env.RECAPTCHA_SECRET_KEY) {
|
||||
if (!data.recaptchaToken) {
|
||||
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
|
||||
}
|
||||
const { success } = await verifyRecaptcha(data.recaptchaToken);
|
||||
if (!success) {
|
||||
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
|
||||
}
|
||||
}
|
||||
|
||||
const validatedData = multisplitFormSchema.parse(data);
|
||||
|
||||
const { error } = await resend.emails.send({
|
||||
from: process.env.RESEND_FROM_EMAIL || "[email protected]",
|
||||
to: process.env.RESEND_TO_EMAIL || "[email protected]",
|
||||
replyTo: validatedData.email || undefined,
|
||||
subject: `Zapytanie o multisplit: ${validatedData.productName} (${validatedData.productSku})`,
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; background-color: #ffffff;">
|
||||
<div style="background-color: #0F2A47; padding: 20px; text-align: center;">
|
||||
<h1 style="color: #ffffff; margin: 0; font-size: 24px;">Zapytanie o multisplit</h1>
|
||||
<p style="color: #2BA8E0; margin: 5px 0 0 0; font-size: 14px;">Domena: thermcool.pl</p>
|
||||
</div>
|
||||
<div style="padding: 24px; color: #1e293b;">
|
||||
<h2 style="font-size: 18px; margin-top: 0; border-bottom: 2px solid #f1f5f9; padding-bottom: 10px;">Dane kontaktowe:</h2>
|
||||
<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
|
||||
<tr>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9; width: 120px;"><strong>Imię:</strong></td>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${validatedData.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>Telefon:</strong></td>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><a href="tel:${validatedData.phone}" style="color: #0F2A47; text-decoration: none;">${validatedData.phone}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>E-mail:</strong></td>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><a href="mailto:${validatedData.email}" style="color: #0F2A47; text-decoration: none;">${validatedData.email || 'Brak'}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>Miasto:</strong></td>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${validatedData.city}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>Produkt:</strong></td>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${validatedData.productName} (${validatedData.productSku})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;"><strong>Pokoje:</strong></td>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">${validatedData.rooms}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2 style="font-size: 18px; border-bottom: 2px solid #f1f5f9; padding-bottom: 10px;">Dodatkowe informacje:</h2>
|
||||
<div style="background-color: #f8fafc; padding: 15px; border-radius: 6px; font-size: 15px; line-height: 1.6; white-space: pre-wrap;">
|
||||
${validatedData.message || 'Brak'}
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 24px; font-size: 12px; color: #64748b; text-align: center;">
|
||||
Zgoda RODO została zaakceptowana: <strong>${validatedData.consent ? "Tak" : "Nie"}</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div style="background-color: #f1f5f9; padding: 15px; text-align: center; font-size: 12px; color: #64748b;">
|
||||
Wiadomość wygenerowana automatycznie z formularza multisplit na stronie thermcool.pl
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Resend Error:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Nie udało się wysłać wiadomości ze względu na błąd serwera pocztowego.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Dziękujemy za zapytanie! Skontaktujemy się z Tobą najszybciej jak to możliwe.",
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Wystąpił błąd walidacji. Sprawdź poprawność danych.",
|
||||
errors: error.flatten().fieldErrors,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: "Wystąpił nieoczekiwany błąd serwera. Spróbuj ponownie później.",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
'use server';
|
||||
|
||||
import { Resend } from 'resend';
|
||||
import { verifyRecaptcha } from '@/lib/recaptcha';
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
@@ -18,8 +19,19 @@ export async function submitAwariaForm(data: {
|
||||
city: string;
|
||||
preferredDate: string;
|
||||
preferredTime: string;
|
||||
recaptchaToken?: string;
|
||||
}) {
|
||||
try {
|
||||
if (process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY && process.env.RECAPTCHA_SECRET_KEY) {
|
||||
if (!data.recaptchaToken) {
|
||||
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
|
||||
}
|
||||
const { success } = await verifyRecaptcha(data.recaptchaToken);
|
||||
if (!success) {
|
||||
return { success: false, message: "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." };
|
||||
}
|
||||
}
|
||||
|
||||
await resend.emails.send({
|
||||
from: process.env.RESEND_FROM_EMAIL || '[email protected]',
|
||||
to: process.env.RESEND_TO_EMAIL || '[email protected]',
|
||||
|
||||
@@ -125,6 +125,14 @@ export function AwariaClient() {
|
||||
}
|
||||
setIsSubmitting(true);
|
||||
|
||||
let recaptchaToken = '';
|
||||
if (process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY) {
|
||||
recaptchaToken = await (window as any).grecaptcha.execute(
|
||||
process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY,
|
||||
{ action: 'submit' }
|
||||
);
|
||||
}
|
||||
|
||||
await submitAwariaForm({
|
||||
isUrgent,
|
||||
brand,
|
||||
@@ -139,6 +147,7 @@ export function AwariaClient() {
|
||||
city,
|
||||
preferredDate: '',
|
||||
preferredTime: '',
|
||||
recaptchaToken,
|
||||
});
|
||||
|
||||
setIsSubmitting(false);
|
||||
|
||||
@@ -49,6 +49,14 @@ export function ContactForm() {
|
||||
});
|
||||
formData.append("consent", consent ? 'true' : 'false');
|
||||
|
||||
if (process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY) {
|
||||
const token = await (window as any).grecaptcha.execute(
|
||||
process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY,
|
||||
{ action: 'submit' }
|
||||
);
|
||||
formData.append('recaptchaToken', token);
|
||||
}
|
||||
|
||||
const response = await submitContactForm(null, formData);
|
||||
|
||||
setServerState({
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Check, Loader2 } from 'lucide-react';
|
||||
import { submitMultisplitForm } from '@/app/zapytanie-multisplit/actions';
|
||||
|
||||
interface Props {
|
||||
productName: string;
|
||||
@@ -38,20 +39,28 @@ export default function MultisplitContactForm({ productName, productSku }: Props
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const res = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: form.name,
|
||||
phone: form.phone,
|
||||
email: form.email,
|
||||
city: form.city,
|
||||
subject: `Zapytanie o multisplit: ${productName} (${productSku})`,
|
||||
message: `Liczba pomieszczeń: ${form.rooms}\n\n${form.message}`,
|
||||
}),
|
||||
let recaptchaToken = '';
|
||||
if (process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY) {
|
||||
recaptchaToken = await (window as any).grecaptcha.execute(
|
||||
process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY,
|
||||
{ action: 'submit' }
|
||||
);
|
||||
}
|
||||
|
||||
const res = await submitMultisplitForm({
|
||||
name: form.name,
|
||||
phone: form.phone,
|
||||
email: form.email,
|
||||
city: form.city,
|
||||
rooms: form.rooms,
|
||||
message: form.message,
|
||||
consent: form.consent,
|
||||
productName,
|
||||
productSku,
|
||||
recaptchaToken,
|
||||
});
|
||||
if (res.ok) setSent(true);
|
||||
else setError('Błąd wysyłki. Zadzwoń do nas bezpośrednio.');
|
||||
if (res.success) setSent(true);
|
||||
else setError(res.message || 'Błąd wysyłki. Zadzwoń do nas bezpośrednio.');
|
||||
} catch {
|
||||
setError('Błąd połączenia. Spróbuj ponownie.');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export async function verifyRecaptcha(token: string): Promise<{ success: boolean; score?: number }> {
|
||||
const res = await fetch('https://www.google.com/recaptcha/api/siteverify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `secret=${process.env.RECAPTCHA_SECRET_KEY}&response=${token}`,
|
||||
});
|
||||
const data = await res.json();
|
||||
return { success: data.success && (data.score === undefined || data.score >= 0.5), score: data.score };
|
||||
}
|
||||
Reference in New Issue
Block a user