From 471291b2c451c748697f764a541af2beed4f0a67 Mon Sep 17 00:00:00 2001 From: Emiliano Frascaro Date: Tue, 26 May 2026 23:17:15 +0200 Subject: [PATCH] Sito marketing, SEO, legal, live programmata e branding Match Live Tv. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Logo e favicon nel header, partite programmate su web e mobile, reset password, pagine FAQ/sitemap, privacy/termini GDPR e icona Android «Match Live Tv». Co-authored-by: Cursor --- .../controllers/api/v1/matches_controller.rb | 2 +- .../app/controllers/public/live_controller.rb | 9 + .../controllers/public/pages_controller.rb | 6 + .../public/password_resets_controller.rb | 52 +++++ .../public/registrations_controller.rb | 6 + .../public/site_base_controller.rb | 2 + .../controllers/public/sitemap_controller.rb | 21 ++ backend/app/helpers/legal_helper.rb | 5 + backend/app/helpers/public/live_helper.rb | 18 ++ backend/app/helpers/seo_helper.rb | 33 +++ backend/app/mailers/application_mailer.rb | 2 +- backend/app/mailers/user_mailer.rb | 11 + backend/app/models/match.rb | 21 ++ backend/app/models/user.rb | 28 +++ backend/app/views/layouts/marketing.html.erb | 2 +- .../app/views/layouts/marketing_live.html.erb | 4 +- .../views/public/invitations/show.html.erb | 4 +- backend/app/views/public/live/index.html.erb | 195 ++++++++++++------ backend/app/views/public/live/show.html.erb | 4 +- backend/app/views/public/pages/faq.html.erb | 83 ++++++++ .../app/views/public/pages/features.html.erb | 47 ++--- backend/app/views/public/pages/home.html.erb | 29 ++- .../app/views/public/pages/pallavolo.html.erb | 67 ++++++ .../app/views/public/pages/pricing.html.erb | 4 +- .../app/views/public/pages/privacy.html.erb | 195 +++++++++++++++++- backend/app/views/public/pages/terms.html.erb | 139 ++++++++++++- .../public/password_resets/edit.html.erb | 17 ++ .../views/public/password_resets/new.html.erb | 15 ++ .../views/public/registrations/new.html.erb | 16 +- backend/app/views/public/replay/show.html.erb | 2 + .../app/views/public/sessions/new.html.erb | 5 +- backend/app/views/public/sitemap/show.xml.erb | 10 + .../app/views/public/teams/billing.html.erb | 1 + .../app/views/public/teams/dashboard.html.erb | 2 + .../app/views/public/teams/invite.html.erb | 1 + backend/app/views/public/teams/new.html.erb | 2 + .../views/shared/_marketing_footer.html.erb | 1 + .../app/views/shared/_marketing_nav.html.erb | 6 +- backend/app/views/shared/_meta_tags.html.erb | 20 +- .../views/user_mailer/password_reset.html.erb | 11 + .../views/user_mailer/password_reset.text.erb | 10 + backend/config/environments/development.rb | 10 +- backend/config/environments/production.rb | 17 ++ backend/config/initializers/match_live_tv.rb | 24 +++ backend/config/routes.rb | 7 + ...60527140000_add_password_reset_to_users.rb | 7 + backend/db/schema.rb | 5 +- backend/public/apple-touch-icon.png | Bin 0 -> 15175 bytes backend/public/favicon-32.png | Bin 0 -> 1597 bytes backend/public/live.css | 110 ++++++++++ backend/public/logo.png | Bin 0 -> 24398 bytes backend/public/marketing.css | 134 +++++++++++- backend/public/og-share.png | Bin 0 -> 107111 bytes backend/public/robots.txt | 25 ++- brand/logo-dark-gradient-256.png | Bin 0 -> 23388 bytes brand/logo-dark-gradient.png | Bin 0 -> 262834 bytes brand/logo-white-m-256.png | Bin 0 -> 24398 bytes brand/logo-white-m.png | Bin 0 -> 251220 bytes infra/.env.example | 6 + infra/.env.production.example | 16 ++ .../android/app/src/main/AndroidManifest.xml | 2 +- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 3497 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 2097 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 5097 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 9857 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 17533 bytes .../app/src/main/res/values/strings.xml | 1 + .../lib/features/matches/matches_screen.dart | 119 ++++++++++- .../matches/schedule_match_sheet.dart | 161 +++++++++++++++ mobile/lib/main.dart | 2 +- 70 files changed, 1601 insertions(+), 153 deletions(-) create mode 100644 backend/app/controllers/public/password_resets_controller.rb create mode 100644 backend/app/controllers/public/sitemap_controller.rb create mode 100644 backend/app/helpers/legal_helper.rb create mode 100644 backend/app/helpers/seo_helper.rb create mode 100644 backend/app/mailers/user_mailer.rb create mode 100644 backend/app/views/public/pages/faq.html.erb create mode 100644 backend/app/views/public/pages/pallavolo.html.erb create mode 100644 backend/app/views/public/password_resets/edit.html.erb create mode 100644 backend/app/views/public/password_resets/new.html.erb create mode 100644 backend/app/views/public/sitemap/show.xml.erb create mode 100644 backend/app/views/user_mailer/password_reset.html.erb create mode 100644 backend/app/views/user_mailer/password_reset.text.erb create mode 100644 backend/db/migrate/20260527140000_add_password_reset_to_users.rb create mode 100644 backend/public/apple-touch-icon.png create mode 100644 backend/public/favicon-32.png create mode 100644 backend/public/logo.png create mode 100644 backend/public/og-share.png create mode 100644 brand/logo-dark-gradient-256.png create mode 100644 brand/logo-dark-gradient.png create mode 100644 brand/logo-white-m-256.png create mode 100644 brand/logo-white-m.png create mode 100644 mobile/lib/features/matches/schedule_match_sheet.dart diff --git a/backend/app/controllers/api/v1/matches_controller.rb b/backend/app/controllers/api/v1/matches_controller.rb index 464ffd0..e38ed37 100644 --- a/backend/app/controllers/api/v1/matches_controller.rb +++ b/backend/app/controllers/api/v1/matches_controller.rb @@ -7,7 +7,7 @@ module Api def index matches = @team.matches .includes(:team, :stream_sessions) - .order(scheduled_at: :desc) + .order(Arel.sql("scheduled_at ASC NULLS LAST"), created_at: :desc) render json: matches.map { |m| match_json(m) } end diff --git a/backend/app/controllers/public/live_controller.rb b/backend/app/controllers/public/live_controller.rb index 0a1b85b..058f9ce 100644 --- a/backend/app/controllers/public/live_controller.rb +++ b/backend/app/controllers/public/live_controller.rb @@ -12,6 +12,15 @@ module Public .search_by_team_or_opponent(@query) .order(Arel.sql("started_at DESC NULLS LAST"), created_at: :desc) + busy_match_ids = StreamSession.where.not(status: %w[ended error]).select(:match_id) + @upcoming_matches = Match + .scheduled_upcoming + .includes(:team) + .search_teams_or_opponents(@query) + .where.not(id: busy_match_ids) + .order(scheduled_at: :asc) + .limit(30) + @online_paths = Mediamtx::Client.new.online_path_names end diff --git a/backend/app/controllers/public/pages_controller.rb b/backend/app/controllers/public/pages_controller.rb index 7e919b2..218151b 100644 --- a/backend/app/controllers/public/pages_controller.rb +++ b/backend/app/controllers/public/pages_controller.rb @@ -15,5 +15,11 @@ module Public def terms end + + def faq + end + + def pallavolo + end end end diff --git a/backend/app/controllers/public/password_resets_controller.rb b/backend/app/controllers/public/password_resets_controller.rb new file mode 100644 index 0000000..9bc15af --- /dev/null +++ b/backend/app/controllers/public/password_resets_controller.rb @@ -0,0 +1,52 @@ +module Public + class PasswordResetsController < WebBaseController + def new + end + + def create + user = User.find_by(email: params[:email]&.downcase&.strip) + if user + token = user.generate_password_reset! + UserMailer.password_reset(user, token).deliver_now + end + + redirect_to public_login_path, + notice: "Se l'email è registrata, riceverai a breve un link per reimpostare la password." + end + + def edit + @user = User.find_by_password_reset_token(params[:token]) + if @user.nil? || @user.password_reset_expired? + redirect_to new_public_password_reset_path, + alert: "Link non valido o scaduto. Richiedi un nuovo reset password." + return + end + @token = params[:token] + end + + def update + @user = User.find_by_password_reset_token(params[:token]) + if @user.nil? || @user.password_reset_expired? + redirect_to new_public_password_reset_path, + alert: "Link non valido o scaduto. Richiedi un nuovo reset password." + return + end + + if params[:password].blank? || params[:password].length < 8 + flash.now[:alert] = "La password deve avere almeno 8 caratteri" + @token = params[:token] + return render :edit, status: :unprocessable_entity + end + + if params[:password] != params[:password_confirmation] + flash.now[:alert] = "Le password non coincidono" + @token = params[:token] + return render :edit, status: :unprocessable_entity + end + + @user.update!(password: params[:password]) + @user.clear_password_reset! + redirect_to public_login_path, notice: "Password aggiornata. Ora puoi accedere." + end + end +end diff --git a/backend/app/controllers/public/registrations_controller.rb b/backend/app/controllers/public/registrations_controller.rb index 8e23d27..b92b811 100644 --- a/backend/app/controllers/public/registrations_controller.rb +++ b/backend/app/controllers/public/registrations_controller.rb @@ -6,6 +6,12 @@ module Public end def create + unless params[:accept_terms] == "1" + @user = User.new(user_params.merge(role: "coach")) + flash.now[:alert] = "Devi accettare l’informativa privacy e i termini di servizio per registrarti." + return render :new, status: :unprocessable_entity + end + @user = User.new(user_params.merge(role: "coach")) if @user.save session[:user_id] = @user.id diff --git a/backend/app/controllers/public/site_base_controller.rb b/backend/app/controllers/public/site_base_controller.rb index bd7ead4..1bce238 100644 --- a/backend/app/controllers/public/site_base_controller.rb +++ b/backend/app/controllers/public/site_base_controller.rb @@ -2,6 +2,8 @@ module Public class SiteBaseController < ActionController::Base layout "marketing" + include ::SeoHelper + include ::LegalHelper helper_method :current_user, :logged_in? private diff --git a/backend/app/controllers/public/sitemap_controller.rb b/backend/app/controllers/public/sitemap_controller.rb new file mode 100644 index 0000000..c9739c6 --- /dev/null +++ b/backend/app/controllers/public/sitemap_controller.rb @@ -0,0 +1,21 @@ +module Public + class SitemapController < ActionController::Base + def show + base = MatchLiveTv.app_public_url.chomp("/") + @entries = [ + { loc: "#{base}/", changefreq: "weekly", priority: "1.0" }, + { loc: "#{base}/funzionalita", changefreq: "monthly", priority: "0.9" }, + { loc: "#{base}/prezzi", changefreq: "monthly", priority: "0.9" }, + { loc: "#{base}/pallavolo-giovanile", changefreq: "monthly", priority: "0.85" }, + { loc: "#{base}/faq", changefreq: "monthly", priority: "0.8" }, + { loc: "#{base}/live", changefreq: "hourly", priority: "0.85" }, + { loc: "#{base}/privacy", changefreq: "yearly", priority: "0.3" }, + { loc: "#{base}/termini", changefreq: "yearly", priority: "0.3" } + ] + + respond_to do |format| + format.xml { render layout: false } + end + end + end +end diff --git a/backend/app/helpers/legal_helper.rb b/backend/app/helpers/legal_helper.rb new file mode 100644 index 0000000..b1d9db3 --- /dev/null +++ b/backend/app/helpers/legal_helper.rb @@ -0,0 +1,5 @@ +module LegalHelper + def legal_last_updated + "26 maggio 2026" + end +end diff --git a/backend/app/helpers/public/live_helper.rb b/backend/app/helpers/public/live_helper.rb index 8ad61e0..0e8ccd4 100644 --- a/backend/app/helpers/public/live_helper.rb +++ b/backend/app/helpers/public/live_helper.rb @@ -15,6 +15,24 @@ module Public partials.map { |p| format_set_partial_entry(p) }.join(" · ") end + def live_scheduled_label(datetime) + return nil unless datetime + + datetime.in_time_zone.strftime("%d/%m/%Y alle %H:%M") + end + + def live_scheduled_relative(datetime) + return nil unless datetime + + if datetime.to_date == Time.zone.today + "Oggi alle #{datetime.strftime('%H:%M')}" + elsif datetime.to_date == Time.zone.tomorrow + "Domani alle #{datetime.strftime('%H:%M')}" + else + live_scheduled_label(datetime) + end + end + def live_score_points_label(match, score_state) return "—" unless score_state diff --git a/backend/app/helpers/seo_helper.rb b/backend/app/helpers/seo_helper.rb new file mode 100644 index 0000000..bdede25 --- /dev/null +++ b/backend/app/helpers/seo_helper.rb @@ -0,0 +1,33 @@ +module SeoHelper + DEFAULT_OG_IMAGE_PATH = "/og-share.png" + + def seo_site_url + MatchLiveTv.app_public_url.chomp("/") + end + + def seo_absolute_url(path) + path = "/#{path}" unless path.start_with?("/") + "#{seo_site_url}#{path}" + end + + def seo_canonical_url + if content_for?(:canonical_url) + content_for(:canonical_url) + else + request.original_url.split("?").first + end + end + + def seo_og_image_url + if content_for?(:og_image) + img = content_for(:og_image) + img.start_with?("http") ? img : seo_absolute_url(img) + else + seo_absolute_url(DEFAULT_OG_IMAGE_PATH) + end + end + + def seo_robots_content + content_for?(:robots) ? content_for(:robots) : "index, follow" + end +end diff --git a/backend/app/mailers/application_mailer.rb b/backend/app/mailers/application_mailer.rb index 3c34c81..e2c7eb2 100644 --- a/backend/app/mailers/application_mailer.rb +++ b/backend/app/mailers/application_mailer.rb @@ -1,4 +1,4 @@ class ApplicationMailer < ActionMailer::Base - default from: "from@example.com" + default from: -> { MatchLiveTv.mail_from } layout "mailer" end diff --git a/backend/app/mailers/user_mailer.rb b/backend/app/mailers/user_mailer.rb new file mode 100644 index 0000000..0d53825 --- /dev/null +++ b/backend/app/mailers/user_mailer.rb @@ -0,0 +1,11 @@ +class UserMailer < ApplicationMailer + default from: -> { MatchLiveTv.mail_from } + + def password_reset(user, token) + @user = user + @reset_url = public_password_reset_url(token: token) + @expiry_hours = MatchLiveTv.password_reset_expiry_hours + + mail to: user.email, subject: "Reimposta la password — Match Live TV" + end +end diff --git a/backend/app/models/match.rb b/backend/app/models/match.rb index cc55889..b67cb52 100644 --- a/backend/app/models/match.rb +++ b/backend/app/models/match.rb @@ -2,6 +2,27 @@ class Match < ApplicationRecord belongs_to :team has_many :stream_sessions, dependent: :destroy + ACTIVE_SESSION_STATUSES = %w[live connecting reconnecting paused].freeze + validates :opponent_name, presence: true validates :sets_to_win, numericality: { greater_than: 0, less_than: 6 } + + scope :scheduled_upcoming, -> { + where.not(scheduled_at: nil).where("scheduled_at >= ?", Time.current) + } + + scope :search_teams_or_opponents, lambda { |query| + q = query.to_s.strip + return all if q.blank? + + term = "%#{sanitize_sql_like(q)}%" + joins(:team).where( + "teams.name ILIKE :term OR matches.opponent_name ILIKE :term", + term: term + ) + } + + def broadcast_active? + stream_sessions.where(status: ACTIVE_SESSION_STATUSES).exists? + end end diff --git a/backend/app/models/user.rb b/backend/app/models/user.rb index 5d6117f..3e00859 100644 --- a/backend/app/models/user.rb +++ b/backend/app/models/user.rb @@ -14,4 +14,32 @@ class User < ApplicationRecord def admin? role == "admin" end + + def generate_password_reset! + raw = SecureRandom.urlsafe_base64(32) + update!( + password_reset_digest: User.digest_token(raw), + password_reset_sent_at: Time.current + ) + raw + end + + def password_reset_expired? + password_reset_sent_at.blank? || + password_reset_sent_at < MatchLiveTv.password_reset_expiry_hours.hours.ago + end + + def clear_password_reset! + update!(password_reset_digest: nil, password_reset_sent_at: nil) + end + + def self.find_by_password_reset_token(token) + return if token.blank? + + find_by(password_reset_digest: User.digest_token(token)) + end + + def self.digest_token(token) + Digest::SHA256.hexdigest(token.to_s) + end end diff --git a/backend/app/views/layouts/marketing.html.erb b/backend/app/views/layouts/marketing.html.erb index 49bd68b..afab5b9 100644 --- a/backend/app/views/layouts/marketing.html.erb +++ b/backend/app/views/layouts/marketing.html.erb @@ -6,7 +6,7 @@ <%= content_for?(:title) ? yield(:title) : "Match Live TV" %> <%= render "shared/meta_tags" %> - + <%= render "shared/marketing_nav" %> diff --git a/backend/app/views/layouts/marketing_live.html.erb b/backend/app/views/layouts/marketing_live.html.erb index 9e8a2d5..027cca7 100644 --- a/backend/app/views/layouts/marketing_live.html.erb +++ b/backend/app/views/layouts/marketing_live.html.erb @@ -6,8 +6,8 @@ <%= content_for?(:title) ? yield(:title) : "Match Live TV" %> <%= render "shared/meta_tags" %> - - + + <%= yield :head %> diff --git a/backend/app/views/public/invitations/show.html.erb b/backend/app/views/public/invitations/show.html.erb index 0f39838..191c53f 100644 --- a/backend/app/views/public/invitations/show.html.erb +++ b/backend/app/views/public/invitations/show.html.erb @@ -1,4 +1,6 @@ -<% content_for :title, "Invito squadra" %> +<% content_for :title, "Invito squadra — Match Live TV" %> +<% content_for :meta_description, "Accetta l'invito per entrare nello staff della squadra su Match Live TV." %> +<% content_for :robots, "noindex, nofollow" %>

