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

@@ -15,5 +15,8 @@
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"express": "^5.1.0", "express": "^5.1.0",
"nodemailer": "^7.0.3" "nodemailer": "^7.0.3"
}, "scripts": {
"start": "node server.js",
"test": "echo \"Error: no test specified\" && exit 1"
} }
} }

View File

@@ -5,48 +5,53 @@ const router = express.Router();
router.post('/send', async (req, res) => { router.post('/send', async (req, res) => {
const { name, email, message } = req.body; const { name, email, message } = req.body;
if (!name ||!email ||!message) { if (!name || !email || !message) {
return res.status(400).json({ message: 'All fields are required.' }); return res.status(400).json({ errorKey: 'status_error_all_fields' });
} }
if (!/\S+@\S+\.\S+/.test(email)) { if (!/\S+@\S+\.\S+/.test(email)) {
return res.status(400).json({ message: 'Invalid email address.' }); return res.status(400).json({ errorKey: 'status_error_invalid_email' });
} }
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST, host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT || '587', 10), port: parseInt(process.env.SMTP_PORT || '587', 10),
secure: process.env.SMTP_SECURE === 'true', // true for 465, false for other ports secure: process.env.SMTP_SECURE === 'true',
auth: { auth: {
user: process.env.SMTP_USER, user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS, pass: process.env.SMTP_PASS,
}, },
// If using self-signed certificates or having issues with TLS: });
// tls: {
// rejectUnauthorized: false // Use with caution, only for development/testing
// }
});
const mailOptions = { const mailOptions = {
from: `"${name}" <${process.env.SMTP_FROM_EMAIL || process.env.SMTP_USER}>`, // Use a configured FROM email or fallback from: `"${name}" <${process.env.SMTP_FROM_EMAIL || process.env.SMTP_USER}>`,
replyTo: email, replyTo: email,
to: process.env.YOUR_RECEIVING_EMAIL, // Your email address to receive submissions to: process.env.YOUR_RECEIVING_EMAIL,
subject: `New Portfolio Contact: ${name}`, subject: `New Portfolio Contact: ${name}`,
text: `Name: ${name}\nEmail: ${email}\nMessage: ${message}`, text: `Name: ${name}\nEmail: ${email}\nMessage: ${message}`,
html: `<p><strong>Name:</strong> ${name}</p> html: `<p><strong>Name:</strong> ${name}</p>
<p><strong>Email:</strong> <a href="mailto:${email}">${email}</a></p> <p><strong>Email:</strong> <a href="mailto:${email}">${email}</a></p>
<p><strong>Message:</strong></p> <p><strong>Message:</strong></p>
<p>${message.replace(/\n/g, '<br>')}</p>`, <p>${message.replace(/\n/g, '<br>')}</p>`,
}; };
try { try {
await transporter.sendMail(mailOptions); await transporter.sendMail(mailOptions);
res.status(200).json({ message: 'Message sent successfully!' }); res.status(200).json({ message: 'Message sent successfully!' });
} catch (error) { } catch (error) {
console.error('Error sending email:', error); if (error.code) {
// Provide a more generic error message to the client switch (error.code) {
res.status(500).json({ message: 'Failed to send message. Please try again later.' }); case 'EAUTH':
return res.status(500).json({ errorKey: 'smtp_auth_failed' });
case 'ECONNECTION':
return res.status(500).json({ errorKey: 'smtp_connection_failed' });
case 'EENVELOPE':
return res.status(400).json({ errorKey: 'smtp_invalid_recipient' });
default:
return res.status(500).json({ errorKey: 'smtp_generic_error' });
}
}
res.status(500).json({ errorKey: 'server_unexpected_error' });
} }
}); });

View File

