First Commit

This commit is contained in:
marco
2026-09-01 10:46:29 -03:00
commit bf96b77e9c
10000 changed files with 3508600 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
SysCMS Commercial License
Copyright (c) 2026 Marco Costa. All rights reserved.
This software is licensed, not sold. Permission is granted to use this software subject to the terms of the SysCMS Commercial License.
Redistribution, sublicensing, resale, or disclosure of this software, in whole or in part, is strictly prohibited without prior written permission from the author.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+21
View File
@@ -0,0 +1,21 @@
# Contact Module for SysCMS
Contact form and information management module.
## Features
- Contact form with validation
- Admin interface to view and manage contacts
- Email notifications
- Multi-language support
## Installation
1. Upload the `contact.zip` file through the module manager or extract it to the `modules/` directory.
2. Run the migrations to register the module.
3. Configure contact settings in the admin panel.
## Routes
- Web routes: `routes/web.php`
## License
Copyright (c) 2026 Marco Costa. All rights reserved.
Licensed under the SysCMS Commercial License.
+321
View File
@@ -0,0 +1,321 @@
<?php
namespace Modules\Contact\Controllers;
use App\Controllers\BaseAdminController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Modules\Contact\Models\Contact;
class ContactController extends BaseAdminController
{
/**
* Display contact form for public
*/
public function index(Request $request): Response
{
$requireCaptcha = $this->getSetting('require_captcha_contact', true);
$pageContent = null;
$metaTitle = null;
$metaDescription = null;
$title = 'Contact Us';
if (class_exists('Modules\Configindex\Models\ConfigPage')) {
try {
$page = \Modules\Configindex\Models\ConfigPage::getByKey('contact', 1);
if ($page) {
$pageContent = $page->config_index_content;
if ($page->config_index_title) {
$title = $page->config_index_title;
}
$metaTitle = $page->config_index_meta_title;
$metaDescription = $page->config_index_meta_description;
}
} catch (\Exception $e) {
// Ignore if table not found or other error
}
}
return new Response($this->view->render('contact/views/index.twig', [
'title' => $title,
'page_content' => $pageContent,
'meta_title' => $metaTitle,
'meta_description' => $metaDescription,
'require_captcha_contact' => $requireCaptcha
]));
}
/**
* Process contact form submission
*/
public function submit(Request $request): Response
{
// Handle JSON request body
if ($request->getContentType() === 'json' || 0 === strpos($request->headers->get('Content-Type'), 'application/json')) {
$data = json_decode($request->getContent(), true) ?? [];
} else {
$data = $request->request->all();
}
// Basic validation
if (empty($data['name']) || empty($data['email']) || empty($data['subject']) || empty($data['message'])) {
if ($request->headers->get('Accept') === 'application/json') {
return new JsonResponse([
'success' => false,
'message' => 'Please fill in all required fields.'
], 400);
}
return new RedirectResponse(url('/contact?error=missing_fields'));
}
// Email format validation
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
if ($request->headers->get('Accept') === 'application/json') {
return new JsonResponse([
'success' => false,
'message' => 'Please provide a valid email address.'
], 400);
}
return new RedirectResponse(url('/contact?error=invalid_email'));
}
// Captcha validation
$requireCaptcha = $this->getSetting('require_captcha_contact', true);
if ($requireCaptcha && empty($data['captcha_valid'])) {
if ($request->headers->get('Accept') === 'application/json') {
return new JsonResponse([
'success' => false,
'message' => 'Invalid captcha. Please try again.'
], 400);
}
return new RedirectResponse(url('/contact?error=invalid_captcha'));
}
// Create contact record
$contact = new Contact();
$contact->contacts_name = $data['name'];
$contact->contacts_email = $data['email'];
$contact->contacts_phone = $data['phone'] ?? '';
$contact->contacts_subject = apply_filters('contact.subject_sanitize', $data['subject']);
$contact->contacts_message = apply_filters('contact.message_sanitize', $data['message']);
$contact->contacts_status = 'new';
$contact->contacts_ip_address = $request->getClientIp();
$contact->contacts_user_agent = $request->headers->get('User-Agent');
$contact->save();
do_action('contact.submitted', $contact);
if ($request->headers->get('Accept') === 'application/json') {
return new JsonResponse([
'success' => true,
'message' => 'Your message has been sent successfully. We will get back to you soon!'
]);
}
return new RedirectResponse(url('/contact?success=true'));
}
/**
* Display list of contacts for admin
*/
public function list(Request $request): Response
{
$status = $request->query->get('status', 'all');
$contacts = Contact::query();
if ($status === 'unread') {
$contacts = $contacts->unread();
} elseif ($status === 'read') {
$contacts = $contacts->read();
} elseif ($status !== 'all') {
$contacts = $contacts->status($status);
}
$contacts = $contacts->orderBy('created_at', 'desc')->get();
return $this->renderAdminView('contact/views/admin/index.twig', [
'contacts' => $contacts,
'current_status' => $status,
'title' => 'Contact Messages Management'
]);
}
/**
* Display contact details
*/
public function show(Request $request, int $id): Response
{
$contact = Contact::find($id);
if (!$contact) {
return new RedirectResponse(url('admin/contacts?error=not_found'));
}
// Mark as read if it's unread
if (!$contact->isRead()) {
$contact->markAsRead();
}
return $this->renderAdminView('contact/views/admin/show.twig', [
'contact' => $contact,
'title' => 'Contact Message Details'
]);
}
/**
* Update contact status
*/
public function updateStatus(Request $request, int $id): Response
{
$contact = Contact::find($id);
if (!$contact) {
if ($request->headers->get('Accept') === 'application/json') {
return new JsonResponse([
'success' => false,
'message' => 'Contact not found.'
], 404);
}
return new RedirectResponse(url('admin/contacts?error=not_found'));
}
$status = $request->request->get('status');
$validStatuses = ['new', 'in_progress', 'resolved', 'closed'];
if (!in_array($status, $validStatuses)) {
if ($request->headers->get('Accept') === 'application/json') {
return new JsonResponse([
'success' => false,
'message' => 'Invalid status.'
], 400);
}
return new RedirectResponse(url('admin/contacts?error=invalid_status'));
}
$contact->contacts_status = $status;
$contact->save();
if ($request->headers->get('Accept') === 'application/json') {
return new JsonResponse([
'success' => true,
'message' => 'Contact status updated successfully.'
]);
}
return new RedirectResponse(url('admin/contacts/' . $id));
}
/**
* Update contact response
*/
public function updateResponse(Request $request, int $id): Response
{
$contact = Contact::find($id);
if (!$contact) {
return new RedirectResponse(url('admin/contacts?error=not_found'));
}
$response = $request->request->get('response');
$contact->contacts_response = $response;
// Auto update status if it is 'new'
if ($contact->contacts_status === 'new' && !empty($response)) {
$contact->contacts_status = 'resolved';
}
$contact->save();
return new RedirectResponse(url('admin/contacts/' . $id . '?success=response_saved'));
}
/**
* Delete contact
*/
public function delete(Request $request, int $id): Response
{
$contact = Contact::find($id);
if (!$contact) {
if ($request->headers->get('Accept') === 'application/json') {
return new JsonResponse([
'success' => false,
'message' => 'Contact not found.'
], 404);
}
return new RedirectResponse(url('admin/contacts?error=not_found'));
}
$contact->delete();
if ($request->headers->get('Accept') === 'application/json') {
return new JsonResponse([
'success' => true,
'message' => 'Contact deleted successfully.'
]);
}
return new RedirectResponse(url('admin/contacts'));
}
/**
* Mark contact as read/unread
*/
public function toggleRead(Request $request, int $id): Response
{
$contact = Contact::find($id);
if (!$contact) {
if ($request->headers->get('Accept') === 'application/json') {
return new JsonResponse([
'success' => false,
'message' => 'Contact not found.'
], 404);
}
return new RedirectResponse(url('admin/contacts?error=not_found'));
}
if ($contact->isRead()) {
$contact->markAsUnread();
$message = 'Contact marked as unread.';
} else {
$contact->markAsRead();
$message = 'Contact marked as read.';
}
if ($request->headers->get('Accept') === 'application/json') {
return new JsonResponse([
'success' => true,
'message' => $message,
'is_read' => $contact->isRead()
]);
}
return new RedirectResponse(url('admin/contacts/' . $id));
}
/**
* Get contact module setting
*/
private function getSetting(string $key, $default = null)
{
try {
$module = \App\Models\Module::byName('contact')->first();
if ($module) {
return $module->getSetting($key, $default);
}
} catch (\Throwable $e) {
}
return $default;
}
}
+152
View File
@@ -0,0 +1,152 @@
<?php
namespace Modules\Contact\Models;
use Illuminate\Database\Eloquent\Model;
class Contact extends Model
{
/**
* The table associated with model.
*
* @var string
*/
protected $table = 'contacts';
/**
* The primary key associated with table.
*
* @var string
*/
protected $primaryKey = 'contacts_id';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'contacts_name',
'contacts_email',
'contacts_phone',
'contacts_subject',
'contacts_message',
'contacts_response',
'contacts_status',
'contacts_ip_address',
'contacts_user_agent',
'contacts_read_at'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'contacts_read_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime'
];
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = [
'contacts_read_at',
'created_at',
'updated_at'
];
/**
* Get unread contacts only.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeUnread($query)
{
return $query->whereNull('contacts_read_at');
}
/**
* Get read contacts only.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeRead($query)
{
return $query->whereNotNull('contacts_read_at');
}
/**
* Get contacts by status.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $status
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeStatus($query, string $status)
{
return $query->where('contacts_status', $status);
}
/**
* Mark contact as read.
*
* @return bool
*/
public function markAsRead(): bool
{
if ($this->contacts_read_at) {
return true;
}
return $this->update(['contacts_read_at' => \Carbon\Carbon::now()]);
}
/**
* Mark contact as unread.
*
* @return bool
*/
public function markAsUnread(): bool
{
return $this->update(['contacts_read_at' => null]);
}
/**
* Check if contact is read.
*
* @return bool
*/
public function isRead(): bool
{
return !is_null($this->contacts_read_at);
}
/**
* Get formatted created date.
*
* @return string
*/
public function getFormattedCreatedAtAttribute(): string
{
return $this->created_at->format('d/m/Y H:i');
}
/**
* Get short message (100 characters).
*
* @return string
*/
public function getShortMessageAttribute(): string
{
return strlen($this->contacts_message) > 100
? substr($this->contacts_message, 0, 100) . '...'
: $this->contacts_message;
}
}
@@ -0,0 +1,80 @@
<?php
namespace Modules\Contact\Providers;
use Kernel\Application;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mime\Email;
use Modules\Contact\Models\Contact;
class ContactServiceProvider
{
public function register()
{
}
public function boot()
{
$app = Application::getInstance();
$app->addPublicPath('/contact*');
if (function_exists('add_filter')) {
add_filter('contact.subject_sanitize', function ($subject) {
$subject = is_string($subject) ? $subject : '';
$subject = strip_tags($subject);
$subject = preg_replace('/\s+/', ' ', trim($subject));
return $subject;
}, 10);
add_filter('contact.message_sanitize', function ($message) {
$message = is_string($message) ? $message : '';
$message = strip_tags($message);
$message = trim($message);
return $message;
}, 10);
}
if (function_exists('add_action')) {
add_action('contact.submitted', function (Contact $contact) {
$dsn = $_ENV['MAILER_DSN'] ?? '';
if (!$dsn) {
$transport = Transport::fromDsn(sprintf(
'smtp://%s:%s@%s:%s?encryption=%s',
$_ENV['MAIL_USERNAME'] ?? '',
$_ENV['MAIL_PASSWORD'] ?? '',
$_ENV['MAIL_HOST'] ?? 'localhost',
$_ENV['MAIL_PORT'] ?? '25',
$_ENV['MAIL_ENCRYPTION'] ?? ''
));
} else {
$transport = Transport::fromDsn($dsn);
}
$mailer = new Mailer($transport);
$to = $_ENV['MAIL_TO_ADDRESS'] ?? $_ENV['MAIL_FROM_ADDRESS'] ?? null;
if (!$to) {
return;
}
$email = (new Email())
->from($_ENV['MAIL_FROM_ADDRESS'] ?? 'no-reply@localhost')
->to($to)
->subject('Nova mensagem de contato: ' . ($contact->subject ?? ''))
->html(
'<h3>Nova mensagem de contato</h3>' .
'<p><strong>Nome:</strong> ' . htmlspecialchars($contact->name) . '</p>' .
'<p><strong>Email:</strong> ' . htmlspecialchars($contact->email) . '</p>' .
'<p><strong>Telefone:</strong> ' . htmlspecialchars((string)$contact->phone) . '</p>' .
'<p><strong>Assunto:</strong> ' . htmlspecialchars($contact->subject) . '</p>' .
'<p><strong>Mensagem:</strong><br>' . nl2br(htmlspecialchars($contact->message)) . '</p>'
);
try {
$mailer->send($email);
} catch (\Throwable $e) {
}
}, 10);
}
}
}
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Schema\Blueprint;
class CreateContactsTable
{
public function up()
{
if (!Capsule::schema()->hasTable('contacts')) {
Capsule::schema()->create('contacts', function (Blueprint $table) {
$table->increments('contacts_id');
$table->string('contacts_name');
$table->string('contacts_email');
$table->string('contacts_phone')->nullable();
$table->string('contacts_subject')->nullable();
$table->text('contacts_message');
$table->text('contacts_response')->nullable();
$table->string('contacts_status')->default('new'); // new, read, replied
$table->string('contacts_ip_address')->nullable();
$table->string('contacts_user_agent')->nullable();
$table->timestamp('contacts_read_at')->nullable();
$table->timestamps();
});
}
}
public function down()
{
Capsule::schema()->dropIfExists('contacts');
}
}
@@ -0,0 +1,78 @@
<?php
use Illuminate\Database\Capsule\Manager as Capsule;
use App\Models\Module;
use Modules\Company\Models\Company;
class InsertContactModule
{
public function up()
{
$moduleData = [
'modules_name' => 'contact',
'modules_display_name' => 'Contact',
'modules_description' => 'Contact form and message management module',
'modules_version' => '1.0.0',
'modules_author' => 'SysCMS Team',
'modules_ative' => true,
'modules_public' => true,
'modules_blocked' => false,
'modules_dependencies' => ["auth"],
'modules_permissions' => [
"contact.view",
"contact.create",
"contact.edit",
"contact.delete"
],
'modules_settings' => [],
'modules_path' => 'modules/contact',
'modules_namespace' => 'Modules\\Contact',
'modules_icon' => 'bi bi-envelope',
'menu_side_bar' => true,
'menu_nav_bar' => false,
'modules_order' => 40,
'modules_install_date' => date('Y-m-d H:i:s'),
'modules_update_date' => date('Y-m-d H:i:s')
];
$existingModule = Module::where('modules_name', 'contact')->first();
if (!$existingModule) {
$module = Module::create($moduleData);
if (Capsule::schema()->hasTable('bus')) {
$companies = Company::all();
foreach ($companies as $company) {
$exists = Capsule::table('bus_modules')
->where('bus_modules_bus_id', $company->bus_id)
->where('bus_modules_module_id', $module->modules_id)
->exists();
if (!$exists) {
Capsule::table('bus_modules')->insert([
'bus_modules_bus_id' => $company->bus_id,
'bus_modules_module_id' => $module->modules_id,
'bus_modules_is_enabled' => true,
'bus_modules_settings' => json_encode([]),
'bus_modules_enabled_by' => 1,
'bus_modules_enabled_at' => date('Y-m-d H:i:s'),
'bus_modules_show_public_nav' => true,
'bus_modules_show_dashboard_nav' => true,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
]);
}
}
}
}
}
public function down()
{
$module = Module::where('modules_name', 'contact')->first();
if ($module) {
Capsule::table('bus_modules')->where('bus_modules_module_id', $module->modules_id)->delete();
$module->delete();
}
}
}
+81
View File
@@ -0,0 +1,81 @@
{
"menu": {
"dashboard": "Kontakt Nachrichten"
},
"title": "Kontaktieren Sie uns",
"subtitle": "Wir sind hier, um Ihnen und Ihrem Unternehmen zu helfen",
"address_title": "Adresse",
"address_line1": "Musterstraße 123, Stadt",
"address_line2": "Land, Postleitzahl",
"phone_title": "Telefon",
"phone_number": "+49 30 12345678",
"phone_hours": "Mo-Fr, 9:00-18:00 Uhr",
"email_title": "E-Mail",
"email_info": "info@syscms.de",
"email_support": "support@syscms.de",
"form_title": "Nachricht senden",
"form_subtitle": "Wir werden so schnell wie möglich antworten",
"form_name": "Vollständiger Name",
"form_email": "Ihre E-Mail",
"form_phone": "Telefon (Optional)",
"form_subject": "Betreff",
"form_message": "Nachricht",
"form_submit": "Nachricht senden",
"sending": "Senden...",
"js_success": "Ihre Nachricht wurde erfolgreich gesendet. Wir melden uns bald bei Ihnen!",
"js_error": "Beim Senden Ihrer Nachricht ist ein Fehler aufgetreten.",
"js_error_generic": "Beim Senden Ihrer Nachricht ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.",
"js_missing_fields": "Bitte füllen Sie alle Pflichtfelder aus.",
"js_invalid_email": "Bitte geben Sie eine gültige E-Mail-Adresse an.",
"js_invalid_captcha": "Falscher Bestätigungscode. Bitte versuchen Sie es erneut.",
"security_check": "Sicherheitsüberprüfung",
"captcha_solved": "Richtig!",
"captcha_instruction": "Lösen Sie die Summe, um fortzufahren.",
"captcha_reload": "Herausforderung neu laden",
"admin": {
"title": "Kontakt Nachrichten",
"list_title": "Nachrichtenliste",
"details_title": "Nachrichtendetails",
"statuses": {
"all": "Alle",
"unread": "Ungelesen",
"read": "Gelesen",
"new": "Neu",
"in_progress": "In Bearbeitung",
"resolved": "Gelöst",
"closed": "Geschlossen"
},
"columns": {
"name": "Name",
"email": "E-Mail",
"subject": "Betreff",
"status": "Status",
"date": "Datum",
"actions": "Aktionen"
},
"fields": {
"name": "Name",
"email": "E-Mail",
"phone": "Telefon",
"subject": "Betreff",
"message": "Nachricht",
"response": "Antwort",
"ip_address": "IP-Adresse",
"date": "Datum"
},
"actions": {
"view": "Ansehen",
"mark_unread": "Als ungelesen markieren",
"mark_read": "Als gelesen markieren",
"delete": "Löschen",
"save_response": "Antwort speichern",
"back": "Zurück"
},
"placeholders": {
"response": "Geben Sie Ihre Antwort hier ein..."
},
"no_messages": "Keine Kontakt-Nachrichten gefunden.",
"confirm_delete": "Sind Sie sicher, dass Sie diese Nachricht löschen möchten?",
"error_occurred": "Ein Fehler ist aufgetreten."
}
}
+81
View File
@@ -0,0 +1,81 @@
{
"menu": {
"dashboard": "Contact Messages"
},
"title": "Contact Us",
"subtitle": "We are here to help you and your business",
"address_title": "Address",
"address_line1": "123 Street Name, City",
"address_line2": "Country, Zip Code",
"phone_title": "Phone",
"phone_number": "+1 (555) 123-4567",
"phone_hours": "Mon-Fri, 9am-6pm",
"email_title": "Email",
"email_info": "info@syscms.com",
"email_support": "support@syscms.com",
"form_title": "Send a Message",
"form_subtitle": "We will respond as soon as possible",
"form_name": "Full Name",
"form_email": "Your Email",
"form_phone": "Phone (Optional)",
"form_subject": "Subject",
"form_message": "Message",
"form_submit": "Send Message",
"sending": "Sending...",
"js_success": "Your message has been sent successfully. We will get back to you soon!",
"js_error": "An error occurred while sending your message.",
"js_error_generic": "An error occurred while sending your message. Please try again.",
"js_missing_fields": "Please fill in all required fields.",
"js_invalid_email": "Please provide a valid email address.",
"js_invalid_captcha": "Incorrect verification code. Please try again.",
"security_check": "Security Check",
"captcha_solved": "Correct!",
"captcha_instruction": "Solve the sum to continue.",
"captcha_reload": "Reload challenge",
"admin": {
"title": "Contact Messages",
"list_title": "Contact Messages List",
"details_title": "Message Details",
"statuses": {
"all": "All",
"unread": "Unread",
"read": "Read",
"new": "New",
"in_progress": "In Progress",
"resolved": "Resolved",
"closed": "Closed"
},
"columns": {
"name": "Name",
"email": "Email",
"subject": "Subject",
"status": "Status",
"date": "Date",
"actions": "Actions"
},
"fields": {
"name": "Name",
"email": "Email",
"phone": "Phone",
"subject": "Subject",
"message": "Message",
"response": "Response",
"ip_address": "IP Address",
"date": "Date"
},
"actions": {
"view": "View",
"mark_unread": "Mark as unread",
"mark_read": "Mark as read",
"delete": "Delete",
"save_response": "Save Response",
"back": "Back"
},
"placeholders": {
"response": "Type your response here..."
},
"no_messages": "No contact messages found.",
"confirm_delete": "Are you sure you want to delete this message?",
"error_occurred": "An error occurred."
}
}
+81
View File
@@ -0,0 +1,81 @@
{
"menu": {
"dashboard": "Mensajes de Contacto"
},
"title": "Contáctenos",
"subtitle": "Estamos aquí para ayudarle a usted y a su negocio",
"address_title": "Dirección",
"address_line1": "Calle Ejemplo 123, Ciudad",
"address_line2": "País, Código Postal",
"phone_title": "Teléfono",
"phone_number": "+34 91 123 45 67",
"phone_hours": "Lun-Vie, 9h-18h",
"email_title": "Correo Electrónico",
"email_info": "info@syscms.es",
"email_support": "soporte@syscms.es",
"form_title": "Enviar un Mensaje",
"form_subtitle": "Responderemos lo antes posible",
"form_name": "Nombre Completo",
"form_email": "Su Correo",
"form_phone": "Teléfono (Opcional)",
"form_subject": "Asunto",
"form_message": "Mensaje",
"form_submit": "Enviar Mensaje",
"sending": "Enviando...",
"js_success": "Su mensaje ha sido enviado con éxito. ¡Nos pondremos en contacto pronto!",
"js_error": "Ocurrió un error al enviar su mensaje.",
"js_error_generic": "Ocurrió un error al enviar su mensaje. Por favor, inténtelo de nuevo.",
"js_missing_fields": "Por favor, complete todos los campos obligatorios.",
"js_invalid_email": "Por favor, proporcione una dirección de correo electrónico válida.",
"js_invalid_captcha": "Código de verificación incorrecto. Inténtelo de nuevo.",
"security_check": "Verificación de seguridad",
"captcha_solved": "¡Correcto!",
"captcha_instruction": "Resuelve la suma para continuar.",
"captcha_reload": "Recargar desafío",
"admin": {
"title": "Mensajes de Contacto",
"list_title": "Lista de Mensajes",
"details_title": "Detalles del Mensaje",
"statuses": {
"all": "Todos",
"unread": "No leídos",
"read": "Leídos",
"new": "Nuevo",
"in_progress": "En Progreso",
"resolved": "Resuelto",
"closed": "Cerrado"
},
"columns": {
"name": "Nombre",
"email": "Correo",
"subject": "Asunto",
"status": "Estado",
"date": "Fecha",
"actions": "Acciones"
},
"fields": {
"name": "Nombre",
"email": "Correo",
"phone": "Teléfono",
"subject": "Asunto",
"message": "Mensaje",
"response": "Respuesta",
"ip_address": "Dirección IP",
"date": "Fecha"
},
"actions": {
"view": "Ver",
"mark_unread": "Marcar como no leído",
"mark_read": "Marcar como leído",
"delete": "Eliminar",
"save_response": "Guardar Respuesta",
"back": "Volver"
},
"placeholders": {
"response": "Escriba su respuesta aquí..."
},
"no_messages": "No se encontraron mensajes de contacto.",
"confirm_delete": "¿Está seguro de que desea eliminar este mensaje?",
"error_occurred": "Ocurrió un error."
}
}
+81
View File
@@ -0,0 +1,81 @@
{
"menu": {
"dashboard": "Messages de Contact"
},
"title": "Contactez-nous",
"subtitle": "Nous sommes là pour vous aider, vous et votre entreprise",
"address_title": "Adresse",
"address_line1": "123 Rue Exemple, Ville",
"address_line2": "Pays, Code Postal",
"phone_title": "Téléphone",
"phone_number": "+33 1 23 45 67 89",
"phone_hours": "Lun-Ven, 9h-18h",
"email_title": "Email",
"email_info": "info@syscms.fr",
"email_support": "support@syscms.fr",
"form_title": "Envoyer un message",
"form_subtitle": "Nous répondrons dès que possible",
"form_name": "Nom Complet",
"form_email": "Votre Email",
"form_phone": "Téléphone (Optionnel)",
"form_subject": "Sujet",
"form_message": "Message",
"form_submit": "Envoyer le Message",
"sending": "Envoi en cours...",
"js_success": "Votre message a été envoyé avec succès. Nous vous recontacterons bientôt !",
"js_error": "Une erreur s'est produite lors de l'envoi de votre message.",
"js_error_generic": "Une erreur s'est produite lors de l'envoi de votre message. Veuillez réessayer.",
"js_missing_fields": "Veuillez remplir tous les champs obligatoires.",
"js_invalid_email": "Veuillez fournir une adresse e-mail valide.",
"js_invalid_captcha": "Code de vérification incorrect. Veuillez réessayer.",
"security_check": "Vérification de sécurité",
"captcha_solved": "Correct !",
"captcha_instruction": "Résolvez l'addition pour continuer.",
"captcha_reload": "Recharger le défi",
"admin": {
"title": "Messages de Contact",
"list_title": "Liste des Messages",
"details_title": "Détails du Message",
"statuses": {
"all": "Tous",
"unread": "Non Lus",
"read": "Lus",
"new": "Nouveau",
"in_progress": "En Cours",
"resolved": "Résolu",
"closed": "Fermé"
},
"columns": {
"name": "Nom",
"email": "Email",
"subject": "Sujet",
"status": "Statut",
"date": "Date",
"actions": "Actions"
},
"fields": {
"name": "Nom",
"email": "Email",
"phone": "Téléphone",
"subject": "Sujet",
"message": "Message",
"response": "Réponse",
"ip_address": "Adresse IP",
"date": "Date"
},
"actions": {
"view": "Voir",
"mark_unread": "Marquer comme non lu",
"mark_read": "Marquer comme lu",
"delete": "Supprimer",
"save_response": "Enregistrer la Réponse",
"back": "Retour"
},
"placeholders": {
"response": "Tapez votre réponse ici..."
},
"no_messages": "Aucun message trouvé.",
"confirm_delete": "Êtes-vous sûr de vouloir supprimer ce message ?",
"error_occurred": "Une erreur est survenue."
}
}
+81
View File
@@ -0,0 +1,81 @@
{
"menu": {
"dashboard": "Mensagens de Contato"
},
"title": "Entre em Contato",
"subtitle": "Estamos aqui para ajudar você e seu negócio",
"address_title": "Endereço",
"address_line1": "Santa Rita do Sapucaí - MG",
"address_line2": "Brasil",
"phone_title": "Telefone",
"phone_number": "+55 (35) 99226-1684",
"phone_hours": "Atendimento Geral",
"email_title": "Email",
"email_info": "contato@syspanel.com.br",
"email_support": "",
"form_title": "Envie uma Mensagem",
"form_subtitle": "Responderemos o mais breve possível",
"form_name": "Nome Completo",
"form_email": "Seu Email",
"form_phone": "Telefone (Opcional)",
"form_subject": "Assunto",
"form_message": "Mensagem",
"form_submit": "Enviar Mensagem",
"sending": "Enviando...",
"js_success": "Sua mensagem foi enviada com sucesso. Entraremos em contato em breve!",
"js_error": "Ocorreu um erro ao enviar sua mensagem.",
"js_error_generic": "Ocorreu um erro ao enviar sua mensagem. Por favor, tente novamente.",
"js_missing_fields": "Por favor, preencha todos os campos obrigatórios.",
"js_invalid_email": "Por favor, forneça um endereço de email válido.",
"js_invalid_captcha": "Código de verificação incorreto. Por favor, tente novamente.",
"security_check": "Verificação de Segurança",
"captcha_solved": "Correto!",
"captcha_instruction": "Resolva a soma para continuar.",
"captcha_reload": "Trocar desafio",
"admin": {
"title": "Mensagens de Contato",
"list_title": "Lista de Mensagens",
"details_title": "Detalhes da Mensagem",
"statuses": {
"all": "Todos",
"unread": "Não Lidos",
"read": "Lidos",
"new": "Novo",
"in_progress": "Em Progresso",
"resolved": "Resolvido",
"closed": "Fechado"
},
"columns": {
"name": "Nome",
"email": "Email",
"subject": "Assunto",
"status": "Status",
"date": "Data",
"actions": "Ações"
},
"fields": {
"name": "Nome",
"email": "Email",
"phone": "Telefone",
"subject": "Assunto",
"message": "Mensagem",
"response": "Resposta",
"ip_address": "Endereço IP",
"date": "Data"
},
"actions": {
"view": "Visualizar",
"mark_unread": "Marcar como não lido",
"mark_read": "Marcar como lido",
"delete": "Excluir",
"save_response": "Salvar Resposta",
"back": "Voltar"
},
"placeholders": {
"response": "Digite sua resposta aqui..."
},
"no_messages": "Nenhuma mensagem encontrada.",
"confirm_delete": "Tem certeza que deseja excluir esta mensagem?",
"error_occurred": "Ocorreu um erro."
}
}
+40
View File
@@ -0,0 +1,40 @@
{
"name": "contact",
"display_name": "Contact",
"description": "Contact form and message management module",
"version": "1.0.0",
"author": "SysCMS Team",
"dependencies": [
"auth"
],
"permissions": [
"contact.view",
"contact.create",
"contact.edit",
"contact.delete"
],
"settings": [],
"routes": {
"web": "routes/web.php"
},
"menu": {
"dashboard": {
"icon": "bi bi-envelope",
"label": "contact.menu.dashboard",
"route": "admin/contacts",
"order": 40
}
},
"views": "views",
"providers": [
"Modules\\Contact\\Providers\\ContactServiceProvider"
],
"locales": "locales",
"module_ative": true,
"module_public": true,
"module_blocked": false,
"ready": true,
"tables": [
"contacts"
]
}
+44
View File
@@ -0,0 +1,44 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2025 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
use Kernel\Application;
/**
* Register contact module routes
*
* @param Application $app
* @return void
*/
return function (Application $app) {
// Public contact form
$app->addRoute('GET', '/contact', [\Modules\Contact\Controllers\ContactController::class, 'index']);
$app->addRoute('POST', '/contact/submit', [\Modules\Contact\Controllers\ContactController::class, 'submit']);
// Admin contact management
$app->addRoute('GET', '/admin/contacts', [\Modules\Contact\Controllers\ContactController::class, 'list']);
$app->addRoute('GET', '/admin/contacts/{id}', [\Modules\Contact\Controllers\ContactController::class, 'show']);
$app->addRoute('POST', '/admin/contacts/{id}/status', [\Modules\Contact\Controllers\ContactController::class, 'updateStatus']);
$app->addRoute('POST', '/admin/contacts/{id}/response', [\Modules\Contact\Controllers\ContactController::class, 'updateResponse']);
$app->addRoute('POST', '/admin/contacts/{id}/toggle-read', [\Modules\Contact\Controllers\ContactController::class, 'toggleRead']);
$app->addRoute('DELETE', '/admin/contacts/{id}', [\Modules\Contact\Controllers\ContactController::class, 'delete']);
};
+157
View File
@@ -0,0 +1,157 @@
{% extends "layouts/dashboard.twig" %}
{% block title %}{{ title }} - {{ platformName }}{% endblock %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">{{ title }}</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="{{ url('admin/dashboard') }}">{{ __('dashboard.title') }}</a></li>
<li class="breadcrumb-item active">{{ __('contact.admin.title') }}</li>
</ol>
<div class="mb-4">
<div class="btn-group" role="group">
<a href="{{ url('admin/contacts?status=all') }}" class="btn {{ current_status == 'all' ? 'btn-primary' : 'btn-outline-primary' }}">
{{ __('contact.admin.statuses.all') }}
</a>
<a href="{{ url('admin/contacts?status=unread') }}" class="btn {{ current_status == 'unread' ? 'btn-primary' : 'btn-outline-primary' }}">
{{ __('contact.admin.statuses.unread') }}
</a>
<a href="{{ url('admin/contacts?status=read') }}" class="btn {{ current_status == 'read' ? 'btn-primary' : 'btn-outline-primary' }}">
{{ __('contact.admin.statuses.read') }}
</a>
<a href="{{ url('admin/contacts?status=new') }}" class="btn {{ current_status == 'new' ? 'btn-primary' : 'btn-outline-primary' }}">
{{ __('contact.admin.statuses.new') }}
</a>
<a href="{{ url('admin/contacts?status=in_progress') }}" class="btn {{ current_status == 'in_progress' ? 'btn-primary' : 'btn-outline-primary' }}">
{{ __('contact.admin.statuses.in_progress') }}
</a>
<a href="{{ url('admin/contacts?status=resolved') }}" class="btn {{ current_status == 'resolved' ? 'btn-primary' : 'btn-outline-primary' }}">
{{ __('contact.admin.statuses.resolved') }}
</a>
<a href="{{ url('admin/contacts?status=closed') }}" class="btn {{ current_status == 'closed' ? 'btn-primary' : 'btn-outline-primary' }}">
{{ __('contact.admin.statuses.closed') }}
</a>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-envelope me-1"></i>
{{ __('contact.admin.list_title') }}
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>{{ __('contact.admin.columns.name') }}</th>
<th>{{ __('contact.admin.columns.email') }}</th>
<th>{{ __('contact.admin.columns.subject') }}</th>
<th>{{ __('contact.admin.columns.status') }}</th>
<th>{{ __('contact.admin.columns.date') }}</th>
<th>{{ __('contact.admin.columns.actions') }}</th>
</tr>
</thead>
<tbody>
{% for contact in contacts %}
<tr class="{{ not contact.isRead() ? 'table-primary' : '' }}">
<td>
{% if not contact.isRead() %}
<i class="bi bi-envelope-fill text-primary me-1"></i>
{% else %}
<i class="bi bi-envelope-open text-muted me-1"></i>
{% endif %}
{{ contact.contacts_name }}
</td>
<td>{{ contact.contacts_email }}</td>
<td>{{ contact.contacts_subject }}</td>
<td>
{% set statusColors = {
'new': 'primary',
'in_progress': 'warning',
'resolved': 'success',
'closed': 'secondary'
} %}
<span class="badge bg-{{ statusColors[contact.contacts_status] ?? 'secondary' }}">
{{ __('contact.admin.statuses.' ~ contact.contacts_status) }}
</span>
</td>
<td>{{ contact.formatted_created_at }}</td>
<td>
<a href="{{ url('admin/contacts/' ~ contact.contacts_id) }}" class="btn btn-info btn-sm" title="{{ __('contact.admin.actions.view') }}">
<i class="bi bi-eye"></i>
</a>
<button type="button" class="btn btn-sm {{ contact.isRead() ? 'btn-secondary' : 'btn-success' }}"
onclick="toggleReadStatus({{ contact.contacts_id }})"
title="{{ contact.isRead() ? __('contact.admin.actions.mark_unread') : __('contact.admin.actions.mark_read') }}">
<i class="bi {{ contact.isRead() ? 'bi-envelope' : 'bi-envelope-check' }}"></i>
</button>
<button type="button" class="btn btn-danger btn-sm"
onclick="deleteContact({{ contact.contacts_id }})"
title="{{ __('contact.admin.actions.delete') }}">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
{% else %}
<tr>
<td colspan="6" class="text-center">{{ __('contact.admin.no_messages') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
function toggleReadStatus(id) {
fetch(`{{ url('admin/contacts/') }}${id}/toggle-read`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert(data.message || '{{ __("contact.admin.error_occurred") }}');
}
})
.catch(error => {
console.error('Error:', error);
alert('{{ __("contact.admin.error_occurred") }}');
});
}
function deleteContact(id) {
if (confirm("{{ __('contact.admin.confirm_delete') }}")) {
fetch(`{{ url('admin/contacts/') }}${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert(data.message || '{{ __("contact.admin.error_occurred") }}');
}
})
.catch(error => {
console.error('Error:', error);
alert('{{ __("contact.admin.error_occurred") }}');
});
}
}
</script>
{% endblock %}
+212
View File
@@ -0,0 +1,212 @@
{% extends "layouts/dashboard.twig" %}
{% block title %}{{ title }} - {{ platformName }}{% endblock %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">{{ title }}</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="{{ url('admin/dashboard') }}">{{ __('dashboard.title') }}</a></li>
<li class="breadcrumb-item"><a href="{{ url('admin/contacts') }}">{{ __('contact.admin.title') }}</a></li>
<li class="breadcrumb-item active">{{ contact.contacts_subject }}</li>
</ol>
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<i class="bi bi-envelope me-1"></i>
{{ __('contact.admin.details_title') }}
</div>
<div>
{% set statusColors = {
'new': 'primary',
'in_progress': 'warning',
'resolved': 'success',
'closed': 'secondary'
} %}
<span class="badge bg-{{ statusColors[contact.contacts_status] ?? 'secondary' }}">
{{ __('contact.admin.statuses.' ~ contact.contacts_status) }}
</span>
</div>
</div>
<div class="card-body">
<div class="row mb-3">
<label class="col-sm-2 col-form-label fw-bold">{{ __('contact.admin.fields.name') }}</label>
<div class="col-sm-10">
<p class="form-control-plaintext">{{ contact.contacts_name }}</p>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label fw-bold">{{ __('contact.admin.fields.email') }}</label>
<div class="col-sm-10">
<p class="form-control-plaintext">
<a href="mailto:{{ contact.contacts_email }}">{{ contact.contacts_email }}</a>
</p>
</div>
</div>
{% if contact.contacts_phone %}
<div class="row mb-3">
<label class="col-sm-2 col-form-label fw-bold">{{ __('contact.admin.fields.phone') }}</label>
<div class="col-sm-10">
<p class="form-control-plaintext">{{ contact.contacts_phone }}</p>
</div>
</div>
{% endif %}
<div class="row mb-3">
<label class="col-sm-2 col-form-label fw-bold">{{ __('contact.admin.fields.subject') }}</label>
<div class="col-sm-10">
<p class="form-control-plaintext">{{ contact.contacts_subject }}</p>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label fw-bold">{{ __('contact.admin.fields.message') }}</label>
<div class="col-sm-10">
<div class="border rounded p-3 bg-light">
{{ contact.contacts_message|nl2br }}
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label fw-bold">{{ __('contact.admin.fields.response') }}</label>
<div class="col-sm-10">
<form action="{{ url('admin/contacts/' ~ contact.contacts_id ~ '/response') }}" method="POST">
<div class="mb-3">
<textarea class="form-control" id="response" name="response" rows="5" placeholder="{{ __('contact.admin.placeholders.response') }}">{{ contact.contacts_response }}</textarea>
</div>
<div class="text-end">
<button type="submit" class="btn btn-primary">
<i class="bi bi-reply me-1"></i> {{ __('contact.admin.actions.save_response') }}
</button>
</div>
</form>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label fw-bold">{{ __('contact.admin.fields.ip_address') }}</label>
<div class="col-sm-10">
<p class="form-control-plaintext">{{ contact.contacts_ip_address }}</p>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label fw-bold">{{ __('contact.admin.fields.date') }}</label>
<div class="col-sm-10">
<p class="form-control-plaintext">{{ contact.formatted_created_at }}</p>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label fw-bold">{{ __('contact.admin.columns.status') }}</label>
<div class="col-sm-10">
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
{{ __('contact.admin.statuses.' ~ contact.contacts_status) }}
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="updateStatus('new')">{{ __('contact.admin.statuses.new') }}</a></li>
<li><a class="dropdown-item" href="#" onclick="updateStatus('in_progress')">{{ __('contact.admin.statuses.in_progress') }}</a></li>
<li><a class="dropdown-item" href="#" onclick="updateStatus('resolved')">{{ __('contact.admin.statuses.resolved') }}</a></li>
<li><a class="dropdown-item" href="#" onclick="updateStatus('closed')">{{ __('contact.admin.statuses.closed') }}</a></li>
</ul>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-10 offset-sm-2">
<button type="button" class="btn btn-secondary me-2" onclick="toggleReadStatus()">
<i class="bi {{ contact.isRead() ? 'bi-envelope' : 'bi-envelope-check' }}"></i>
{{ contact.isRead() ? __('contact.admin.actions.mark_unread') : __('contact.admin.actions.mark_read') }}
</button>
<a href="{{ url('admin/contacts') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> {{ __('contact.admin.actions.back') }}
</a>
<button type="button" class="btn btn-danger float-end" onclick="deleteContact()">
<i class="bi bi-trash"></i> {{ __('contact.admin.actions.delete') }}
</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
function updateStatus(status) {
fetch(`{{ url('admin/contacts/') }}{{ contact.contacts_id }}/status`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
status: status
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert(data.message || '{{ __("contact.admin.error_occurred") }}');
}
})
.catch(error => {
console.error('Error:', error);
alert('{{ __("contact.admin.error_occurred") }}');
});
}
function toggleReadStatus() {
fetch(`{{ url('admin/contacts/') }}{{ contact.contacts_id }}/toggle-read`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert(data.message || '{{ __("contact.admin.error_occurred") }}');
}
})
.catch(error => {
console.error('Error:', error);
alert('{{ __("contact.admin.error_occurred") }}');
});
}
function deleteContact() {
if (confirm("{{ __('contact.admin.confirm_delete') }}")) {
fetch(`{{ url('admin/contacts/') }}{{ contact.contacts_id }}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.href = '{{ url("admin/contacts") }}';
} else {
alert(data.message || '{{ __("contact.admin.error_occurred") }}');
}
})
.catch(error => {
console.error('Error:', error);
alert('{{ __("contact.admin.error_occurred") }}');
});
}
}
</script>
{% endblock %}
+313
View File
@@ -0,0 +1,313 @@
{% extends "@index/layout.twig" %}
{% block title %}{{ __('contact.title') }} - {{ platformName }}{% endblock %}
{% block head %}
<style>
:root {
--primary-gradient: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 100%);
--ai-gradient: linear-gradient(135deg, #8b5cf6 0%, #d946ef 100%);
--dark-bg: #0f172a;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background: #f8fafc !important;
}
.contact-hero {
background: var(--dark-bg);
background-image:
radial-gradient(circle at 10% 20%, rgba(59, 130, 246, 0.1) 0%, transparent 40%),
radial-gradient(circle at 90% 80%, rgba(139, 92, 246, 0.1) 0%, transparent 40%);
color: white;
padding: 100px 0 60px;
text-align: center;
}
.contact-badge {
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.3);
color: #60a5fa;
padding: 8px 20px;
border-radius: 50px;
font-size: 0.9rem;
font-weight: 600;
display: inline-block;
margin-bottom: 20px;
}
.contact-title {
font-size: 3rem;
font-weight: 800;
margin-bottom: 20px;
}
.contact-card {
background: white;
border-radius: 24px;
border: 1px solid #e2e8f0;
overflow: hidden;
box-shadow: 0 20px 40px rgba(0,0,0,0.05);
margin-top: -50px;
}
.contact-info-sidebar {
background: #f1f5f9;
padding: 40px;
height: 100%;
}
.info-item {
display: flex;
gap: 20px;
margin-bottom: 30px;
}
.info-icon {
width: 48px;
height: 48px;
background: white;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
color: #3b82f6;
box-shadow: 0 4px 10px rgba(0,0,0,0.05);
flex-shrink: 0;
}
.info-content h5 {
font-weight: 700;
margin-bottom: 5px;
color: #0f172a;
}
.info-content p {
color: #64748b;
margin-bottom: 0;
}
.contact-form-body {
padding: 40px;
}
.form-label {
font-weight: 600;
color: #334155;
margin-bottom: 8px;
}
.form-control {
border-radius: 12px;
padding: 12px 16px;
border: 1px solid #e2e8f0;
transition: all 0.3s;
}
.form-control:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
}
.btn-submit {
background: var(--primary-gradient);
color: white;
border: none;
padding: 14px 30px;
border-radius: 12px;
font-weight: 700;
transition: all 0.3s;
width: 100%;
}
.btn-submit:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(59, 130, 246, 0.3);
color: white;
}
.captcha-box {
background: #f8fafc;
border-radius: 12px;
padding: 15px;
border: 1px dashed #cbd5e1;
margin-bottom: 20px;
}
.alert-toast {
position: fixed;
top: 20px;
right: 20px;
z-index: 1060;
min-width: 300px;
}
</style>
{% endblock %}
{% block layout_body %}
<div class="alert-toast" x-data="{ show: false, message: '', type: 'success' }"
@show-alert.window="show = true; message = $event.detail.message; type = $event.detail.type; setTimeout(() => show = false, 5000)">
<div x-show="show" x-transition
:class="`alert alert-${type} shadow-lg border-0 rounded-4`" role="alert">
<div class="d-flex align-items-center gap-2">
<i class="bi" :class="type === 'success' ? 'bi-check-circle-fill' : 'bi-exclamation-triangle-fill'"></i>
<span x-text="message"></span>
</div>
</div>
</div>
<section class="contact-hero">
<div class="container">
<div class="contact-badge">Contato</div>
<h1 class="contact-title">{{ __('contact.title') }}</h1>
<p class="lead opacity-75 mx-auto" style="max-width: 600px;">
{{ __('contact.subtitle') }}
</p>
</div>
</section>
<section class="pb-5">
<div class="container">
<div class="contact-card">
<div class="row g-0">
<div class="col-lg-5">
<div class="contact-info-sidebar">
<h3 class="fw-bold mb-4" style="color: #0f172a;">Informações</h3>
<div class="info-item">
<div class="info-icon"><i class="bi bi-geo-alt"></i></div>
<div class="info-content">
<h5>{{ __('contact.address_title') }}</h5>
<p>{{ __('contact.address_line1') }}<br>{{ __('contact.address_line2') }}</p>
</div>
</div>
<div class="info-item">
<div class="info-icon"><i class="bi bi-telephone"></i></div>
<div class="info-content">
<h5>{{ __('contact.phone_title') }}</h5>
<p>{{ __('contact.phone_number') }}<br><small>{{ __('contact.phone_hours') }}</small></p>
</div>
</div>
<div class="info-item">
<div class="info-icon"><i class="bi bi-envelope"></i></div>
<div class="info-content">
<h5>{{ __('contact.email_title') }}</h5>
<p>{{ __('contact.email_info') }}{% if __('contact.email_support') %}<br>{{ __('contact.email_support') }}{% endif %}</p>
</div>
</div>
</div>
</div>
<div class="col-lg-7">
<div class="contact-form-body" x-data="contactForm()">
<h3 class="fw-bold mb-4" style="color: #0f172a;">{{ __('contact.form_title') }}</h3>
<form @submit.prevent="submitForm">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">{{ __('contact.form_name') }}</label>
<input type="text" class="form-control" x-model="formData.name" required placeholder="Seu nome completo">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">{{ __('contact.form_email') }}</label>
<input type="email" class="form-control" x-model="formData.email" required placeholder="seu@email.com">
</div>
</div>
<div class="mb-3">
<label class="form-label">{{ __('contact.form_subject') }}</label>
<input type="text" class="form-control" x-model="formData.subject" required placeholder="Como podemos ajudar?">
</div>
<div class="mb-4">
<label class="form-label">{{ __('contact.form_message') }}</label>
<textarea class="form-control" rows="5" x-model="formData.message" required placeholder="Descreva sua dúvida ou sugestão..."></textarea>
</div>
{% if require_captcha_contact %}
<div class="captcha-box" x-data="{
num1: 0, num2: 0, userAnswer: '', solved: false,
generate() {
this.num1 = Math.floor(Math.random() * 10) + 1;
this.num2 = Math.floor(Math.random() * 10) + 1;
this.userAnswer = ''; this.solved = false;
},
check() { this.solved = (parseInt(this.userAnswer) === (this.num1 + this.num2)); }
}" x-init="generate()" @reset-captcha.window="generate()">
<label class="form-label d-block mb-2">{{ __('contact.security_check') }}</label>
<div class="d-flex align-items-center gap-3">
<div class="bg-white px-3 py-2 rounded border fw-bold">
<span x-text="num1"></span> + <span x-text="num2"></span> =
</div>
<input type="number" class="form-control" style="width: 100px;" x-model="userAnswer" @input="check()" placeholder="?">
<span x-show="solved" class="text-success"><i class="bi bi-check-circle-fill"></i></span>
</div>
<input type="hidden" name="captcha_valid" :value="solved ? '1' : ''">
</div>
{% endif %}
<button type="submit" class="btn-submit" :disabled="loading">
<span x-show="!loading"><i class="bi bi-send-fill me-2"></i> {{ __('contact.form_submit') }}</span>
<span x-show="loading" class="spinner-border spinner-border-sm me-2"></span>
<span x-show="loading">{{ __('contact.sending') }}</span>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block scripts %}
<script>
function contactForm() {
return {
formData: { name: '', email: '', subject: '', message: '' },
loading: false,
submitForm() {
const captchaInput = document.querySelector('input[name="captcha_valid"]');
if (captchaInput && captchaInput.value !== '1') {
window.dispatchEvent(new CustomEvent('show-alert', {
detail: { message: '{{ __("contact.js_invalid_captcha") }}', type: 'danger' }
}));
return;
}
this.loading = true;
// Inclui o captcha_valid no corpo da requisição JSON para o backend
const payload = { ...this.formData };
if (captchaInput) {
payload.captcha_valid = captchaInput.value;
}
fetch('/contact/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify(payload)
})
.then(res => res.json())
.then(data => {
if (data.success) {
window.dispatchEvent(new CustomEvent('show-alert', {
detail: { message: '{{ __("contact.js_success") }}', type: 'success' }
}));
this.formData = { name: '', email: '', subject: '', message: '' };
window.dispatchEvent(new CustomEvent('reset-captcha'));
} else {
throw new Error(data.message);
}
})
.catch(err => {
window.dispatchEvent(new CustomEvent('show-alert', {
detail: { message: err.message || '{{ __("contact.js_error") }}', type: 'danger' }
}));
})
.finally(() => { this.loading = false; });
}
}
}
</script>
{% endblock %}