Accesso staff — <%= @invitation.team.name %>

diff --git a/backend/app/views/public/live/index.html.erb b/backend/app/views/public/live/index.html.erb index 8e465b6..c68dd3b 100644 --- a/backend/app/views/public/live/index.html.erb +++ b/backend/app/views/public/live/index.html.erb @@ -1,76 +1,135 @@ -<% content_for :title do %>Dirette in corso — Match Live TV<% end %> +<% content_for :title, "Dirette live partite giovanili in corso — Match Live TV" %> +<% content_for :meta_description, "Guarda le dirette live delle partite giovanili ora in corso e le prossime gare programmate. Cerca la tua squadra e apri il link senza installare app." %> +<% content_for :canonical_url, seo_absolute_url(public_live_index_path) %>
-

Dirette in corso

-

- Cerca per nome squadra o avversario, poi apri la diretta per guardare lo stream. -

- -<%= form_with url: public_live_index_path, method: :get, local: true, class: "search-form" do %> - - - <% if @query.present? %> - <%= link_to "Azzera", public_live_index_path, class: "btn btn-secondary" %> - <% end %> -<% end %> - -<% if @query.present? %> +

Dirette e partite in programma

- Risultati per «<%= h @query %>»: <%= @sessions.size %> diretta<%= @sessions.size == 1 ? "" : "e" %> + Cerca la tua squadra: trovi le dirette attive e le partite già programmate dall’app.

