new: usr: add Contact page with email sending behaviour #4
All checks were successful
Deploy to Production / deploy (push) Successful in 39s
All checks were successful
Deploy to Production / deploy (push) Successful in 39s
This commit is contained in:
@@ -180,6 +180,41 @@
|
|||||||
input[type="checkbox"] { accent-color: #236f87; }
|
input[type="checkbox"] { accent-color: #236f87; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-checkbox {
|
||||||
|
accent-color: #236f87;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
font: 400 14px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #95cff5;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: color 180ms;
|
||||||
|
|
||||||
|
&:hover { color: #c5e8ff; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.auth-input {
|
||||||
|
padding: 11px 14px;
|
||||||
|
min-height: 120px;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.auth-submit {
|
.auth-submit {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
83
assets/js/components/ContactForm.jsx
Normal file
83
assets/js/components/ContactForm.jsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ContactForm Component
|
||||||
|
*
|
||||||
|
* Handles reCAPTCHA v3 integration for the contact form.
|
||||||
|
* Intercepts form submission, executes reCAPTCHA, and submits the form with the token.
|
||||||
|
*
|
||||||
|
* @param {string} siteKey - Google reCAPTCHA site key
|
||||||
|
* @param {string} recaptchaFieldId - ID of the hidden recaptcha input field
|
||||||
|
*/
|
||||||
|
const ContactForm = ({ siteKey, recaptchaFieldId }) => {
|
||||||
|
const formRef = useRef(null);
|
||||||
|
const isSubmittingRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const form = document.querySelector('.auth-form');
|
||||||
|
|
||||||
|
if (!form) {
|
||||||
|
console.warn('ContactForm: No .auth-form found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formRef.current = form;
|
||||||
|
|
||||||
|
const handleSubmit = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (isSubmittingRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmittingRef.current = true;
|
||||||
|
|
||||||
|
if ('undefined' !== typeof grecaptcha) {
|
||||||
|
grecaptcha.ready(() => {
|
||||||
|
grecaptcha
|
||||||
|
.execute(siteKey, { action: 'contact' })
|
||||||
|
.then(token => {
|
||||||
|
const recaptchaField = document.getElementById(recaptchaFieldId);
|
||||||
|
|
||||||
|
if (recaptchaField) {
|
||||||
|
recaptchaField.value = token;
|
||||||
|
} else {
|
||||||
|
console.error(`ContactForm: Recaptcha field with ID "${recaptchaFieldId}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmittingRef.current = false;
|
||||||
|
form.submit();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('ContactForm: reCAPTCHA execution failed', error);
|
||||||
|
isSubmittingRef.current = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('ContactForm: grecaptcha is not loaded');
|
||||||
|
isSubmittingRef.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
form.addEventListener('submit', handleSubmit);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (formRef.current) {
|
||||||
|
formRef.current.removeEventListener('submit', handleSubmit);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [siteKey, recaptchaFieldId]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactForm;
|
||||||
31
assets/js/contact.jsx
Normal file
31
assets/js/contact.jsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import ContactForm from './components/ContactForm';
|
||||||
|
|
||||||
|
const wrapper = document.getElementById('contact-form-wrapper');
|
||||||
|
|
||||||
|
if (wrapper) {
|
||||||
|
const siteKey = wrapper.dataset.siteKey;
|
||||||
|
const recaptchaFieldId = wrapper.dataset.recaptchaFieldId;
|
||||||
|
|
||||||
|
if (siteKey && recaptchaFieldId) {
|
||||||
|
createRoot(wrapper).render(
|
||||||
|
<ContactForm
|
||||||
|
siteKey={siteKey}
|
||||||
|
recaptchaFieldId={recaptchaFieldId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error('ContactForm: Missing siteKey or recaptchaFieldId in data attributes');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -10,10 +10,19 @@
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\ContactMessage;
|
||||||
|
use App\Form\ContactFormType;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use RuntimeException;
|
||||||
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Attribute\AsController;
|
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,11 +40,14 @@ class GameController extends AbstractController
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
#[Autowire(env: 'APP_ENV')]
|
#[Autowire(env: 'APP_ENV')]
|
||||||
private readonly string $env,
|
private readonly string $env,
|
||||||
#[Autowire(env: 'MERCURE_PUBLIC_URL')]
|
#[Autowire(env: 'MERCURE_PUBLIC_URL')]
|
||||||
private readonly string $mercurePublicUrl,
|
private readonly string $mercurePublicUrl,
|
||||||
#[Autowire(env: 'MERCURE_SUBSCRIBER_JWT')]
|
#[Autowire(env: 'MERCURE_SUBSCRIBER_JWT')]
|
||||||
private readonly string $mercureSubscriberJwt,
|
private readonly string $mercureSubscriberJwt,
|
||||||
|
#[Autowire(env: 'APP_CONTACT_MAIL_ADDRESS')]
|
||||||
|
private readonly string $appContactMailAddress,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,9 +81,28 @@ class GameController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/contact', name: 'MineSeekerBundle_contact')]
|
#[Route('/contact', name: 'MineSeekerBundle_contact')]
|
||||||
public function contact(): Response
|
public function contact(
|
||||||
{
|
Request $request,
|
||||||
return $this->render('Official/contact.html.twig');
|
EntityManagerInterface $em,
|
||||||
|
MailerInterface $mailer,
|
||||||
|
): Response {
|
||||||
|
$contactMessage = new ContactMessage();
|
||||||
|
$form = $this->createForm(ContactFormType::class, $contactMessage);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$contactMessage->setIpAddress($request->getClientIp());
|
||||||
|
$em->persist($contactMessage);
|
||||||
|
$em->flush();
|
||||||
|
$this->sendMail($mailer, $contactMessage);
|
||||||
|
$this->addFlash('contact_success', 'Thank you for your message! We will get back to you soon.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('MineSeekerBundle_contact');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('Official/contact.html.twig', [
|
||||||
|
'form' => $form,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/landing-page', name: 'MineSeekerBundle_landing')]
|
#[Route('/landing-page', name: 'MineSeekerBundle_landing')]
|
||||||
@@ -79,4 +110,31 @@ class GameController extends AbstractController
|
|||||||
{
|
{
|
||||||
return $this->render('Official/landing.html.twig');
|
return $this->render('Official/landing.html.twig');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function sendMail(MailerInterface $mailer, ContactMessage $contactMessage): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$mailer->send(
|
||||||
|
new TemplatedEmail()
|
||||||
|
->from('noreply@mineseeker.hu')
|
||||||
|
->to($this->appContactMailAddress)
|
||||||
|
->replyTo($contactMessage->getEmail())
|
||||||
|
->subject('New Contact Message from ' . $contactMessage->getName())
|
||||||
|
->htmlTemplate('emails/contact_notification.html.twig')
|
||||||
|
->context(['message' => $contactMessage])
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->logger->error('Failed to send contact notification email: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'message' => $contactMessage,
|
||||||
|
]);
|
||||||
|
throw new RuntimeException('Failed to send contact notification email: ' . $e->getMessage());
|
||||||
|
} catch (TransportExceptionInterface $e) {
|
||||||
|
$this->logger->error('Failed to send contact notification email: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'message' => $contactMessage,
|
||||||
|
]);
|
||||||
|
throw new RuntimeException('Failed to send contact notification email: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
128
src/Entity/ContactMessage.php
Normal file
128
src/Entity/ContactMessage.php
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\ContactMessageRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping\Column;
|
||||||
|
use Doctrine\ORM\Mapping\Entity;
|
||||||
|
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||||
|
use Doctrine\ORM\Mapping\Id;
|
||||||
|
use Doctrine\ORM\Mapping\Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ContactMessage
|
||||||
|
*
|
||||||
|
* @package App\Entity
|
||||||
|
* @author Lang <https://www.splendidbear.org>
|
||||||
|
* @category Class
|
||||||
|
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
||||||
|
* @link www.splendidbear.org
|
||||||
|
* @since 2026. 04. 15.
|
||||||
|
*/
|
||||||
|
#[Entity(repositoryClass: ContactMessageRepository::class)]
|
||||||
|
#[Table(name: 'contact_messages')]
|
||||||
|
class ContactMessage
|
||||||
|
{
|
||||||
|
#[Id, GeneratedValue, Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[Column]
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
#[Column]
|
||||||
|
private string $email;
|
||||||
|
|
||||||
|
#[Column(type: Types::TEXT)]
|
||||||
|
private string $content;
|
||||||
|
|
||||||
|
#[Column]
|
||||||
|
private bool $consent = false;
|
||||||
|
|
||||||
|
#[Column]
|
||||||
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
#[Column(length: 45, nullable: true)]
|
||||||
|
private ?string $ipAddress = null;
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->createdAt = new DateTimeImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): self
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmail(): string
|
||||||
|
{
|
||||||
|
return $this->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEmail(string $email): self
|
||||||
|
{
|
||||||
|
$this->email = $email;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContent(): string
|
||||||
|
{
|
||||||
|
return $this->content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setContent(string $content): self
|
||||||
|
{
|
||||||
|
$this->content = $content;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isConsent(): bool
|
||||||
|
{
|
||||||
|
return $this->consent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setConsent(bool $consent): self
|
||||||
|
{
|
||||||
|
$this->consent = $consent;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIpAddress(): ?string
|
||||||
|
{
|
||||||
|
return $this->ipAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIpAddress(?string $ipAddress): self
|
||||||
|
{
|
||||||
|
$this->ipAddress = $ipAddress;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
89
src/Form/ContactFormType.php
Normal file
89
src/Form/ContactFormType.php
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Entity\ContactMessage;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Validator\Constraints\Email;
|
||||||
|
use Symfony\Component\Validator\Constraints\IsTrue;
|
||||||
|
use Symfony\Component\Validator\Constraints\Length;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ContactFormType
|
||||||
|
*
|
||||||
|
* @package App\Form
|
||||||
|
* @author Lang <https://www.splendidbear.org>
|
||||||
|
* @category Class
|
||||||
|
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
||||||
|
* @link www.splendidbear.org
|
||||||
|
* @since 2026. 04. 15.
|
||||||
|
*/
|
||||||
|
class ContactFormType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('name', TextType::class, [
|
||||||
|
'label' => 'Name',
|
||||||
|
'constraints' => [
|
||||||
|
new NotBlank(message: 'Please enter your name.'),
|
||||||
|
new Length(
|
||||||
|
min: 2,
|
||||||
|
max: 255,
|
||||||
|
minMessage: 'Name must be at least {{ limit }} characters.',
|
||||||
|
maxMessage: 'Name cannot be longer than {{ limit }} characters.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('email', EmailType::class, [
|
||||||
|
'label' => 'Email',
|
||||||
|
'constraints' => [
|
||||||
|
new NotBlank(message: 'Please enter your email address.'),
|
||||||
|
new Email(message: 'Please enter a valid email address.'),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('content', TextareaType::class, [
|
||||||
|
'label' => 'Message',
|
||||||
|
'constraints' => [
|
||||||
|
new NotBlank(message: 'Please enter your message.'),
|
||||||
|
new Length(
|
||||||
|
min: 10,
|
||||||
|
max: 5000,
|
||||||
|
minMessage: 'Message must be at least {{ limit }} characters.',
|
||||||
|
maxMessage: 'Message cannot be longer than {{ limit }} characters.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('consent', CheckboxType::class, [
|
||||||
|
'label' => 'I have read the Privacy and Data Processing Policy and I consent to the processing of my data.',
|
||||||
|
'mapped' => true,
|
||||||
|
'constraints' => [
|
||||||
|
new IsTrue(message: 'You must agree to the privacy policy to submit this form.'),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('recaptcha', RecaptchaType::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => ContactMessage::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
47
src/Migrations/2026/04/Version20260415160446.php
Normal file
47
src/Migrations/2026/04/Version20260415160446.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Migrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Version20260415160446
|
||||||
|
*
|
||||||
|
* @package App\Migrations
|
||||||
|
* @author Lang <https://www.splendidbear.org>
|
||||||
|
* @category Class
|
||||||
|
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
||||||
|
* @link www.splendidbear.org
|
||||||
|
* @since 2026. 04. 15.
|
||||||
|
*/
|
||||||
|
final class Version20260415160446 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add contact mail storage support';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE SEQUENCE contact_messages_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||||
|
$this->addSql('CREATE TABLE contact_messages (id INT NOT NULL, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, content TEXT NOT NULL, consent BOOLEAN NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, ip_address VARCHAR(45) DEFAULT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('COMMENT ON COLUMN contact_messages.created_at IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('ALTER INDEX played_game_uuid_unique RENAME TO UNIQ_54BE8039D17F50A6');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP SEQUENCE contact_messages_id_seq CASCADE');
|
||||||
|
$this->addSql('DROP TABLE contact_messages');
|
||||||
|
$this->addSql('ALTER INDEX uniq_54be8039d17f50a6 RENAME TO played_game_uuid_unique');
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/Repository/ContactMessageRepository.php
Normal file
35
src/Repository/ContactMessageRepository.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\ContactMessage;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ContactMessageRepository
|
||||||
|
*
|
||||||
|
* @package App\Repository
|
||||||
|
* @author Lang <https://www.splendidbear.org>
|
||||||
|
* @category Class
|
||||||
|
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
||||||
|
* @link www.splendidbear.org
|
||||||
|
* @since 2026. 04. 15.
|
||||||
|
*
|
||||||
|
* @extends ServiceEntityRepository<ContactMessage>
|
||||||
|
*/
|
||||||
|
class ContactMessageRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, ContactMessage::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,8 +20,117 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="txt">
|
<div class="txt">
|
||||||
<h2>Contact and user support</h2>
|
<h2 style="text-align: center;">Contact and user support</h2>
|
||||||
<h3>Under construction</h3>
|
|
||||||
<a href="mailto:langlasz@gmail.com">langlasz@gmail.com</a>
|
{% for message in app.flashes('contact_success') %}
|
||||||
|
<div class="auth-card auth-card--sent" style="margin: 20px auto; max-width: 600px;">
|
||||||
|
<div class="auth-sent-icon"><i class="far fa-envelope"></i></div>
|
||||||
|
<h3 style="color: #667eea; margin: 16px 0;">Message Sent!</h3>
|
||||||
|
<p class="auth-sent-note">{{ message }}</p>
|
||||||
|
<a href="{{ path('MineSeekerBundle_homepage') }}" class="auth-submit" style="text-decoration:none; margin-top:16px;">
|
||||||
|
Back to Home
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p style="text-align: center; color: #666; margin-bottom: 30px;">
|
||||||
|
Have a question, feedback, or need support? We'd love to hear from you!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="auth-card" style="max-width: 600px; margin: 0 auto;">
|
||||||
|
{{ form_start(form, {attr: {class: 'auth-form'}}) }}
|
||||||
|
|
||||||
|
<div class="auth-field">
|
||||||
|
<label for="{{ form.name.vars.id }}" class="auth-label">Name *</label>
|
||||||
|
<div class="auth-input-wrap">
|
||||||
|
<i class="fas fa-user auth-input-icon"></i>
|
||||||
|
{{ form_widget(form.name, {
|
||||||
|
attr: {
|
||||||
|
class: 'auth-input' ~ (not form.name.vars.valid ? ' auth-input--error' : ''),
|
||||||
|
placeholder: 'Your name',
|
||||||
|
autofocus: true,
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
|
</div>
|
||||||
|
{% if not form.name.vars.valid %}
|
||||||
|
{% for error in form.name.vars.errors %}
|
||||||
|
<p class="auth-field-error"><i class="fas fa-circle-exclamation"></i> {{ error.message }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="auth-field">
|
||||||
|
<label for="{{ form.email.vars.id }}" class="auth-label">Email *</label>
|
||||||
|
<div class="auth-input-wrap">
|
||||||
|
<i class="fas fa-envelope auth-input-icon"></i>
|
||||||
|
{{ form_widget(form.email, {
|
||||||
|
attr: {
|
||||||
|
class: 'auth-input' ~ (not form.email.vars.valid ? ' auth-input--error' : ''),
|
||||||
|
placeholder: 'your.email@example.com',
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
|
</div>
|
||||||
|
{% if not form.email.vars.valid %}
|
||||||
|
{% for error in form.email.vars.errors %}
|
||||||
|
<p class="auth-field-error"><i class="fas fa-circle-exclamation"></i> {{ error.message }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="auth-field">
|
||||||
|
<label for="{{ form.content.vars.id }}" class="auth-label">Message *</label>
|
||||||
|
<div class="auth-input-wrap">
|
||||||
|
<i class="fas fa-comment-dots auth-input-icon" style="top: 16px;"></i>
|
||||||
|
{{ form_widget(form.content, {
|
||||||
|
attr: {
|
||||||
|
class: 'auth-input' ~ (not form.content.vars.valid ? ' auth-input--error' : ''),
|
||||||
|
placeholder: 'Tell us what\'s on your mind...',
|
||||||
|
rows: 6,
|
||||||
|
style: 'min-height: 150px; resize: vertical;'
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
|
</div>
|
||||||
|
{% if not form.content.vars.valid %}
|
||||||
|
{% for error in form.content.vars.errors %}
|
||||||
|
<p class="auth-field-error"><i class="fas fa-circle-exclamation"></i> {{ error.message }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="auth-field">
|
||||||
|
<label class="auth-checkbox-label" style="display: flex; align-items: flex-start; cursor: pointer; user-select: none;">
|
||||||
|
{{ form_widget(form.consent, {
|
||||||
|
attr: {
|
||||||
|
class: 'auth-checkbox',
|
||||||
|
style: 'margin-right: 10px; margin-top: 3px;'
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
|
<span style="flex: 1; font-size: 14px; line-height: 1.5; color: #666;">
|
||||||
|
I have read the <a href="{{ path('MineSeekerBundle_privacy') }}" target="_blank" style="color: #667eea; text-decoration: none;">Privacy and Data Processing Policy</a> and I consent to the processing of my data. *
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{% if not form.consent.vars.valid %}
|
||||||
|
{% for error in form.consent.vars.errors %}
|
||||||
|
<p class="auth-field-error"><i class="fas fa-circle-exclamation"></i> {{ error.message }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="auth-submit">
|
||||||
|
<i class="fas fa-paper-plane"></i> Send Message
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{{ form_end(form) }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascripts %}
|
||||||
|
{{ parent() }}
|
||||||
|
<script src="https://www.google.com/recaptcha/api.js?render={{ recaptcha_site_key }}" async defer></script>
|
||||||
|
{{ vite_entry_script_tags('contact') }}
|
||||||
|
<div id="contact-form-wrapper"
|
||||||
|
data-site-key="{{ recaptcha_site_key }}"
|
||||||
|
data-recaptcha-field-id="{{ form.recaptcha.vars.id }}">
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
98
templates/emails/contact_notification.html.twig
Normal file
98
templates/emails/contact_notification.html.twig
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>New Contact Message</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.field-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.field-value {
|
||||||
|
background: white;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #667eea;
|
||||||
|
}
|
||||||
|
.message-content {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>📬 New Contact Message</h1>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="field">
|
||||||
|
<div class="field-label">From</div>
|
||||||
|
<div class="field-value">
|
||||||
|
<strong>{{ message.name }}</strong><br>
|
||||||
|
<a href="mailto:{{ message.email }}">{{ message.email }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<div class="field-label">Message</div>
|
||||||
|
<div class="field-value message-content">{{ message.content }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<div class="field-label">Details</div>
|
||||||
|
<div class="field-value">
|
||||||
|
<strong>Submitted:</strong> {{ message.createdAt|date('Y-m-d H:i:s') }}<br>
|
||||||
|
{% if message.ipAddress %}
|
||||||
|
<strong>IP Address:</strong> {{ message.ipAddress }}<br>
|
||||||
|
{% endif %}
|
||||||
|
<strong>Consent:</strong> {% if message.consent %}✓ Given{% else %}✗ Not given{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
This message was sent via the MineSeeker contact form.<br>
|
||||||
|
You can reply directly to this email to respond to {{ message.name }}.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ export default defineConfig({
|
|||||||
mineseeker: './assets/js/app.jsx',
|
mineseeker: './assets/js/app.jsx',
|
||||||
passkey: './assets/js/passkey.jsx',
|
passkey: './assets/js/passkey.jsx',
|
||||||
profile: './assets/js/profile.jsx',
|
profile: './assets/js/profile.jsx',
|
||||||
|
contact: './assets/js/contact.jsx',
|
||||||
mineseekerStyle: './assets/css/style.mineseeker.scss',
|
mineseekerStyle: './assets/css/style.mineseeker.scss',
|
||||||
homeStyle: './assets/css/style.layout.scss',
|
homeStyle: './assets/css/style.layout.scss',
|
||||||
passkeyStyle: './assets/css/passkey.scss',
|
passkeyStyle: './assets/css/passkey.scss',
|
||||||
|
|||||||
Reference in New Issue
Block a user