diff --git a/backend/app/controllers/public/contacts_controller.rb b/backend/app/controllers/public/contacts_controller.rb
index 7e07e40..f7a3586 100644
--- a/backend/app/controllers/public/contacts_controller.rb
+++ b/backend/app/controllers/public/contacts_controller.rb
@@ -7,7 +7,7 @@ module Public
def create
result = Contacts::SubmitInquiry.call(params: inquiry_params, ip: request.remote_ip)
- if result.ok?
+ if result.ok
redirect_to public_contatti_path, notice: t("flash.contacts.sent")
return
end
diff --git a/backend/app/views/layouts/marketing.html.erb b/backend/app/views/layouts/marketing.html.erb
index 3fe0a0b..a6f3296 100644
--- a/backend/app/views/layouts/marketing.html.erb
+++ b/backend/app/views/layouts/marketing.html.erb
@@ -8,7 +8,7 @@
<%= render "shared/meta_tags" %>
<%= yield :head %>
-
+
data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>>
<%= render "shared/cookie_banner" %>
diff --git a/backend/app/views/layouts/marketing_live.html.erb b/backend/app/views/layouts/marketing_live.html.erb
index 1cb99aa..8544ed2 100644
--- a/backend/app/views/layouts/marketing_live.html.erb
+++ b/backend/app/views/layouts/marketing_live.html.erb
@@ -6,7 +6,7 @@
<%= content_for?(:title) ? yield(:title) : "Match Live TV" %>
<%= render "shared/meta_tags" %>
-
+
<%= yield :head %>
diff --git a/backend/public/marketing.css b/backend/public/marketing.css
index aae7bbc..87cb9a3 100644
--- a/backend/public/marketing.css
+++ b/backend/public/marketing.css
@@ -3381,3 +3381,87 @@ a.replay-archive__thumb:hover {
border-color: #e53935;
background: rgba(229, 57, 53, 0.16);
}
+
+.contacts-hero h1 {
+ max-width: 22ch;
+}
+.contacts-hero__meta {
+ margin: 0;
+ color: #888;
+ font-size: 0.92rem;
+}
+.contacts-channels {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 16px;
+}
+.contacts-channels .feature-card h2 {
+ margin: 0 0 8px;
+ font-size: 1.05rem;
+}
+.contacts-channels__email {
+ margin: 12px 0 0;
+}
+.contacts-channels__email a {
+ color: #e53935;
+ font-weight: 600;
+ word-break: break-all;
+}
+.contacts-company__details {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ text-align: center;
+ color: #ccc;
+ line-height: 1.7;
+}
+.contacts-form-panel {
+ max-width: 640px;
+ margin: 0 auto;
+ background: #14141c;
+ border: 1px solid #2a2a36;
+ border-radius: 14px;
+ padding: 28px 24px;
+}
+.contacts-form-panel h2 { margin: 0 0 10px; }
+.contacts-form-panel__lead {
+ margin: 0 0 20px;
+ color: #aaa;
+ line-height: 1.55;
+}
+.contacts-form textarea {
+ width: 100%;
+ padding: 12px 14px;
+ margin-bottom: 12px;
+ border-radius: 8px;
+ border: 1px solid #3d3d4a;
+ background: #1c1c26;
+ color: #fff;
+ font-size: 1rem;
+ font-family: inherit;
+ resize: vertical;
+ min-height: 140px;
+ box-sizing: border-box;
+}
+.contacts-form textarea:focus {
+ outline: none;
+ border-color: #e53935;
+ box-shadow: 0 0 0 2px rgba(229, 57, 53, 0.25);
+}
+.contacts-form .btn-primary {
+ width: 100%;
+ margin-top: 8px;
+ padding: 14px;
+}
+.contacts-hp {
+ position: absolute;
+ left: -10000px;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+}
+@media (max-width: 999px) {
+ .contacts-channels {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/backend/spec/mailers/contact_mailer_spec.rb b/backend/spec/mailers/contact_mailer_spec.rb
new file mode 100644
index 0000000..99960ad
--- /dev/null
+++ b/backend/spec/mailers/contact_mailer_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe ContactMailer, type: :mailer do
+ it "invia la richiesta commerciale al supporto con reply-to" do
+ mail = described_class.inquiry(
+ name: "Anna Bianchi",
+ email: "anna@test.it",
+ club_name: "ASD Test",
+ topic: "commercial",
+ message: "Serve un piano per due palestre."
+ )
+
+ expect(mail.to).to eq([MatchLiveTv.support_email])
+ expect(mail.reply_to).to eq(["anna@test.it"])
+ expect(mail.subject).to include("Anna Bianchi")
+ expect(mail.body.encoded).to include("ASD Test")
+ expect(mail.body.encoded).to include("due palestre")
+ end
+
+ it "invia la richiesta privacy all'indirizzo privacy" do
+ mail = described_class.inquiry(
+ name: "Luca Verdi",
+ email: "luca@test.it",
+ club_name: "",
+ topic: "privacy",
+ message: "Chiedo la cancellazione dei dati."
+ )
+
+ expect(mail.to).to eq([MatchLiveTv.privacy_controller_email])
+ expect(mail.body.encoded).to include(I18n.t("mailers.contact.club_none", locale: :it))
+ end
+end
diff --git a/backend/spec/requests/public/contacts_spec.rb b/backend/spec/requests/public/contacts_spec.rb
new file mode 100644
index 0000000..7c47012
--- /dev/null
+++ b/backend/spec/requests/public/contacts_spec.rb
@@ -0,0 +1,149 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe "Public contacts page", type: :request do
+ let(:valid_params) do
+ {
+ name: "Mario Rossi",
+ email: "mario@test.it",
+ club_name: "Volley Club",
+ topic: "commercial",
+ message: "Vorremmo un preventivo per tre squadre della società.",
+ accept_privacy: "1"
+ }
+ end
+
+ it "è pubblica, indicizzabile e mostra i recapiti" do
+ get public_contatti_path
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include(I18n.t("pages.contacts.title", locale: :it))
+ expect(response.body).to include(MatchLiveTv.support_email)
+ expect(response.body).to include(MatchLiveTv.privacy_controller_email)
+ expect(response.body).to include(MatchLiveTv.privacy_controller_name)
+ expect(response.body).to include(MatchLiveTv.privacy_controller_address)
+ expect(response.body).to include('rel="canonical"')
+ expect(response.body).to include(public_contatti_path)
+ expect(response.body).to include(public_support_path)
+ expect(response.body).to include(public_faq_path)
+ expect(response.body).to include(public_signup_path)
+ expect(response.body).not_to include("translation missing")
+ end
+
+ it "appare nel footer del sito, non nel chrome App Store" do
+ get root_path
+ expect(response.body).to include(public_contatti_path)
+
+ get public_support_path
+ expect(response.body).not_to include(public_contatti_path)
+ end
+
+ it "collega il CTA Prezzi alla pagina contatti" do
+ load Rails.root.join("db/seeds/plans.rb")
+ get public_prezzi_path
+
+ expect(response.body).to include(public_contatti_path)
+ expect(response.body).not_to include("mailto:info@matchlivetv.it")
+ end
+
+ it "include /contatti nella sitemap e reindirizza /contact" do
+ get "/sitemap.xml"
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include("#{MatchLiveTv.app_public_url.chomp('/')}/contatti")
+
+ get "/contact"
+ expect(response).to redirect_to("/contatti")
+ end
+
+ {
+ "it" => "Contatti",
+ "en" => "Contact",
+ "fr" => "Contact",
+ "de" => "Kontakt",
+ "es" => "Contacto"
+ }.each do |locale, heading|
+ it "renderizza correttamente in #{locale} senza translation missing" do
+ cookies[:mltv_locale] = locale
+ get public_contatti_path
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include(heading)
+ expect(response.body).not_to include("translation missing")
+ end
+ end
+
+ it "invia la mail commerciale a info@ con reply-to del mittente" do
+ ActionMailer::Base.deliveries.clear
+
+ expect {
+ post public_contatti_path, params: valid_params
+ }.to change { ActionMailer::Base.deliveries.size }.by(1)
+
+ expect(response).to redirect_to(public_contatti_path)
+ expect(flash[:notice]).to eq(I18n.t("flash.contacts.sent", locale: :it))
+
+ mail = ActionMailer::Base.deliveries.last
+ expect(mail.to).to eq([MatchLiveTv.support_email])
+ expect(mail.reply_to).to eq(["mario@test.it"])
+ expect(mail.subject).to include("Mario Rossi")
+ expect(mail.body.encoded).to include("Volley Club")
+ expect(mail.body.encoded).to include("tre squadre")
+ end
+
+ it "invia le richieste privacy all'email privacy" do
+ ActionMailer::Base.deliveries.clear
+
+ post public_contatti_path, params: valid_params.merge(topic: "privacy")
+
+ mail = ActionMailer::Base.deliveries.last
+ expect(mail.to).to eq([MatchLiveTv.privacy_controller_email])
+ expect(mail.subject).to include("privacy")
+ end
+
+ it "non invia mail se il honeypot è compilato, ma risponde come successo" do
+ ActionMailer::Base.deliveries.clear
+
+ expect {
+ post public_contatti_path, params: valid_params.merge(website: "http://spam.test")
+ }.not_to change { ActionMailer::Base.deliveries.size }
+
+ expect(response).to redirect_to(public_contatti_path)
+ end
+
+ it "rifiuta un modulo incompleto" do
+ ActionMailer::Base.deliveries.clear
+
+ post public_contatti_path, params: valid_params.merge(name: "", accept_privacy: "1")
+
+ expect(response).to have_http_status(:unprocessable_entity)
+ expect(ActionMailer::Base.deliveries).to be_empty
+ expect(flash[:alert]).to eq(I18n.t("flash.contacts.invalid", locale: :it))
+ end
+
+ it "rifiuta senza consenso privacy" do
+ post public_contatti_path, params: valid_params.merge(accept_privacy: "0")
+
+ expect(response).to have_http_status(:unprocessable_entity)
+ expect(flash[:alert]).to eq(I18n.t("flash.contacts.privacy_required", locale: :it))
+ end
+
+ it "limita gli invii ripetuti dallo stesso IP" do
+ previous = Rails.cache
+ Rails.cache = ActiveSupport::Cache::MemoryStore.new
+ ActionMailer::Base.deliveries.clear
+
+ Contacts::SubmitInquiry::LIMIT.times do
+ post public_contatti_path, params: valid_params
+ expect(response).to redirect_to(public_contatti_path)
+ end
+
+ post public_contatti_path, params: valid_params.merge(email: "altro@test.it")
+
+ expect(response).to have_http_status(:too_many_requests)
+ expect(flash[:alert]).to eq(I18n.t("flash.contacts.throttled", locale: :it))
+ expect(ActionMailer::Base.deliveries.size).to eq(Contacts::SubmitInquiry::LIMIT)
+ ensure
+ Rails.cache = previous
+ end
+end