feat: enhance contact form with toast notifications and error handling
This commit is contained in:
@@ -15,5 +15,8 @@
|
||||
"dotenv": "^16.5.0",
|
||||
"express": "^5.1.0",
|
||||
"nodemailer": "^7.0.3"
|
||||
}, "scripts": {
|
||||
"start": "node server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,48 +5,53 @@ const router = express.Router();
|
||||
router.post('/send', async (req, res) => {
|
||||
const { name, email, message } = req.body;
|
||||
|
||||
if (!name ||!email ||!message) {
|
||||
return res.status(400).json({ message: 'All fields are required.' });
|
||||
if (!name || !email || !message) {
|
||||
return res.status(400).json({ errorKey: 'status_error_all_fields' });
|
||||
}
|
||||
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({
|
||||
host: process.env.SMTP_HOST,
|
||||
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: {
|
||||
user: process.env.SMTP_USER,
|
||||
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 = {
|
||||
from: `"${name}" <${process.env.SMTP_FROM_EMAIL || process.env.SMTP_USER}>`, // Use a configured FROM email or fallback
|
||||
replyTo: email,
|
||||
to: process.env.YOUR_RECEIVING_EMAIL, // Your email address to receive submissions
|
||||
subject: `New Portfolio Contact: ${name}`,
|
||||
text: `Name: ${name}\nEmail: ${email}\nMessage: ${message}`,
|
||||
html: `<p><strong>Name:</strong> ${name}</p>
|
||||
<p><strong>Email:</strong> <a href="mailto:${email}">${email}</a></p>
|
||||
<p><strong>Message:</strong></p>
|
||||
<p>${message.replace(/\n/g, '<br>')}</p>`,
|
||||
};
|
||||
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
});
|
||||
|
||||
const mailOptions = {
|
||||
from: `"${name}" <${process.env.SMTP_FROM_EMAIL || process.env.SMTP_USER}>`,
|
||||
replyTo: email,
|
||||
to: process.env.YOUR_RECEIVING_EMAIL,
|
||||
subject: `New Portfolio Contact: ${name}`,
|
||||
text: `Name: ${name}\nEmail: ${email}\nMessage: ${message}`,
|
||||
html: `<p><strong>Name:</strong> ${name}</p>
|
||||
<p><strong>Email:</strong> <a href="mailto:${email}">${email}</a></p>
|
||||
<p><strong>Message:</strong></p>
|
||||
<p>${message.replace(/\n/g, '<br>')}</p>`,
|
||||
};
|
||||
|
||||
try {
|
||||
await transporter.sendMail(mailOptions);
|
||||
res.status(200).json({ message: 'Message sent successfully!' });
|
||||
} catch (error) {
|
||||
console.error('Error sending email:', error);
|
||||
// Provide a more generic error message to the client
|
||||
res.status(500).json({ message: 'Failed to send message. Please try again later.' });
|
||||
if (error.code) {
|
||||
switch (error.code) {
|
||||
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' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
npm run dev
|
||||
# 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.
|
||||
npm run build
|
||||
28
frontend/package-lock.json
generated
28
frontend/package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
"next-intl": "^4.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-icons": "^5.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -2443,7 +2444,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
@@ -3544,6 +3544,15 @@
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -5203,6 +5212,23 @@
|
||||
"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": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"next-intl": "^4.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-icons": "^5.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 223 KiB |
@@ -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