-<% end %> -<% if @sessions.any? %> -
- <% @sessions.each do |session| %> - <% match = session.match %> - <% on_air = @online_paths.include?(session.mediamtx_path_name) %> -
-

<%= match.team.name %> vs <%= match.opponent_name %>

-

- <% if match.location.present? %><%= match.location %> · <% end %> - <%= session.platform == "matchlivetv" ? "Match Live TV" : session.platform.capitalize %> -

- <% if session.score_state %> -

- <%= live_score_sets_label(session.score_state) %> - <% if live_score_partials_label(session.score_state).present? %> - Parziali: <%= live_score_partials_label(session.score_state) %> - <% end %> - <%= session.score_state.home_points %> - <%= session.score_state.away_points %> -

- <% end %> -
- <% if on_air %> - In onda - <% elsif session.status == "live" %> - Live - <% elsif session.status == "reconnecting" %> - Riconnessione - <% else %> - In avvio - <% end %> -
- <%= link_to "Guarda diretta →", public_live_path(session), class: "btn-watch" %> -
- <% end %> -
-<% else %> -
+ <%= form_with url: public_live_index_path, method: :get, local: true, class: "search-form" do %> + + <% if @query.present? %> -

Nessuna diretta attiva per «<%= h @query %>».

-

Prova un altro nome squadra o <%= link_to "mostra tutte le dirette", public_live_index_path %>.

