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";
|
"use server";
|
||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Resend } from "resend";
|
import { Resend } from "resend";
|
||||||
|
import { verifyRecaptcha } from "@/lib/recaptcha";
|
||||||
|
|
||||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
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) {
|
export async function submitContactForm(prevState: any, formData: FormData) {
|
||||||
try {
|
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
|
// 1. Walidacja danych z formularza
|
||||||
const rawData = {
|
const rawData = {
|
||||||
name: formData.get("name"),
|
name: formData.get("name"),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Header } from "@/components/layout/Header";
|
|||||||
import { TrustBar } from "@/components/layout/TrustBar";
|
import { TrustBar } from "@/components/layout/TrustBar";
|
||||||
import { Footer } from "@/components/layout/Footer";
|
import { Footer } from "@/components/layout/Footer";
|
||||||
import CookieBanner from '@/components/CookieBanner';
|
import CookieBanner from '@/components/CookieBanner';
|
||||||
|
import Script from 'next/script';
|
||||||
import { CompareBar } from '@/components/shop/CompareBar';
|
import { CompareBar } from '@/components/shop/CompareBar';
|
||||||
import { AnalyticsWrapper } from "@/components/ui/AnalyticsWrapper";
|
import { AnalyticsWrapper } from "@/components/ui/AnalyticsWrapper";
|
||||||
import { CookieSettingsFloatingIcon } from "@/components/ui/CookieSettingsFloatingIcon";
|
import { CookieSettingsFloatingIcon } from "@/components/ui/CookieSettingsFloatingIcon";
|
||||||
@@ -67,6 +68,12 @@ export default function RootLayout({
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||||
</head>
|
</head>
|
||||||
<body className="min-h-screen flex flex-col bg-background text-foreground">
|
<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 />
|
<Header />
|
||||||
<main className="flex-grow">{children}</main>
|
<main className="flex-grow">{children}</main>
|
||||||
<TrustBar />
|
<TrustBar />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { Resend } from 'resend';
|
import { Resend } from 'resend';
|
||||||
|
import { verifyRecaptcha } from '@/lib/recaptcha';
|
||||||
|
|
||||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
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) {
|
export async function submitOrderForm(prevState: any, formData: FormData) {
|
||||||
try {
|
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({
|
const data = orderSchema.parse({
|
||||||
name: formData.get('name'),
|
name: formData.get('name'),
|
||||||
phone: formData.get('phone'),
|
phone: formData.get('phone'),
|
||||||
|
|||||||
+15
-2
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCartStore } from '@/lib/cart-store';
|
import { useCartStore } from '@/lib/cart-store';
|
||||||
import { useActionState, useEffect } from 'react';
|
import { useActionState, useEffect, startTransition } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { submitOrderForm } from './actions';
|
import { submitOrderForm } from './actions';
|
||||||
import { ArrowLeft, CheckCircle } from 'lucide-react';
|
import { ArrowLeft, CheckCircle } from 'lucide-react';
|
||||||
@@ -75,7 +75,20 @@ Wróć do sklepu
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Formularz */}
|
{/* 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>
|
<h2 className="text-base font-bold text-primary font-heading">Twoje dane</h2>
|
||||||
|
|
||||||
{/* Hidden: produkty jako JSON */}
|
{/* 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';
|
'use server';
|
||||||
|
|
||||||
import { Resend } from 'resend';
|
import { Resend } from 'resend';
|
||||||
|
import { verifyRecaptcha } from '@/lib/recaptcha';
|
||||||
|
|
||||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||||
|
|
||||||
@@ -18,8 +19,19 @@ export async function submitAwariaForm(data: {
|
|||||||
city: string;
|
city: string;
|
||||||
preferredDate: string;
|
preferredDate: string;
|
||||||
preferredTime: string;
|
preferredTime: string;
|
||||||
|
recaptchaToken?: string;
|
||||||
}) {
|
}) {
|
||||||
try {
|
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({
|
await resend.emails.send({
|
||||||
from: process.env.RESEND_FROM_EMAIL || '[email protected]',
|
from: process.env.RESEND_FROM_EMAIL || '[email protected]',
|
||||||
to: process.env.RESEND_TO_EMAIL || '[email protected]',
|
to: process.env.RESEND_TO_EMAIL || '[email protected]',
|
||||||
|
|||||||
@@ -125,6 +125,14 @@ export function AwariaClient() {
|
|||||||
}
|
}
|
||||||
setIsSubmitting(true);
|
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({
|
await submitAwariaForm({
|
||||||
isUrgent,
|
isUrgent,
|
||||||
brand,
|
brand,
|
||||||
@@ -139,6 +147,7 @@ export function AwariaClient() {
|
|||||||
city,
|
city,
|
||||||
preferredDate: '',
|
preferredDate: '',
|
||||||
preferredTime: '',
|
preferredTime: '',
|
||||||
|
recaptchaToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ export function ContactForm() {
|
|||||||
});
|
});
|
||||||
formData.append("consent", consent ? 'true' : 'false');
|
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);
|
const response = await submitContactForm(null, formData);
|
||||||
|
|
||||||
setServerState({
|
setServerState({
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Check, Loader2 } from 'lucide-react';
|
import { Check, Loader2 } from 'lucide-react';
|
||||||
|
import { submitMultisplitForm } from '@/app/zapytanie-multisplit/actions';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
productName: string;
|
productName: string;
|
||||||
@@ -38,20 +39,28 @@ export default function MultisplitContactForm({ productName, productSku }: Props
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/contact', {
|
let recaptchaToken = '';
|
||||||
method: 'POST',
|
if (process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY) {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
recaptchaToken = await (window as any).grecaptcha.execute(
|
||||||
body: JSON.stringify({
|
process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY,
|
||||||
name: form.name,
|
{ action: 'submit' }
|
||||||
phone: form.phone,
|
);
|
||||||
email: form.email,
|
}
|
||||||
city: form.city,
|
|
||||||
subject: `Zapytanie o multisplit: ${productName} (${productSku})`,
|
const res = await submitMultisplitForm({
|
||||||
message: `Liczba pomieszczeń: ${form.rooms}\n\n${form.message}`,
|
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);
|
if (res.success) setSent(true);
|
||||||
else setError('Błąd wysyłki. Zadzwoń do nas bezpośrednio.');
|
else setError(res.message || 'Błąd wysyłki. Zadzwoń do nas bezpośrednio.');
|
||||||
} catch {
|
} catch {
|
||||||
setError('Błąd połączenia. Spróbuj ponownie.');
|
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