From 43d5003de881e213f482c4db462eb028c8c8bfad Mon Sep 17 00:00:00 2001 From: Emiliano Frascaro Date: Thu, 20 Aug 2026 15:39:11 +0200 Subject: [PATCH] Completa UI e test del form contatti. Co-authored-by: Cursor --- .../controllers/public/contacts_controller.rb | 2 +- backend/app/views/layouts/marketing.html.erb | 2 +- .../app/views/layouts/marketing_live.html.erb | 2 +- backend/public/marketing.css | 84 ++++++++++ backend/spec/mailers/contact_mailer_spec.rb | 34 ++++ backend/spec/requests/public/contacts_spec.rb | 149 ++++++++++++++++++ 6 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 backend/spec/mailers/contact_mailer_spec.rb create mode 100644 backend/spec/requests/public/contacts_spec.rb 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