- <% else %> -

Al momento non ci sono dirette attive.

-

Quando una squadra avvia lo streaming dall’app, la partita comparirà qui.

+ <%= link_to "Azzera", public_live_index_path, class: "btn btn-secondary" %> <% end %> -
-<% end %> + <% end %> + + <% if @query.present? %> +

+ Risultati per «<%= h @query %>»: + <%= @sessions.size %> in diretta · <%= @upcoming_matches.size %> in programma +

+ <% end %> + + <% if @sessions.any? %> +

In diretta adesso

+
+ <% @sessions.each do |session| %> + <% match = session.match %> + <% on_air = @online_paths.include?(session.mediamtx_path_name) %> +
+

<%= match.team.name %> vs <%= match.opponent_name %>

+

+ <% if match.location.present? %><%= match.location %> · <% end %> + <%= session.platform == "matchlivetv" ? "Match Live TV" : session.platform.capitalize %> +

+ <% if session.score_state %> +

+ <%= live_score_sets_label(session.score_state) %> + <% if live_score_partials_label(session.score_state).present? %> + Parziali: <%= live_score_partials_label(session.score_state) %> + <% end %> + <%= session.score_state.home_points %> - <%= session.score_state.away_points %> +

+ <% end %> +
+ <% if on_air %> + In onda + <% elsif session.status == "live" %> + Live + <% elsif session.status == "reconnecting" %> + Riconnessione + <% else %> + In avvio + <% end %> +
+ <%= link_to "Guarda diretta →", public_live_path(session), class: "btn-watch" %> +
+ <% end %> +
+ <% end %> + + <% if @upcoming_matches.any? %> +

Prossime partite

+

Programmate dall’app: la diretta partirà quando lo staff avvierà la trasmissione.

+
+ <% @upcoming_matches.each do |match| %> +
+

<%= match.team.name %> vs <%= match.opponent_name %>

+

+ <% if match.location.present? %><%= match.location %> · <% end %> + <%= match.category.presence || match.sport&.capitalize %> +

+

+ <%= live_scheduled_relative(match.scheduled_at) %> +

+
+ In programma +
+
+ <% end %> +
+ <% end %> + + <% if @sessions.empty? %> + <% if @upcoming_matches.any? %> +
+

Nessuna diretta in questo momento.

+

Torna all’orario indicato: quando la squadra avvierà lo streaming, la partita comparirà in «In diretta adesso».

+
+ <% else %> +
+
+

Nessuna diretta adesso — ma puoi già organizzarti

+

+ Se la tua squadra usa Match Live TV, dal telefono si programma la partita con data e ora. + Genitori e parenti trovano qui la diretta quando il fischio d’inizio è vicino. +

+
    +
  • Programma la gara dall’app (senza avviare subito la diretta)
  • +
  • La partita compare in «Prossime partite» con orario e campo
  • +
  • Lo streaming parte solo quando premi avvio in palestra
  • +
+

+ <%= link_to "Registra la tua squadra", public_signup_path, class: "btn btn-primary" %> + <%= link_to "Scopri come funziona", public_features_path, class: "btn btn-secondary" %> +

