feat: enhance contact form with toast notifications and error handling
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 |
BIN
frontend/src/app/faviconOld.ico
Normal file
BIN
frontend/src/app/faviconOld.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 213 KiB |
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user