Aggiunge i tornei con hub, pagina pubblica e tabellone per semifinali e finali.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-05 15:05:57 +02:00
co-authored by Cursor
parent d52434fb2e
commit a1f4c24a43
124 changed files with 6979 additions and 91 deletions
@@ -0,0 +1,118 @@
# frozen_string_literal: true
module Tournaments
class ComposeInviteEmail
LINK_TOKEN = "{{link_invito}}"
EXPIRY_TOKEN = "{{scadenza}}"
MAX_HTML_BYTES = 100_000
ALLOWED_TAGS = %w[p br strong b em i u ul ol li a img h2 h3 h4 span div blockquote].freeze
ALLOWED_ATTR = %w[href src alt width height style target rel data-invite-note].freeze
ALLOWED_STYLE = %w[
width height max-width min-width margin margin-top margin-bottom margin-left margin-right
padding padding-top padding-bottom padding-left padding-right
float display text-align color background background-color
font-weight font-size line-height text-decoration border-radius border
].freeze
def self.call(html:, invite_url:, expires_on: nil)
new(html: html, invite_url: invite_url, expires_on: expires_on).call
end
def self.sanitize_html(html)
new(html: html.to_s, invite_url: "").sanitize(html.to_s)
end
def self.default_html(tournament:, invited_by:)
intro = I18n.t(
"mailers.tournament_invite.body_html",
inviter: CGI.escapeHTML(invited_by.name.to_s),
tournament: CGI.escapeHTML(tournament.name.to_s),
club: CGI.escapeHTML(tournament.club.name.to_s),
assignment: I18n.t("tournaments.hub.invite_email_scope_generic")
)
cta = CGI.escapeHTML(I18n.t("mailers.tournament_invite.cta"))
<<~HTML
<p>#{CGI.escapeHTML(I18n.t("mailers.tournament_invite.hello"))}</p>
<p>#{intro}</p>
<p><a href="#{LINK_TOKEN}" style="display:inline-block;background:#e53935;color:#ffffff;text-decoration:none;padding:12px 18px;border-radius:8px;font-weight:600;">#{cta}</a></p>
<p>#{CGI.escapeHTML(I18n.t("mailers.tournament_invite.link_fallback"))}<br>#{LINK_TOKEN}</p>
<p>#{CGI.escapeHTML(I18n.t("mailers.tournament_invite.steps"))}</p>
<p>#{CGI.escapeHTML(I18n.t("mailers.tournament_invite.expiry", date: EXPIRY_TOKEN))}</p>
<p>#{CGI.escapeHTML(I18n.t("mailers.tournament_invite.ignore"))}</p>
HTML
end
def initialize(html:, invite_url:, expires_on: nil)
@html = html.to_s
@invite_url = invite_url.to_s
@expires_on = expires_on.to_s
end
def call
html = @html.dup
html.gsub!(LINK_TOKEN, @invite_url)
html.gsub!(EXPIRY_TOKEN, @expires_on) if @expires_on.present?
sanitized = sanitize(html)
sanitized += fallback_cta if @invite_url.present? && !sanitized.include?(@invite_url)
sanitized
end
def sanitize(html)
html = html.bytesize > MAX_HTML_BYTES ? html.byteslice(0, MAX_HTML_BYTES) : html
fragment = Loofah.fragment(html.to_s)
fragment.css("[data-invite-note]").each do |el|
el.remove if el.text.to_s.strip.blank?
end
scrubber = Rails::HTML::PermitScrubber.new
scrubber.tags = ALLOWED_TAGS
scrubber.attributes = ALLOWED_ATTR
fragment.scrub!(scrubber)
fragment.css("a").each { |node| scrub_url!(node, "href") }
fragment.css("img").each do |node|
scrub_url!(node, "src")
end
fragment.css("[style]").each { |node| scrub_style!(node) }
fragment.css("a[target='_blank']").each do |node|
node["rel"] = "noopener noreferrer"
end
fragment.to_s
end
private
def scrub_url!(node, attr)
url = node[attr].to_s.strip
return if url == LINK_TOKEN || url.include?(LINK_TOKEN)
return if url.match?(/\Ahttps?:\/\//i) || url.start_with?("/rails/active_storage")
if attr == "src"
node.remove
else
node.remove_attribute(attr)
end
end
def scrub_style!(node)
decls = node["style"].to_s.split(";").map(&:strip).reject(&:blank?)
kept = decls.select do |decl|
prop, value = decl.split(":", 2).map { |part| part.to_s.strip }
next false if prop.blank? || value.blank?
next false unless ALLOWED_STYLE.include?(prop.downcase)
next false if value.match?(/expression|javascript|url\s*\(/i)
true
end
if kept.any?
node["style"] = kept.join("; ")
else
node.remove_attribute("style")
end
end
def fallback_cta
label = CGI.escapeHTML(I18n.t("mailers.tournament_invite.cta"))
url = CGI.escapeHTML(@invite_url)
%(<p><a href="#{url}" style="display:inline-block;background:#e53935;color:#ffffff;text-decoration:none;padding:12px 18px;border-radius:8px;font-weight:600;">#{label}</a></p><p>#{url}</p>)
end
end
end