+
+ +
+ <% end %> + <% end %>
diff --git a/backend/app/views/public/live/show.html.erb b/backend/app/views/public/live/show.html.erb index 1a3db06..f689425 100644 --- a/backend/app/views/public/live/show.html.erb +++ b/backend/app/views/public/live/show.html.erb @@ -1,4 +1,6 @@ -<% content_for :title do %><%= @match.team.name %> vs <%= @match.opponent_name %> — Match Live TV<% end %> +<% content_for :title do %><%= @match.team.name %> vs <%= @match.opponent_name %> — Diretta live<% end %> +<% content_for :meta_description do %>Segui in diretta live <%= @match.team.name %> vs <%= @match.opponent_name %><% if @match.location.present? %> — <%= @match.location %><% end %>. Guarda dal browser senza installare app.<% end %> +<% content_for :robots, "noindex, follow" %> <% content_for :head do %> <% unless @stream_closed %> diff --git a/backend/app/views/public/pages/faq.html.erb b/backend/app/views/public/pages/faq.html.erb new file mode 100644 index 0000000..59a84d4 --- /dev/null +++ b/backend/app/views/public/pages/faq.html.erb @@ -0,0 +1,83 @@ +<% content_for :title, "FAQ — Diretta live partite giovanili | Match Live TV" %> +<% content_for :meta_description, "Domande frequenti su streaming partite giovanili: come guardare la diretta senza app, costi, archivio, pallavolo Under 16, inviti staff e piani Free e Premium." %> +<% content_for :canonical_url, seo_absolute_url(public_faq_path) %> + +
+

Domande frequenti sullo streaming delle partite giovanili

+

+ Risposte per genitori, allenatori e dirigenti che vogliono mandare in diretta live le partite + di pallavolo, calcio e altri sport dilettantistici. +

+ +
+
+ Come guardo la partita di mio figlio senza installare app? +

+ La società condivide un link (via WhatsApp, email o gruppo squadra). Apri il link dal telefono o dal computer: + vedi la diretta live nel browser, senza registrarti. Funziona per nonni, parenti e amici lontani. +

+
+ +
+ Chi può avviare la diretta dalla palestra? +

+ Coach, dirigenti o volontari invitati dalla società. Usano l’app Match Live TV sul telefono per filmare e, + se serve, un secondo telefono per aggiornare il punteggio. La diretta parte solo quando premo avvio in palestra. +

+
+ +
+ È adatto alla pallavolo giovanile (Under 14, Under 16)? +

+ Sì. Il punteggio a set è pensato per la pallavolo; puoi personalizzare regole per tornei particolari. + Scopri la pagina dedicata: <%= link_to "Match Live TV per pallavolo giovanile", public_pallavolo_path %>. +

+
+ +
+ Cosa succede se perdo la diretta? +

+ Con i piani Premium Light o Full la partita viene salvata in archivio (30 o 90 giorni) e puoi rivederla dal sito. + Il piano Free non include replay su server, ma la diretta resta gratuita per chi trasmette e per chi guarda. +

+
+ +
+ Quanto costa per la società? +

+ Puoi iniziare con il piano Free (limiti su staff e una diretta alla volta). + Premium Light e Full aggiungono più partite in parallelo, archivio e YouTube. + <%= link_to "Confronta i prezzi", public_prezzi_path %>. +

+
+ +
+ Posso programmare una partita prima del giorno gara? +

+ Sì, dall’app si imposta data e ora: la partita compare in «Prossime partite» sul sito. + Lo streaming parte solo quando lo staff avvia la trasmissione in palestra. +

+
+ +
+ La diretta va anche su YouTube? +

+ Con Premium Light puoi mandarla sul canale Match TV Light; con Premium Full anche sul canale YouTube della società. + Chi preferisce resta sul link Match Live TV, comodo per le famiglie. +

+
+ +
+ Devo aprire porte sul router o avere attrezzatura da TV? +

+ No per chi guarda. Per la società che trasmette basta lo smartphone e una buona connessione in palestra; + lo staff tecnico della piattaforma gestisce l’infrastruttura. Niente camere broadcast o mixer. +

+
+
+ +

+ <%= link_to "Registra la squadra", public_signup_path, class: "btn btn-primary" %> + <%= link_to "Guarda le dirette", public_live_index_path, class: "btn btn-secondary" %> +

+
diff --git a/backend/app/views/public/pages/features.html.erb b/backend/app/views/public/pages/features.html.erb index 7cd3c5a..629e394 100644 --- a/backend/app/views/public/pages/features.html.erb +++ b/backend/app/views/public/pages/features.html.erb @@ -1,24 +1,25 @@ -<% content_for :title, "Funzionalità — Match Live TV" %> -<% content_for :meta_description, "Camera, regia, punteggio live, gestione staff, archivio gare e YouTube con Premium Light e Full." %> +<% content_for :title, "Funzionalità streaming partite giovanili — Match Live TV" %> +<% content_for :meta_description, "App per diretta live da telefono: filma la partita, aggiorna il punteggio, condividi il link con genitori e nonni. Archivio gare e piani per società sportive giovanili." %> +<% content_for :canonical_url, seo_absolute_url(public_features_path) %>

Funzionalità

-

Tutto ciò che serve per trasmettere partite giovanili in modo professionale, senza complessità da broadcaster TV.

+

Tutto ciò che serve per far seguire le partite a chi non può essere in palestra: semplice per coach e volontari, comodo per genitori, nonni e parenti.

Per la società e lo staff

-

Staff con email

-

Delega trasmissione e regia a coach e volontari: ognuno ha credenziali personali.

+

Inviti con la propria email

+

Il presidente o l’owner invita chi filma e chi aggiorna il punteggio: ognuno accede con la sua email, senza condividere password.

-

Revoca accessi

-

L'owner rimuove membri dello staff e annulla richieste pendenti dalla dashboard web.

+

Gestione accessi

+

Puoi togliere l’accesso a un volontario o annullare un invito ancora in sospeso dalla dashboard della squadra.

-

Limiti per piano

-

Posti staff (trasmissione e regia) e partite concorrenti scalano con Free, Premium Light e Premium Full.

+

Piani su misura

+

Con Free, Premium Light o Premium Full aumenti quante persone possono aiutare in diretta e quante partite possono andare in parallelo.

@@ -27,32 +28,32 @@

In campo e in diretta

-

Modalità camera

-

Streaming RTMP resiliente: la diretta continua anche con reti instabili.

