feat: enhance contact form with toast notifications and error handling

This commit is contained in:
2025-06-09 11:25:28 -03:00
parent a16374afd0
commit e0161c0c99
14 changed files with 234 additions and 148 deletions

View File

@@ -2,6 +2,7 @@ import { Inter } from "next/font/google";
import Header from "@/app/components/Header"; // Usando o Header que forneci
import Footer from "@/app/components/Footer";
import "@/app/globals.css"; // Importando o CSS global
import { Toaster } from 'react-hot-toast';
// Importações importantes do next-intl
import {hasLocale, Locale, NextIntlClientProvider} from 'next-intl';
@@ -45,11 +46,37 @@ export default async function RootLayout({children, params}: Props) {
<body className={`${inter.className} bg-background text-text-primary`}>
<ThemeProvider>
<NextIntlClientProvider locale={locale}>
<Header />
<main className="flex flex-col items-center">
{children}
</main>
<Footer />
<Toaster
position="bottom-right"
toastOptions={{
// General styles for all toasts
style: {
background: 'var(--color-card)', //
color: 'var(--color-text-primary)', //
border: '1px solid var(--color-border)', //
},
// Specific styles for success toasts
success: {
iconTheme: {
primary: 'var(--color-primary)', //
secondary: 'var(--color-card)', //
},
},
// Specific styles for error toasts
error: {
iconTheme: {
primary: '#ef4444', // A standard red color for errors
secondary: 'var(--color-card)', //
},
},
}}
/>
</NextIntlClientProvider>
</ThemeProvider>
</body>

View File

@@ -1,74 +0,0 @@
'use client';
import React, { useState, FormEvent } from 'react';
import { useTranslations } from 'next-intl';
export default function ContactForm() {
const t = useTranslations('ContactForm');
const [formData, setFormData] = useState<{ name: string; email: string; message: string }>({ name: '', email: '', message: '' });
const [status, setStatus] = useState<string>('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData({...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setStatus(t('sending'));
if (!formData.name ||!formData.email ||!formData.message) {
setStatus(t('errorAllFields'));
return;
}
if (!/\S+@\S+\.\S+/.test(formData.email)) {
setStatus(t('errorInvalidEmail'));
return;
}
try {
// The backend will run on a different port or subpath, proxied by Nginx
const response = await fetch('/api/email/send', { // Adjusted API path
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
if (response.ok) {
setStatus(t('success'));
setFormData({ name: '', email: '', message: '' });
} else {
const errorData = await response.json();
setStatus(errorData.message || t('errorFailed'));
}
} catch (error) {
console.error('Contact form error:', error);
setStatus(t('errorGeneric'));
}
};
return (
<form onSubmit={handleSubmit} className="space-y-6 max-w-lg mx-auto p-8 bg-white dark:bg-gray-800 shadow-xl rounded-lg">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300">{t('nameLabel')}</label>
<input type="text" name="name" id="name" value={formData.name} onChange={handleChange} required
className="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm dark:bg-gray-700 dark:text-white" />
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300">{t('emailLabel')}</label>
<input type="email" name="email" id="email" value={formData.email} onChange={handleChange} required
className="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm dark:bg-gray-700 dark:text-white" />
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700 dark:text-gray-300">{t('messageLabel')}</label>
<textarea name="message" id="message" rows={4} value={formData.message} onChange={handleChange} required
className="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm dark:bg-gray-700 dark:text-white"></textarea>
</div>
<div>
<button type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-dark transition-colors">
{t('sendButton')}
</button>
</div>
{status && <p className={`mt-4 text-sm text-center ${status.includes('success') || status.includes('sucesso') ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>{status}</p>}
</form>
);
}

View File

@@ -1,13 +1,69 @@
'use client';
import {useTranslations} from 'next-intl';
import { FormEvent, useState } from 'react';
import toast from 'react-hot-toast'; // <-- Import toast
export default function Contact() {
const t = useTranslations('contact');
const [formData, setFormData] = useState<{ name: string; email: string; message: string }>({ name: '', email: '', message: '' });
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData({...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
alert('Contact form submission logic needs to be implemented!');
setIsSubmitting(true);
if (!formData.name || !formData.email || !formData.message) {
toast.error(t('status_error_all_fields'));
setIsSubmitting(false);
return;
}
if (!/\S+@\S+\.\S+/.test(formData.email)) {
toast.error(t('status_error_invalid_email'));
setIsSubmitting(false);
return;
}
const submissionPromise = async () => {
//const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL;
try {
const response = await fetch(`http://localhost:3001/api/email/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
if (!response.ok) {
const body = await response.json();
throw new Error(body.errorKey || 'status_error_generic');
}
return await response.json();
} catch (error: any) {
if (error instanceof TypeError) {
throw new Error('status_error_generic');
}
throw error;
}
};
toast.promise(submissionPromise(), {
loading: t('status_sending'),
success: () => {
setFormData({ name: '', email: '', message: '' });
setIsSubmitting(false);
return t('status_success');
},
error: (err: Error) => {
setIsSubmitting(false);
return t(err.message as any);
},
});
};
return (
@@ -19,15 +75,18 @@ export default function Contact() {
<form onSubmit={handleSubmit} className="max-w-xl mx-auto">
<div className="mb-4">
<label htmlFor="name" className="block mb-2 text-sm font-medium text-[var(--color-text-secondary)]">{t('form_name')}</label>
<input type="text" id="name" className="bg-[var(--color-card)] border border-[var(--color-border)] text-[var(--color-text-primary)] text-sm rounded-lg focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] block w-full p-2.5" required />
<input type="text" name="name" id="name" value={formData.name} onChange={handleChange} required
className="bg-[var(--color-card)] border border-[var(--color-border)] text-[var(--color-text-primary)] text-sm rounded-lg focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] block w-full p-2.5" />
</div>
<div className="mb-4">
<label htmlFor="email" className="block mb-2 text-sm font-medium text-[var(--color-text-secondary)]">{t('form_email')}</label>
<input type="email" id="email" className="bg-[var(--color-card)] border border-[var(--color-border)] text-[var(--color-text-primary)] text-sm rounded-lg focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] block w-full p-2.5" required />
<input type="email" name="email" id="email" value={formData.email} onChange={handleChange} required
className="bg-[var(--color-card)] border border-[var(--color-border)] text-[var(--color-text-primary)] text-sm rounded-lg focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] block w-full p-2.5" />
</div>
<div className="mb-6">
<label htmlFor="message" className="block mb-2 text-sm font-medium text-[var(--color-text-secondary)]">{t('form_message')}</label>
<textarea id="message" rows={5} className="bg-[var(--color-card)] border border-[var(--color-border)] text-[var(--color-text-primary)] text-sm rounded-lg focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] block w-full p-2.5" required></textarea>
<textarea name="message" id="message" rows={5} value={formData.message} onChange={handleChange} required
className="bg-[var(--color-card)] border border-[var(--color-border)] text-[var(--color-text-primary)] text-sm rounded-lg focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] block w-full p-2.5"></textarea>
</div>
<div className="text-center">
<button type="submit" className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white font-bold py-3 px-8 rounded-full transition-colors">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

View File

@@ -3,11 +3,11 @@
@tailwind utilities;
@theme {
--color-background: #0A0A0A;
--color-background: #161616;
--color-card: #121212;
--color-primary: #0ea5e9;
--color-text-primary: #F4F4F5;
--color-text-secondary: #A1A1AA;
--color-primary: #636B97;
--color-text-primary: #FFFFFF;
--color-text-secondary: #EAE3DB;
--color-border: #27272A;
}

View File

@@ -52,8 +52,17 @@
"form_email": "Your Email",
"form_message": "Your Message",
"submit_button": "Send Message",
"success_message": "Thank you! Your message has been sent successfully.",
"error_message": "Oops! Something went wrong. Please try again later."
"status_sending": "Sending your message...",
"status_success": "Message sent successfully. Thank you!",
"status_error_failed": "Failed to send message. Please try again.",
"status_error_all_fields": "All fields are required.",
"status_error_invalid_email": "Please enter a valid email address.",
"status_error_generic": "An unexpected error occurred. Please try again later.",
"server_unexpected_error": "An unexpected server error occurred. Please try again later.",
"smtp_auth_failed": "Authentication with the mail server failed.",
"smtp_connection_failed": "Could not connect to the mail server.",
"smtp_invalid_recipient": "Invalid recipient email address.",
"smtp_generic_error": "A mail-sending error occurred."
},
"footer": {
"copyright": "All rights reserved.",

View File

@@ -52,8 +52,17 @@
"form_email": "Seu Email",
"form_message": "Sua Mensagem",
"submit_button": "Enviar Mensagem",
"success_message": "Obrigado! Sua mensagem foi enviada com sucesso.",
"error_message": "Oops! Algo deu errado. Por favor, tente novamente mais tarde."
"status_sending": "Enviando sua mensagem...",
"status_success": "Mensagem enviada com sucesso. Obrigado!",
"status_error_failed": "Falha ao enviar a mensagem. Por favor, tente novamente.",
"status_error_all_fields": "Todos os campos são obrigatórios.",
"status_error_invalid_email": "Por favor, insira um email válido.",
"status_error_generic": "Ocorreu um erro inesperado. Tente novamente mais tarde.",
"server_unexpected_error": "Ocorreu um erro inesperado no servidor. Por favor, tente novamente mais tarde.",
"smtp_auth_failed": "Falha na autenticação com o servidor de email.",
"smtp_connection_failed": "Não foi possível conectar ao servidor de email.",
"smtp_invalid_recipient": "Endereço de email do destinatário inválido.",
"smtp_generic_error": "Ocorreu um erro ao enviar o email."
},
"footer": {
"copyright": "Todos os direitos reservados.",