@@ -1,36 +1,57 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). # Personal Portfolio & Blog - Frontend
## Getting Started This is the frontend for my personal portfolio, built with [Next.js](https://nextjs.org), [Tailwind CSS](https://tailwindcss.com/), and [TypeScript](https://www.typescriptlang.org/). The project is designed to be a modern, responsive, and performant showcase of my skills and projects.
First, run the development server: ## ✨ Features
* **Internationalization (i18n)**: Supports both English and Portuguese, with language switching capabilities.
* **Dark Mode**: A sleek dark mode that can be toggled by the user, with preferences saved in local storage.
* **Component-Based Architecture**: Built with reusable React components for maintainability and scalability.
* **Responsive Design**: Fully responsive layout that looks great on all devices, from mobile phones to desktop screens.
* **Contact Form**: A functional contact form that communicates with a backend service to send emails.
## 🛠️ Tech Stack
* **Framework**: [Next.js 15](https://nextjs.org/)
* **Styling**: [Tailwind CSS 4](https://tailwindcss.com/)
* **Language**: [TypeScript](https://www.typescriptlang.org/)
* **Internationalization**: [next-intl](https://next-intl-docs.vercel.app/)
* **Icons**: [Heroicons](https://heroicons.com/) & [React Icons](https://react-icons.github.io/react-icons/)
* **Linting**: [ESLint](https://eslint.org/)
## 🚀 Getting Started
To get a local copy up and running, follow these simple steps.
### Prerequisites
* Node.js (v18.18 or later)
* npm, yarn, or pnpm
### Installation & Development
1. **Clone the repository:**
```bash
git clone <your-repository-url>
cd <your-repository-folder>/frontend
```
2. **Install dependencies:**
```bash
npm install
```
3. **Run the development server:**
The development server uses Turbopack for faster performance.
```bash
npm run dev
```
4. **Open your browser:**
Navigate to [http://localhost:3000](http://localhost:3000) to see the result.
### Building for Production
To create a production-ready build, run:
```bash ```bash
npm run dev npm run build
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@@ -13,6 +13,7 @@
"next-intl": "^4.1.0", "next-intl": "^4.1.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hot-toast": "^2.5.2",
"react-icons": "^5.5.0" "react-icons": "^5.5.0"
}, },
"devDependencies": { "devDependencies": {
@@ -2443,7 +2444,6 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/damerau-levenshtein": { "node_modules/damerau-levenshtein": {
@@ -3544,6 +3544,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/goober": {
"version": "2.1.16",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
"integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
"license": "MIT",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -5203,6 +5212,23 @@
"react": "^19.1.0" "react": "^19.1.0"
} }
}, },
"node_modules/react-hot-toast": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz",
"integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==",
"license": "MIT",
"dependencies": {
"csstype": "^3.1.3",
"goober": "^2.1.16"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-icons": { "node_modules/react-icons": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",

View File

@@ -14,6 +14,7 @@
"next-intl": "^4.1.0", "next-intl": "^4.1.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hot-toast": "^2.5.2",
"react-icons": "^5.5.0" "react-icons": "^5.5.0"
}, },
"devDependencies": { "devDependencies": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 223 KiB

View File

@@ -2,6 +2,7 @@ import { Inter } from "next/font/google";
import Header from "@/app/components/Header"; // Usando o Header que forneci import Header from "@/app/components/Header"; // Usando o Header que forneci
import Footer from "@/app/components/Footer"; import Footer from "@/app/components/Footer";
import "@/app/globals.css"; // Importando o CSS global import "@/app/globals.css"; // Importando o CSS global
import { Toaster } from 'react-hot-toast';
// Importações importantes do next-intl // Importações importantes do next-intl
import {hasLocale, Locale, NextIntlClientProvider} from '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`}> <body className={`${inter.className} bg-background text-text-primary`}>
<ThemeProvider> <ThemeProvider>
<NextIntlClientProvider locale={locale}> <NextIntlClientProvider locale={locale}>
<Header /> <Header />
<main className="flex flex-col items-center"> <main className="flex flex-col items-center">
{children} {children}
</main> </main>
<Footer /> <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> </NextIntlClientProvider>
</ThemeProvider> </ThemeProvider>
</body> </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'; 'use client';
import {useTranslations} from 'next-intl'; import {useTranslations} from 'next-intl';
import { FormEvent, useState } from 'react';
import toast from 'react-hot-toast'; // <-- Import toast
export default function Contact() { export default function Contact() {
const t = useTranslations('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(); 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 ( return (
@@ -19,15 +75,18 @@ export default function Contact() {
<form onSubmit={handleSubmit} className="max-w-xl mx-auto"> <form onSubmit={handleSubmit} className="max-w-xl mx-auto">
<div className="mb-4"> <div className="mb-4">
<label htmlFor="name" className="block mb-2 text-sm font-medium text-[var(--color-text-secondary)]">{t('form_name')}</label> <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>
<div className="mb-4"> <div className="mb-4">
<label htmlFor="email" className="block mb-2 text-sm font-medium text-[var(--color-text-secondary)]">{t('form_email')}</label> <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>
<div className="mb-6"> <div className="mb-6">
<label htmlFor="message" className="block mb-2 text-sm font-medium text-[var(--color-text-secondary)]">{t('form_message')}</label> <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>
<div className="text-center"> <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"> <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; @tailwind utilities;
@theme { @theme {
--color-background: #0A0A0A; --color-background: #161616;
--color-card: #121212; --color-card: #121212;
--color-primary: #0ea5e9; --color-primary: #636B97;
--color-text-primary: #F4F4F5; --color-text-primary: #FFFFFF;
--color-text-secondary: #A1A1AA; --color-text-secondary: #EAE3DB;
--color-border: #27272A; --color-border: #27272A;
} }

View File

@@ -52,8 +52,17 @@
"form_email": "Your Email", "form_email": "Your Email",
"form_message": "Your Message", "form_message": "Your Message",
"submit_button": "Send Message", "submit_button": "Send Message",
"success_message": "Thank you! Your message has been sent successfully.", "status_sending": "Sending your message...",
"error_message": "Oops! Something went wrong. Please try again later." "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": { "footer": {
"copyright": "All rights reserved.", "copyright": "All rights reserved.",

View File

@@ -52,8 +52,17 @@
"form_email": "Seu Email", "form_email": "Seu Email",
"form_message": "Sua Mensagem", "form_message": "Sua Mensagem",
"submit_button": "Enviar Mensagem", "submit_button": "Enviar Mensagem",
"success_message": "Obrigado! Sua mensagem foi enviada com sucesso.", "status_sending": "Enviando sua mensagem...",
"error_message": "Oops! Algo deu errado. Por favor, tente novamente mais tarde." "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": { "footer": {
"copyright": "Todos os direitos reservados.", "copyright": "Todos os direitos reservados.",