+

Filma dal telefono

+

Appoggi il telefono e avvii la diretta: anche con il 4G debole la trasmissione resta stabile il più possibile, senza impazzire con cavi o mixer.

-

Regia separata

-

Secondo dispositivo per aggiornare il punteggio via QR pairing.

+

Punteggio da un secondo telefono

+

Un altro volontario aggiorna set e punti da un secondo cellulare: basta inquadrare il codice che compare sullo schermo di chi filma.

-

Pagina pubblica /live

-

Genitori e tifosi guardano dal browser senza installare nulla.

+

Link per famiglie e tifosi

+

Condividi un link: genitori, nonni e amici guardano dal browser del telefono o del computer, senza installare app.

-

Archivio replay

-

Premium Light/Full: salvataggio server (30 o 90 giorni) e download su telefono.

+

Archivio partite

+

Con Premium Light o Full le gare restano salvate sul server (30 o 90 giorni) e puoi scaricarle sul telefono quando serve.

-

YouTube Match TV Light

-

Premium Light: le dirette sono visibili anche sul canale YouTube Match TV Light.

+

Anche su YouTube (Match TV Light)

+

Con Premium Light la diretta è visibile anche sul canale YouTube Match TV Light, oltre al link Match Live TV.

-

YouTube della società

-

Premium Full: relay verso il canale YouTube del club.

+

Canale YouTube della società

+

Con Premium Full puoi mandare la stessa diretta anche sul canale YouTube ufficiale del club.

-

Sito della società

-

Integrazione embed sul sito del club — in arrivo con Premium Full.

+

Sul sito della società

+

Integrazione per incorporare il player sul sito del club — in arrivo con Premium Full.

diff --git a/backend/app/views/public/pages/home.html.erb b/backend/app/views/public/pages/home.html.erb index 7e0e541..aad71cd 100644 --- a/backend/app/views/public/pages/home.html.erb +++ b/backend/app/views/public/pages/home.html.erb @@ -1,5 +1,6 @@ -<% content_for :title, "Match Live TV — Ogni partita, ogni evento, per chi non può esserci" %> -<% content_for :meta_description, "Trasmetti le partite dallo smartphone. Nonni, parenti e amici guardano dal browser, senza app. Se te la perdi, la ritrovi nell'archivio." %> +<% content_for :title, "Match Live TV — Diretta live partite giovanili da telefono" %> +<% content_for :meta_description, "Non puoi andare in palestra? Guarda la diretta live della partita di tuo figlio dal telefono o dal computer, senza installare app. Pallavolo, calcio e sport giovanili: link da condividere con nonni e parenti, archivio se te la perdi." %> +<% content_for :canonical_url, seo_absolute_url(root_path) %>
@@ -41,7 +42,7 @@
-

Come funziona

+

Come funziona lo streaming per squadre giovanili

@@ -79,10 +80,30 @@
-

Piani per ogni esigenza

+

Piani per dirette live e archivio partite

Inizia gratis. Passa a Premium quando vuoi più partite in contemporanea, archivio più lungo e diretta anche su YouTube.

<%= image_tag "/home-piani-ecosistema.png?v=1", alt: "Ecosistema Match Live TV: smartphone in diretta, laptop con live e statistiche, tablet con archivio partite e cloud", class: "plans-teaser-img", loading: "lazy" %>
<%= link_to "Confronta i piani", public_prezzi_path, class: "btn btn-primary" %>
+ +
+

Streaming partite giovanili: perché le famiglie scelgono Match Live TV

+

+ Allenatori e dirigenti cercano un modo semplice per mandare in diretta live le partite + di pallavolo giovanile, calcio, basket e altri sport: senza attrezzature da TV, + solo con lo smartphone in palestra. Match Live TV è pensato per le società dilettantistiche che vogliono + far guardare Under 14, Under 16, Under 18 e settori giovanili a chi è lontano. +

+

+ Un genitore che lavora, un nonno che non può viaggiare, un parente all’estero: aprono un link e seguono + la partita dal browser. Se arrivano in ritardo, la registrazione in archivio (con i piani Premium) + resta disponibile per giorni. Niente account complicati per chi guarda — solo per lo staff che trasmette. +

+

+ Hai dubbi su come funziona? Leggi le <%= link_to "domande frequenti", public_faq_path %>, + scopri <%= link_to "Match Live TV per la pallavolo giovanile", public_pallavolo_path %> + o <%= link_to "registra la squadra gratis", public_signup_path %>. +

+
diff --git a/backend/app/views/public/pages/pallavolo.html.erb b/backend/app/views/public/pages/pallavolo.html.erb new file mode 100644 index 0000000..b636f0e --- /dev/null +++ b/backend/app/views/public/pages/pallavolo.html.erb @@ -0,0 +1,67 @@ +<% content_for :title, "Diretta live pallavolo giovanile — Under 14, Under 16 | Match Live TV" %> +<% content_for :meta_description, "Streaming partite di pallavolo giovanile da telefono: diretta live per genitori e nonni, punteggio a set, link da condividere. Ideale per società Under 14, Under 16 e Under 18." %> +<% content_for :canonical_url, seo_absolute_url(public_pallavolo_path) %> + +
+

Match Live TV per la pallavolo giovanile

+

+ La soluzione per società, coach e genitori che vogliono una diretta live affidabile + delle partite Under 14, Under 16, Under 18 e settori giovanili — senza diventare telecronisti. +

+ +
+

Perché la pallavolo giovanile ha bisogno di uno streaming semplice

+

+ In palestra i genitori non sono sempre tutti in tribuna: turni di lavoro, fratelli piccoli, trasferte. + Un nonno in un’altra città o un parente all’estero vuole comunque vedere il tie-break del terzo set. + Match Live TV nasce per questo: streaming partite pallavolo giovanile dal telefono, + con punteggio aggiornato e link immediato da mandare su WhatsApp. +

+ +

Cosa fa il coach in due minuti

+
    +
  • Programma la partita con data, avversario e campo dall’app
  • +
  • Il giorno gara appoggia il telefono e avvia la diretta
  • +
  • Un volontario può aggiornare set e punti da un secondo cellulare
  • +
  • Condivide il link con il gruppo genitori: tutti guardano dal browser
  • +
+ +

Cosa vede la famiglia a casa

+

+ Nessuna app da installare. Apri il link e segui la partita in diretta live: nome squadre, punteggio, + stato del set. Se hai perso l’inizio, con Premium la gara resta in archivio per rivederla quando vuoi. +

+ +

Pallavolo: punteggio e regole

+

+ Set da vincere 2 o 3, punti 25/21/15 configurabili per tornei non standard. La regia è separata dalla ripresa: + in campo resta concentrato sul gioco, non sul telefono. +

+ +

Altri sport giovanili

+

+ La stessa piattaforma funziona anche per calcio, basket e sport di squadra dilettantistici. + La pallavolo resta lo sport con cui molte società iniziano perché il punteggio a set è già integrato. +

+
+ +
+
+

Genitori e nonni

+

Link su WhatsApp, visione da telefono o TV con browser. Zero registrazione per chi guarda.

+
+
+

Società e staff

+

Inviti con email, più operatori con Premium, revoca accessi dalla dashboard web.

+
+
+

Archivio gare

+

Premium Light/Full: replay per chi ha perso la diretta o vuole rivedere un punto chiave.

+
+
+ +

+ <%= link_to "Inizia gratis", public_signup_path, class: "btn btn-primary" %> + <%= link_to "Domande frequenti", public_faq_path, class: "btn btn-secondary" %> +

+
diff --git a/backend/app/views/public/pages/pricing.html.erb b/backend/app/views/public/pages/pricing.html.erb index bb20003..ede03b0 100644 --- a/backend/app/views/public/pages/pricing.html.erb +++ b/backend/app/views/public/pages/pricing.html.erb @@ -1,4 +1,6 @@ -<% content_for :title, "Prezzi — Match Live TV" %> +<% content_for :title, "Prezzi streaming partite giovanili — Match Live TV" %> +<% content_for :meta_description, "Piani Free, Premium Light e Premium Full per dirette live e archivio partite. Abbonamento annuale per società: più staff, più partite in parallelo, replay e YouTube." %> +<% content_for :canonical_url, seo_absolute_url(public_prezzi_path) %>

Piani per la tua squadra

diff --git a/backend/app/views/public/pages/privacy.html.erb b/backend/app/views/public/pages/privacy.html.erb index da3c60d..1c61be3 100644 --- a/backend/app/views/public/pages/privacy.html.erb +++ b/backend/app/views/public/pages/privacy.html.erb @@ -1,9 +1,188 @@ -<% content_for :title, "Privacy — Match Live TV" %> -
-

Informativa privacy

-
-

Match Live TV tratta i dati necessari per account, gestione staff e streaming (email, nome, dati di utilizzo delle sessioni).

-

Per richieste o cancellazione account contattare il gestore del servizio all'indirizzo indicato nella dashboard della squadra.

-

Documento provvisorio — da completare con i dati del titolare del trattamento.

-
+<% content_for :title, "Informativa privacy — Match Live TV" %> +<% content_for :meta_description, "Informativa privacy GDPR di Match Live TV: dati account, streaming partite giovanili, immagini di minori, pagamenti Stripe e diritti degli interessati." %> +<% content_for :canonical_url, seo_absolute_url(public_privacy_path) %> + + diff --git a/backend/app/views/public/pages/terms.html.erb b/backend/app/views/public/pages/terms.html.erb index 6c81b3b..90d6f77 100644 --- a/backend/app/views/public/pages/terms.html.erb +++ b/backend/app/views/public/pages/terms.html.erb @@ -1,9 +1,134 @@ <% content_for :title, "Termini di servizio — Match Live TV" %> -
-

Termini di servizio

-
-

L'uso di Match Live TV implica l'accettazione dei limiti del piano sottoscritto (staff, dirette concorrenti, archivio).

-

Le dirette sono responsabilità della squadra titolare dell'account. È vietato l'uso per contenuti non conformi alle norme sulla protezione dei minori.

-

Documento provvisorio — da rivedere con consulenza legale.

-
+<% content_for :meta_description, "Termini e condizioni d'uso di Match Live TV per società sportive: dirette live, minori, piani, pagamenti Stripe e responsabilità degli utenti." %> +<% content_for :canonical_url, seo_absolute_url(public_termini_path) %> + + diff --git a/backend/app/views/public/password_resets/edit.html.erb b/backend/app/views/public/password_resets/edit.html.erb new file mode 100644 index 0000000..4019208 --- /dev/null +++ b/backend/app/views/public/password_resets/edit.html.erb @@ -0,0 +1,17 @@ +<% content_for :title, "Nuova password — Match Live TV" %> +<% content_for :robots, "noindex, nofollow" %> + +
+

Scegli una nuova password

+
+ <%= form_with url: public_password_reset_path, method: :patch, local: true do %> + <%= hidden_field_tag :token, @token %> + <%= label_tag :password, "Nuova password (min. 8 caratteri)" %> + <%= password_field_tag :password, required: true, minlength: 8, autocomplete: "new-password" %> + <%= label_tag :password_confirmation, "Conferma password" %> + <%= password_field_tag :password_confirmation, required: true, autocomplete: "new-password" %> + <%= submit_tag "Salva password", class: "btn btn-primary" %> + <% end %> + +
+
diff --git a/backend/app/views/public/password_resets/new.html.erb b/backend/app/views/public/password_resets/new.html.erb new file mode 100644 index 0000000..3c7e42b --- /dev/null +++ b/backend/app/views/public/password_resets/new.html.erb @@ -0,0 +1,15 @@ +<% content_for :title, "Password dimenticata — Match Live TV" %> +<% content_for :robots, "noindex, nofollow" %> + +
+

Password dimenticata

+

Inserisci l’email dell’account: ti invieremo un link per reimpostare la password.

+
+ <%= form_with url: public_password_forgot_path, method: :post, local: true do %> + <%= label_tag :email, "Email" %> + <%= email_field_tag :email, params[:email], required: true, autocomplete: "email" %> + <%= submit_tag "Invia link di reset", class: "btn btn-primary" %> + <% end %> + +
+
diff --git a/backend/app/views/public/registrations/new.html.erb b/backend/app/views/public/registrations/new.html.erb index 062a178..2dcd957 100644 --- a/backend/app/views/public/registrations/new.html.erb +++ b/backend/app/views/public/registrations/new.html.erb @@ -1,4 +1,6 @@ -<% content_for :title, "Registrati — Match Live TV" %> +<% content_for :title, "Registra la squadra — Match Live TV" %> +<% content_for :meta_description, "Iscrivi la tua società sportiva giovanile su Match Live TV: dirette live da telefono, link per genitori e piani Free o Premium." %> +<% content_for :robots, "noindex, follow" %>

Registra la squadra

@@ -13,6 +15,18 @@ <%= f.password_field :password, required: true, minlength: 8 %> <%= f.label :password_confirmation, "Conferma password" %> <%= f.password_field :password_confirmation, required: true %> + + <%= f.submit "Continua", class: "btn btn-primary" %> <% end %> diff --git a/backend/app/views/public/replay/show.html.erb b/backend/app/views/public/replay/show.html.erb index 04cc59b..c3efd03 100644 --- a/backend/app/views/public/replay/show.html.erb +++ b/backend/app/views/public/replay/show.html.erb @@ -1,4 +1,6 @@ <% content_for :title, "Replay — #{@match.team.name} vs #{@match.opponent_name}" %> +<% content_for :meta_description, "Replay partita #{@match.team.name} vs #{@match.opponent_name} su Match Live TV." %> +<% content_for :robots, "noindex, nofollow" %>
<%= link_to "← Tutte le dirette", public_live_index_path, class: "back-link" %> diff --git a/backend/app/views/public/sessions/new.html.erb b/backend/app/views/public/sessions/new.html.erb index c05c7b3..668d02f 100644 --- a/backend/app/views/public/sessions/new.html.erb +++ b/backend/app/views/public/sessions/new.html.erb @@ -1,4 +1,6 @@ <% content_for :title, "Accedi — Match Live TV" %> +<% content_for :meta_description, "Accesso area riservata per staff e società registrate su Match Live TV." %> +<% content_for :robots, "noindex, nofollow" %>

Accedi

@@ -7,7 +9,8 @@ <%= label_tag :email, "Email" %> <%= email_field_tag :email, params[:email], required: true %> <%= label_tag :password, "Password" %> - <%= password_field_tag :password, required: true %> + <%= password_field_tag :password, required: true, autocomplete: "current-password" %> +

<%= link_to "Password dimenticata?", public_password_forgot_path %>

<%= submit_tag "Entra", class: "btn btn-primary" %> <% end %> diff --git a/backend/app/views/public/sitemap/show.xml.erb b/backend/app/views/public/sitemap/show.xml.erb new file mode 100644 index 0000000..84eba59 --- /dev/null +++ b/backend/app/views/public/sitemap/show.xml.erb @@ -0,0 +1,10 @@ + + +<% @entries.each do |entry| %> + + <%= entry[:loc] %> + <%= entry[:changefreq] %> + <%= entry[:priority] %> + +<% end %> + diff --git a/backend/app/views/public/teams/billing.html.erb b/backend/app/views/public/teams/billing.html.erb index 256efa2..c4b63a1 100644 --- a/backend/app/views/public/teams/billing.html.erb +++ b/backend/app/views/public/teams/billing.html.erb @@ -1,4 +1,5 @@ <% content_for :title, "Abbonamento — #{@team.name}" %> +<% content_for :robots, "noindex, nofollow" %>

Abbonamento

diff --git a/backend/app/views/public/teams/dashboard.html.erb b/backend/app/views/public/teams/dashboard.html.erb index 379bd4f..491c4a8 100644 --- a/backend/app/views/public/teams/dashboard.html.erb +++ b/backend/app/views/public/teams/dashboard.html.erb @@ -1,4 +1,6 @@ <% content_for :title, "#{@team.name} — Dashboard" %> +<% content_for :meta_description, "Dashboard squadra #{@team.name} su Match Live TV." %> +<% content_for :robots, "noindex, nofollow" %>

<%= @team.name %>

diff --git a/backend/app/views/public/teams/invite.html.erb b/backend/app/views/public/teams/invite.html.erb index 2c86303..508e9a0 100644 --- a/backend/app/views/public/teams/invite.html.erb +++ b/backend/app/views/public/teams/invite.html.erb @@ -1,4 +1,5 @@ <% content_for :title, "Aggiungi staff — #{@team.name}" %> +<% content_for :robots, "noindex, nofollow" %>

Aggiungi staff

diff --git a/backend/app/views/public/teams/new.html.erb b/backend/app/views/public/teams/new.html.erb index 6f9e65e..cea7856 100644 --- a/backend/app/views/public/teams/new.html.erb +++ b/backend/app/views/public/teams/new.html.erb @@ -1,4 +1,6 @@ <% content_for :title, "Crea squadra — Match Live TV" %> +<% content_for :meta_description, "Configura la squadra dopo la registrazione su Match Live TV." %> +<% content_for :robots, "noindex, nofollow" %>

La tua squadra

diff --git a/backend/app/views/shared/_marketing_footer.html.erb b/backend/app/views/shared/_marketing_footer.html.erb index f362b95..dddd7cf 100644 --- a/backend/app/views/shared/_marketing_footer.html.erb +++ b/backend/app/views/shared/_marketing_footer.html.erb @@ -6,6 +6,7 @@
<%= link_to "Prezzi", public_prezzi_path %> · <%= link_to "Dirette", public_live_index_path %> · + <%= link_to "FAQ", public_faq_path %> · <%= link_to "Privacy", public_privacy_path %> · <%= link_to "Termini", public_termini_path %>
diff --git a/backend/app/views/shared/_marketing_nav.html.erb b/backend/app/views/shared/_marketing_nav.html.erb index 7ecb4f6..fd4b02c 100644 --- a/backend/app/views/shared/_marketing_nav.html.erb +++ b/backend/app/views/shared/_marketing_nav.html.erb @@ -2,7 +2,10 @@
- <%= link_to root_path, class: "brand" do %>Match Live TV<% end %> + <%= link_to root_path, class: "mast-brand", aria: { label: "Match Live Tv" } do %> + + Match Live TV + <% end %>