feat: fale conosco
All checks were successful
Deploy QR Rapido / test (push) Successful in 45s
Deploy QR Rapido / build-and-push (push) Successful in 14m58s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m12s

This commit is contained in:
Ricardo Carneiro 2025-10-21 22:45:52 -03:00
parent 916838820a
commit 251cbe56a4
13 changed files with 1492 additions and 1422 deletions

View File

@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace QRRapidoApp.Controllers
{
[Authorize]
public class SupportController : Controller
{
[HttpGet]
public IActionResult PremiumContact()
{
return View();
}
}
}

View File

@ -1184,6 +1184,63 @@
<data name="PremiumSupport" xml:space="preserve"> <data name="PremiumSupport" xml:space="preserve">
<value>Soporte Premium (usuarios pagos)</value> <value>Soporte Premium (usuarios pagos)</value>
</data> </data>
<data name="PremiumSupportFabButton" xml:space="preserve">
<value>¡Quiero hablar con ustedes!</value>
</data>
<data name="PremiumSupportMenuIntro" xml:space="preserve">
<value>Elegí cómo hablar con nuestro equipo:</value>
</data>
<data name="PremiumSupportOptionTelegram" xml:space="preserve">
<value>Hablar por Telegram</value>
</data>
<data name="PremiumSupportOptionForm" xml:space="preserve">
<value>Enviar formulario</value>
</data>
<data name="PremiumSupportFormPageTitle" xml:space="preserve">
<value>Contacto con Soporte Premium</value>
</data>
<data name="PremiumSupportFormHeading" xml:space="preserve">
<value>Hablá con nuestro equipo premium</value>
</data>
<data name="PremiumSupportFormDescription" xml:space="preserve">
<value>Completá tus datos y te responderemos lo antes posible.</value>
</data>
<data name="PremiumSupportFormNameLabel" xml:space="preserve">
<value>Tu nombre</value>
</data>
<data name="PremiumSupportFormNamePlaceholder" xml:space="preserve">
<value>¿Cómo debemos llamarte?</value>
</data>
<data name="PremiumSupportFormEmailLabel" xml:space="preserve">
<value>Tu correo</value>
</data>
<data name="PremiumSupportFormEmailPlaceholder" xml:space="preserve">
<value>vos@ejemplo.com</value>
</data>
<data name="PremiumSupportFormMessageLabel" xml:space="preserve">
<value>Tu mensaje</value>
</data>
<data name="PremiumSupportFormMessagePlaceholder" xml:space="preserve">
<value>Contanos cómo podemos ayudarte…</value>
</data>
<data name="PremiumSupportFormPreferredChannelLabel" xml:space="preserve">
<value>Canal de contacto preferido</value>
</data>
<data name="PremiumSupportFormChannelTelegram" xml:space="preserve">
<value>Telegram</value>
</data>
<data name="PremiumSupportFormChannelEmail" xml:space="preserve">
<value>Correo</value>
</data>
<data name="PremiumSupportFormChannelPhone" xml:space="preserve">
<value>Teléfono / WhatsApp</value>
</data>
<data name="PremiumSupportFormSubmitLabel" xml:space="preserve">
<value>Enviar mensaje</value>
</data>
<data name="PremiumSupportFormPrivacyDisclaimer" xml:space="preserve">
<value>Usamos tus datos solo para responder la consulta. Los mensajes se procesan vía Formspree.</value>
</data>
<data name="ProfessionalServices" xml:space="preserve"> <data name="ProfessionalServices" xml:space="preserve">
<value>Servicios Profesionales</value> <value>Servicios Profesionales</value>
</data> </data>

View File

@ -1,110 +1,51 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version='1.0' encoding='utf-8'?>
<root> <root xmlns:ns1="urn:schemas-microsoft-com:xml-msdata" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- <xs:schema id="root">
Microsoft ResX Schema <xs:import namespace="http://www.w3.org/XML/1998/namespace" />
<xs:element name="root" ns1:IsDataSet="true">
Version 2.0 <xs:complexType>
<xs:choice maxOccurs="unbounded">
The primary goals of this format is to allow a simple XML format <xs:element name="metadata">
that is mostly human readable. The generation and parsing of the <xs:complexType>
various data types are done through the TypeConverter classes <xs:sequence>
associated with the data types. <xs:element name="value" type="xsd:string" minOccurs="0" />
</xs:sequence>
Example: <xs:attribute name="name" use="required" type="xsd:string" />
<xs:attribute name="type" type="xsd:string" />
... ado.net/XML headers & schema ... <xs:attribute name="mimetype" type="xsd:string" />
<resheader name="resmimetype">text/microsoft-resx</resheader> <xs:attribute ref="xml:space" />
<resheader name="version">2.0</resheader> </xs:complexType>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> </xs:element>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> <xs:element name="assembly">
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> <xs:complexType>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> <xs:attribute name="alias" type="xsd:string" />
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> <xs:attribute name="name" type="xsd:string" />
<value>[base64 mime encoded serialized .NET Framework object]</value> </xs:complexType>
</data> </xs:element>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> <xs:element name="data">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <xs:complexType>
<comment>This is a comment</comment> <xs:sequence>
</data> <xs:element name="value" type="xsd:string" minOccurs="0" ns1:Ordinal="1" />
<xs:element name="comment" type="xsd:string" minOccurs="0" ns1:Ordinal="2" />
There are any number of "resheader" rows that contain simple </xs:sequence>
name/value pairs. <xs:attribute name="name" type="xsd:string" use="required" ns1:Ordinal="1" />
<xs:attribute name="type" type="xsd:string" ns1:Ordinal="3" />
Each data row contains a name, and value. The row also contains a <xs:attribute name="mimetype" type="xsd:string" ns1:Ordinal="4" />
type or mimetype. Type corresponds to a .NET class that support <xs:attribute ref="xml:space" />
text/value conversion through the TypeConverter architecture. </xs:complexType>
Classes that don't support this are serialized and stored with the </xs:element>
mimetype set. <xs:element name="resheader">
<xs:complexType>
The mimetype is used for serialized objects, and tells the <xs:sequence>
ResXResourceReader how to depersist the object. This is currently not <xs:element name="value" type="xsd:string" minOccurs="0" ns1:Ordinal="1" />
extensible. For a given mimetype the value must be set accordingly: </xs:sequence>
<xs:attribute name="name" type="xsd:string" use="required" />
Note - application/x-microsoft.net.object.binary.base64 is the format </xs:complexType>
that the ResXResourceWriter will generate, however the reader can </xs:element>
read any of the formats listed below. </xs:choice>
</xs:complexType>
mimetype: application/x-microsoft.net.object.binary.base64 </xs:element>
value : The object must be serialized with </xs:schema>
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype"> <resheader name="resmimetype">
<value>text/microsoft-resx</value> <value>text/microsoft-resx</value>
</resheader> </resheader>
@ -973,4 +914,61 @@
<data name="BackToHome" xml:space="preserve"> <data name="BackToHome" xml:space="preserve">
<value>Volver al Inicio</value> <value>Volver al Inicio</value>
</data> </data>
<data name="PremiumSupportFabButton" xml:space="preserve">
<value>¡Quiero hablar con ustedes!</value>
</data>
<data name="PremiumSupportMenuIntro" xml:space="preserve">
<value>Elegí cómo hablar con nuestro equipo:</value>
</data>
<data name="PremiumSupportOptionTelegram" xml:space="preserve">
<value>Hablar por Telegram</value>
</data>
<data name="PremiumSupportOptionForm" xml:space="preserve">
<value>Enviar formulario</value>
</data>
<data name="PremiumSupportFormPageTitle" xml:space="preserve">
<value>Contacto con Soporte Premium</value>
</data>
<data name="PremiumSupportFormHeading" xml:space="preserve">
<value>Habla con nuestro equipo premium</value>
</data>
<data name="PremiumSupportFormDescription" xml:space="preserve">
<value>Completa tus datos y te responderemos lo antes posible.</value>
</data>
<data name="PremiumSupportFormNameLabel" xml:space="preserve">
<value>Tu nombre</value>
</data>
<data name="PremiumSupportFormNamePlaceholder" xml:space="preserve">
<value>¿Cómo debemos llamarte?</value>
</data>
<data name="PremiumSupportFormEmailLabel" xml:space="preserve">
<value>Tu correo</value>
</data>
<data name="PremiumSupportFormEmailPlaceholder" xml:space="preserve">
<value>tu@ejemplo.com</value>
</data>
<data name="PremiumSupportFormMessageLabel" xml:space="preserve">
<value>Tu mensaje</value>
</data>
<data name="PremiumSupportFormMessagePlaceholder" xml:space="preserve">
<value>Cuéntanos cómo podemos ayudarte…</value>
</data>
<data name="PremiumSupportFormPreferredChannelLabel" xml:space="preserve">
<value>Canal de contacto preferido</value>
</data>
<data name="PremiumSupportFormChannelTelegram" xml:space="preserve">
<value>Telegram</value>
</data>
<data name="PremiumSupportFormChannelEmail" xml:space="preserve">
<value>Correo</value>
</data>
<data name="PremiumSupportFormChannelPhone" xml:space="preserve">
<value>Teléfono / WhatsApp</value>
</data>
<data name="PremiumSupportFormSubmitLabel" xml:space="preserve">
<value>Enviar mensaje</value>
</data>
<data name="PremiumSupportFormPrivacyDisclaimer" xml:space="preserve">
<value>Usamos esta información solo para responder tu solicitud. Los mensajes se procesan mediante Formspree.</value>
</data>
</root> </root>

View File

@ -1274,6 +1274,63 @@
<data name="PremiumSupport" xml:space="preserve"> <data name="PremiumSupport" xml:space="preserve">
<value>Suporte Premium (usuários pagos)</value> <value>Suporte Premium (usuários pagos)</value>
</data> </data>
<data name="PremiumSupportFabButton" xml:space="preserve">
<value>Quero falar com vocês!</value>
</data>
<data name="PremiumSupportMenuIntro" xml:space="preserve">
<value>Escolha como prefere falar com nossa equipe:</value>
</data>
<data name="PremiumSupportOptionTelegram" xml:space="preserve">
<value>Falar no Telegram</value>
</data>
<data name="PremiumSupportOptionForm" xml:space="preserve">
<value>Enviar formulário</value>
</data>
<data name="PremiumSupportFormPageTitle" xml:space="preserve">
<value>Contato com Suporte Premium</value>
</data>
<data name="PremiumSupportFormHeading" xml:space="preserve">
<value>Fale com nossa equipe premium</value>
</data>
<data name="PremiumSupportFormDescription" xml:space="preserve">
<value>Preencha seus dados e entraremos em contato o quanto antes.</value>
</data>
<data name="PremiumSupportFormNameLabel" xml:space="preserve">
<value>Seu nome</value>
</data>
<data name="PremiumSupportFormNamePlaceholder" xml:space="preserve">
<value>Como devemos te chamar?</value>
</data>
<data name="PremiumSupportFormEmailLabel" xml:space="preserve">
<value>Seu email</value>
</data>
<data name="PremiumSupportFormEmailPlaceholder" xml:space="preserve">
<value>voce@exemplo.com</value>
</data>
<data name="PremiumSupportFormMessageLabel" xml:space="preserve">
<value>Sua mensagem</value>
</data>
<data name="PremiumSupportFormMessagePlaceholder" xml:space="preserve">
<value>Conte como podemos ajudar…</value>
</data>
<data name="PremiumSupportFormPreferredChannelLabel" xml:space="preserve">
<value>Canal de contato preferido</value>
</data>
<data name="PremiumSupportFormChannelTelegram" xml:space="preserve">
<value>Telegram</value>
</data>
<data name="PremiumSupportFormChannelEmail" xml:space="preserve">
<value>Email</value>
</data>
<data name="PremiumSupportFormChannelPhone" xml:space="preserve">
<value>Telefone / WhatsApp</value>
</data>
<data name="PremiumSupportFormSubmitLabel" xml:space="preserve">
<value>Enviar mensagem</value>
</data>
<data name="PremiumSupportFormPrivacyDisclaimer" xml:space="preserve">
<value>Usaremos suas informações apenas para responder à solicitação. As mensagens são processadas via Formspree.</value>
</data>
<data name="ProfessionalServices" xml:space="preserve"> <data name="ProfessionalServices" xml:space="preserve">
<value>Serviços Profissionais</value> <value>Serviços Profissionais</value>
</data> </data>

View File

@ -426,4 +426,61 @@
<data name="Deleting" xml:space="preserve"> <data name="Deleting" xml:space="preserve">
<value>Deleting</value> <value>Deleting</value>
</data> </data>
<data name="PremiumSupportFabButton" xml:space="preserve">
<value>Talk with our team</value>
</data>
<data name="PremiumSupportMenuIntro" xml:space="preserve">
<value>Choose how you&apos;d like to reach us:</value>
</data>
<data name="PremiumSupportOptionTelegram" xml:space="preserve">
<value>Chat on Telegram</value>
</data>
<data name="PremiumSupportOptionForm" xml:space="preserve">
<value>Send contact form</value>
</data>
<data name="PremiumSupportFormPageTitle" xml:space="preserve">
<value>Premium Support Contact</value>
</data>
<data name="PremiumSupportFormHeading" xml:space="preserve">
<value>Talk to our premium support team</value>
</data>
<data name="PremiumSupportFormDescription" xml:space="preserve">
<value>Fill out the form below and we will reach you back as soon as possible.</value>
</data>
<data name="PremiumSupportFormNameLabel" xml:space="preserve">
<value>Your name</value>
</data>
<data name="PremiumSupportFormNamePlaceholder" xml:space="preserve">
<value>How should we call you?</value>
</data>
<data name="PremiumSupportFormEmailLabel" xml:space="preserve">
<value>Your email</value>
</data>
<data name="PremiumSupportFormEmailPlaceholder" xml:space="preserve">
<value>you@example.com</value>
</data>
<data name="PremiumSupportFormMessageLabel" xml:space="preserve">
<value>Your message</value>
</data>
<data name="PremiumSupportFormMessagePlaceholder" xml:space="preserve">
<value>Tell us how we can help…</value>
</data>
<data name="PremiumSupportFormPreferredChannelLabel" xml:space="preserve">
<value>Preferred contact channel</value>
</data>
<data name="PremiumSupportFormChannelTelegram" xml:space="preserve">
<value>Telegram</value>
</data>
<data name="PremiumSupportFormChannelEmail" xml:space="preserve">
<value>Email</value>
</data>
<data name="PremiumSupportFormChannelPhone" xml:space="preserve">
<value>Phone / WhatsApp</value>
</data>
<data name="PremiumSupportFormSubmitLabel" xml:space="preserve">
<value>Send message</value>
</data>
<data name="PremiumSupportFormPrivacyDisclaimer" xml:space="preserve">
<value>We use this information only to respond to your request. Messages are processed via Formspree.</value>
</data>
</root> </root>

View File

@ -75,7 +75,7 @@
<link rel="preload" href="~/css/qrrapido-theme.css" as="style"> <link rel="preload" href="~/css/qrrapido-theme.css" as="style">
<!-- Hotjar Tracking Code for https://qrrapido.site --> <!-- Hotjar Tracking Code for https://qrrapido.site -->
<script> <script data-cfasync="false">
(function(h,o,t,j,a,r){ (function(h,o,t,j,a,r){
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)}; h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
h._hjSettings={hjid:6550944,hjsv:6}; h._hjSettings={hjid:6550944,hjsv:6};
@ -174,6 +174,7 @@
<!-- Bootstrap 5 - Optimized loading --> <!-- Bootstrap 5 - Optimized loading -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" media="print" onload="this.media='all'"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" media="print" onload="this.media='all'">
<link rel="stylesheet" href="~/css/vendor/fontawesome.min.css" asp-append-version="true" media="print" onload="this.media='all'" /> <link rel="stylesheet" href="~/css/vendor/fontawesome.min.css" asp-append-version="true" media="print" onload="this.media='all'" />
<link rel="stylesheet" href="~/css/telegram-fab.css" asp-append-version="true" />
<!-- Custom CSS - Critical above fold with cache busting --> <!-- Custom CSS - Critical above fold with cache busting -->
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
@ -407,8 +408,14 @@
<!-- Cookie Consent Banner --> <!-- Cookie Consent Banner -->
@await Html.PartialAsync("_CookieConsent") @await Html.PartialAsync("_CookieConsent")
@if (isPremiumUser)
{
@await Html.PartialAsync("_TelegramPremiumFab")
}
<!-- Bootstrap 5 JS --> <!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/telegram-fab.js" asp-append-version="true" defer></script>
@if (isDevelopment) @if (isDevelopment)
{ {

View File

@ -1,94 +1,51 @@
@using System.Globalization
@using Microsoft.Extensions.Localization @using Microsoft.Extensions.Localization
@inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer @inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration @inject Microsoft.Extensions.Configuration.IConfiguration Configuration
@{ @{
var culture = CultureInfo.CurrentUICulture; var telegramUrl = Configuration["Support:TelegramUrl"] ?? "https://t.me/jobmakerbr";
var telegramLang = culture.Name.Replace('-', '_'); var formConfigured = !string.IsNullOrWhiteSpace(Configuration["Support:FormspreeUrl"]);
var botUsername = Configuration["Telegram:LoginWidgetBotUsername"] ?? string.Empty; var formLink = Url.Action("PremiumContact", "Support");
var botId = Configuration["Telegram:BotId"] ?? string.Empty; var formEnabled = formConfigured && !string.IsNullOrEmpty(formLink);
var requestAccess = Configuration["Telegram:LoginWidgetRequestAccess"] ?? "write";
} }
<div id="telegramFabRoot" <div class="support-fab-root" data-support-fab>
class="telegram-fab-root" <div id="supportFabMenu"
data-telegram-lang="@telegramLang" class="support-fab-menu"
data-bot-username="@botUsername" data-support-fab-menu
data-bot-id="@botId" role="menu"
data-request-access="@requestAccess" aria-label="@Localizer["PremiumSupportMenuIntro"]"
data-status-loading="@Localizer["TelegramPremiumStatusLoading"]" hidden>
data-status-error="@Localizer["TelegramPremiumStatusError"]" <p class="support-fab-text">@Localizer["PremiumSupportMenuIntro"]</p>
data-status-connected="@Localizer["TelegramPremiumStatusConnected"]" <div class="support-fab-actions">
data-status-disconnected="@Localizer["TelegramPremiumStatusDisconnected"]" <a class="support-fab-link support-telegram"
data-link-success="@Localizer["TelegramPremiumLinkSuccess"]" href="@telegramUrl"
data-unlink-success="@Localizer["TelegramPremiumUnlinkSuccess"]" target="_blank"
data-link-error="@Localizer["TelegramPremiumLinkError"]" rel="noopener"
data-unlink-error="@Localizer["TelegramPremiumUnlinkError"]" role="menuitem">
data-retry-label="@Localizer["TelegramPremiumRetry"]" <i class="fab fa-telegram-plane icon" aria-hidden="true"></i>
data-open-label="@Localizer["TelegramPremiumOpenTelegram"]" <span>@Localizer["PremiumSupportOptionTelegram"]</span>
data-unlink-label="@Localizer["TelegramPremiumUnlink"]"> </a>
<button id="telegramPremiumFab" @if (formEnabled)
type="button" {
class="btn btn-primary fab-telegram d-flex align-items-center justify-content-center" <a class="support-fab-link support-form"
data-bs-toggle="offcanvas" href="@formLink"
data-bs-target="#telegramPremiumOffcanvas" target="_blank"
aria-controls="telegramPremiumOffcanvas" rel="noopener"
aria-label="@Localizer["TelegramPremiumHelp"]" role="menuitem">
title="@Localizer["TelegramPremiumHelp"]"> <i class="fas fa-envelope-open-text icon" aria-hidden="true"></i>
<i class="fab fa-telegram-plane" aria-hidden="true"></i> <span>@Localizer["PremiumSupportOptionForm"]</span>
<span class="visually-hidden">@Localizer["TelegramPremiumHelp"]</span> </a>
</button> }
<div class="offcanvas offcanvas-end offcanvas-sm-bottom"
tabindex="-1"
id="telegramPremiumOffcanvas"
aria-labelledby="telegramPremiumOffcanvasLabel">
<div class="offcanvas-header border-bottom">
<h5 class="offcanvas-title" id="telegramPremiumOffcanvasLabel">@Localizer["TelegramPremiumTitle"]</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="@Localizer["Close"]"></button>
</div> </div>
<div class="offcanvas-body">
<div id="telegramFabAlert" class="alert alert-danger d-none" role="alert"></div>
<div id="telegramFabLoading" class="d-flex align-items-center gap-2 telegram-fab-loading" role="status">
<div class="spinner-border spinner-border-sm text-primary" aria-hidden="true"></div>
<span>@Localizer["TelegramPremiumStatusLoading"]</span>
</div> </div>
<div id="telegramFabConnected" class="telegram-fab-connected d-none"> <button type="button"
<p class="mb-3 text-body">@Html.Raw(Localizer["TelegramPremiumStatusConnected", "<span class=\"fw-semibold telegram-fab-username\"></span>"])</p> class="btn btn-primary fab-trigger"
<div class="d-grid gap-2"> data-support-fab-toggle
<button type="button" class="btn btn-success telegram-fab-open"> aria-controls="supportFabMenu"
<i class="fab fa-telegram-plane me-2" aria-hidden="true"></i> aria-expanded="false">
@Localizer["TelegramPremiumOpenTelegram"] <i class="fas fa-headset" aria-hidden="true"></i>
</button> <span class="fab-trigger-text">@Localizer["PremiumSupportFabButton"]</span>
<button type="button" class="btn btn-outline-secondary telegram-fab-unlink">
@Localizer["TelegramPremiumUnlink"]
</button> </button>
</div> </div>
</div>
<div id="telegramFabDisconnected" class="telegram-fab-disconnected d-none">
<p class="mb-3 text-body-secondary">@Localizer["TelegramPremiumLoginHint"]</p>
<div id="telegramLoginWidgetPlaceholder" class="telegram-fab-widget-placeholder">
<div class="telegram-fab-widget-skeleton">
<div class="placeholder-wave">
<span class="placeholder col-8"></span>
<span class="placeholder col-6 mt-2"></span>
</div>
</div>
</div>
<div class="d-grid gap-2 mt-3">
<button type="button" class="btn btn-outline-primary telegram-fab-retry d-none">
@Localizer["TelegramPremiumRetry"]
</button>
</div>
</div>
</div>
</div>
<div id="telegramFabToasts" class="toast-container position-fixed bottom-0 end-0 p-3">
<!-- Toasts injected dynamically -->
</div>
</div>

View File

@ -0,0 +1,82 @@
@using System.Globalization
@using Microsoft.Extensions.Localization
@inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
@{
ViewData["Title"] = Localizer["PremiumSupportFormPageTitle"];
var formAction = Configuration["Support:FormspreeUrl"] ?? "https://formspree.io/f/xpwynqpj";
var culture = CultureInfo.CurrentUICulture.Name;
Layout = "~/Views/Shared/_Layout.cshtml";
}
<section class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="card shadow-sm border-0">
<div class="card-body p-4 p-md-5">
<h1 class="h4 mb-3">@Localizer["PremiumSupportFormHeading"]</h1>
<p class="text-muted mb-4">@Localizer["PremiumSupportFormDescription"]</p>
<form action="@formAction" method="POST" class="support-form">
<input type="hidden" name="_language" value="@culture" />
<input type="hidden" name="_subject" value="QR Rápido - Premium Support" />
<input type="hidden" name="_template" value="table" />
<div class="mb-3">
<label for="supportName" class="form-label">@Localizer["PremiumSupportFormNameLabel"]</label>
<input type="text"
id="supportName"
name="name"
class="form-control"
required
autocomplete="name"
placeholder="@Localizer["PremiumSupportFormNamePlaceholder"]" />
</div>
<div class="mb-3">
<label for="supportEmail" class="form-label">@Localizer["PremiumSupportFormEmailLabel"]</label>
<input type="email"
id="supportEmail"
name="email"
class="form-control"
required
autocomplete="email"
placeholder="@Localizer["PremiumSupportFormEmailPlaceholder"]" />
</div>
<div class="mb-3">
<label for="supportMessage" class="form-label">@Localizer["PremiumSupportFormMessageLabel"]</label>
<textarea id="supportMessage"
name="message"
class="form-control"
rows="5"
required
placeholder="@Localizer["PremiumSupportFormMessagePlaceholder"]"></textarea>
</div>
<div class="mb-4">
<label for="supportPreferredChannel" class="form-label">@Localizer["PremiumSupportFormPreferredChannelLabel"]</label>
<select id="supportPreferredChannel"
name="preferred_channel"
class="form-select">
<option value="telegram">@Localizer["PremiumSupportFormChannelTelegram"]</option>
<option value="email">@Localizer["PremiumSupportFormChannelEmail"]</option>
<option value="phone">@Localizer["PremiumSupportFormChannelPhone"]</option>
</select>
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-paper-plane me-2"></i>@Localizer["PremiumSupportFormSubmitLabel"]
</button>
</form>
<p class="mt-3 text-center text-muted small">
@Localizer["PremiumSupportFormPrivacyDisclaimer"]
</p>
</div>
</div>
</div>
</div>
</section>

View File

@ -7,6 +7,10 @@
"TaglineEN": "Generate QR codes in seconds!", "TaglineEN": "Generate QR codes in seconds!",
"Version": "1.0.0" "Version": "1.0.0"
}, },
"Support": {
"TelegramUrl": "https://t.me/jobmakerbr",
"FormspreeUrl": "https://formspree.io/f/xpwynqpj"
},
"ApplicationName": "QRRapido-Dev", "ApplicationName": "QRRapido-Dev",
"Environment": "Dev", "Environment": "Dev",
"Serilog": { "Serilog": {

View File

@ -1,6 +1,10 @@
{ {
"ApplicationName": "QRRapido-Prod", "ApplicationName": "QRRapido-Prod",
"Environment": "Prod", "Environment": "Prod",
"Support": {
"TelegramUrl": "https://t.me/jobmakerbr",
"FormspreeUrl": "https://formspree.io/f/xpwynqpj"
},
"ConnectionStrings": { "ConnectionStrings": {
"MongoDB": "mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/QrRapido?replicaSet=rs0&authSource=admin" "MongoDB": "mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/QrRapido?replicaSet=rs0&authSource=admin"
}, },
@ -38,7 +42,11 @@
"DatabaseSizeErrorMB": 5120, "DatabaseSizeErrorMB": 5120,
"GrowthRateWarningMBPerHour": 100, "GrowthRateWarningMBPerHour": 100,
"IncludeCollectionStats": true, "IncludeCollectionStats": true,
"CollectionsToMonitor": [ "Users", "QRCodeHistory", "AdFreeSessions" ] "CollectionsToMonitor": [
"Users",
"QRCodeHistory",
"AdFreeSessions"
]
}, },
"HealthChecks": { "HealthChecks": {
"MongoDB": { "MongoDB": {

View File

@ -10,6 +10,10 @@
"ConnectionStrings": { "ConnectionStrings": {
"MongoDB": "mongodb://localhost:27017/QrRapido" "MongoDB": "mongodb://localhost:27017/QrRapido"
}, },
"Support": {
"TelegramUrl": "https://t.me/jobmakerbr",
"FormspreeUrl": "https://formspree.io/f/xpwynqpj"
},
"Authentication": { "Authentication": {
"Google": { "Google": {
"ClientId": "1080447252222-dqjsu999tvrpb69oj5iapckdh9g8rvha.apps.googleusercontent.com", "ClientId": "1080447252222-dqjsu999tvrpb69oj5iapckdh9g8rvha.apps.googleusercontent.com",
@ -105,7 +109,7 @@
}, },
"Premium": { "Premium": {
"FreeQRLimit": 10, "FreeQRLimit": 10,
"PremiumPrice": 12.90, "PremiumPrice": 12.9,
"Features": { "Features": {
"UnlimitedQR": true, "UnlimitedQR": true,
"DynamicQR": true, "DynamicQR": true,
@ -149,7 +153,11 @@
"DatabaseSizeErrorMB": 5120, "DatabaseSizeErrorMB": 5120,
"GrowthRateWarningMBPerHour": 100, "GrowthRateWarningMBPerHour": 100,
"IncludeCollectionStats": true, "IncludeCollectionStats": true,
"CollectionsToMonitor": [ "Users", "QRCodeHistory", "AdFreeSessions" ] "CollectionsToMonitor": [
"Users",
"QRCodeHistory",
"AdFreeSessions"
]
}, },
"HealthChecks": { "HealthChecks": {
"MongoDB": { "MongoDB": {

View File

@ -1,80 +1,147 @@
.fab-telegram { .support-fab-root {
position: fixed; position: fixed;
bottom: var(--telegram-fab-offset, 1.5rem); bottom: var(--support-fab-offset, 1.5rem);
right: var(--telegram-fab-offset, 1.5rem); right: var(--support-fab-offset, 1.5rem);
width: 3rem; display: flex;
height: 3rem; flex-direction: column;
border-radius: 50%; align-items: flex-end;
gap: 0.75rem;
z-index: 1080;
}
.support-fab-root .fab-trigger {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
min-width: 3rem;
padding: 0.75rem 1rem;
border-radius: 999px;
box-shadow: 0 0.75rem 1.5rem rgba(0, 0, 0, 0.18); box-shadow: 0 0.75rem 1.5rem rgba(0, 0, 0, 0.18);
z-index: 1052;
opacity: 0.95; opacity: 0.95;
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
transition: transform 0.2s ease, opacity 0.2s ease; transition: transform 0.2s ease, opacity 0.2s ease;
} }
.fab-telegram:hover, .support-fab-root .fab-trigger:hover,
.fab-telegram:focus-visible { .support-fab-root .fab-trigger:focus-visible {
opacity: 1; opacity: 1;
transform: translateY(-2px); transform: translateY(-2px);
} }
.fab-telegram:focus-visible { .support-fab-root .fab-trigger:focus-visible {
outline: 2px solid rgba(0, 123, 255, 0.6); outline: 2px solid rgba(0, 123, 255, 0.6);
outline-offset: 2px; outline-offset: 2px;
} }
@media (max-width: 575.98px) { .support-fab-root .fab-trigger .fab-trigger-text {
.fab-telegram { font-weight: 600;
width: 2.75rem;
height: 2.75rem;
bottom: 1rem;
right: 1rem;
}
} }
.telegram-fab-root .toast-container { .support-fab-menu {
z-index: 1080; position: relative;
display: none;
width: 240px;
padding: 1rem;
border-radius: 1rem;
background: rgba(17, 24, 39, 0.92);
color: #fff;
box-shadow: 0 1.5rem 3rem rgba(15, 23, 42, 0.45);
backdrop-filter: blur(8px);
} }
.telegram-fab-loading .spinner-border { .support-fab-root.is-open .support-fab-menu {
width: 1.5rem;
height: 1.5rem;
}
.telegram-fab-widget-placeholder {
min-height: 90px;
display: flex; display: flex;
flex-direction: column;
gap: 0.75rem;
}
.support-fab-menu::before {
content: "";
position: absolute;
bottom: -8px;
right: 32px;
width: 16px;
height: 16px;
background: inherit;
transform: rotate(45deg);
z-index: 0;
border-radius: 2px;
box-shadow: 0 1.5rem 3rem rgba(15, 23, 42, 0.4);
}
.support-fab-text {
margin: 0;
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.82);
}
.support-fab-actions {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.support-fab-link {
display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 0.5rem;
border-radius: 999px;
padding: 0.625rem 0.75rem;
transition: transform 0.2s ease, background-color 0.2s ease;
} }
.telegram-fab-widget-skeleton .placeholder { .support-fab-link .icon {
display: inline-block; font-size: 1rem;
height: 0.875rem;
} }
.telegram-fab-widget-skeleton .placeholder.col-8 { .support-fab-link.support-telegram {
width: 70%; background: rgba(59, 130, 246, 0.15);
color: #60a5fa;
} }
.telegram-fab-widget-skeleton .placeholder.col-6 { .support-fab-link.support-telegram:hover,
width: 55%; .support-fab-link.support-telegram:focus-visible {
background: rgba(59, 130, 246, 0.25);
color: #fff;
} }
.offcanvas-sm-bottom { .support-fab-link.support-form {
--bs-offcanvas-width: min(420px, 90vw); background: rgba(249, 115, 22, 0.15);
color: #f97316;
} }
@media (max-width: 767.98px) { .support-fab-link.support-form:hover,
.offcanvas-sm-bottom { .support-fab-link.support-form:focus-visible {
--bs-offcanvas-width: 100%; background: rgba(249, 115, 22, 0.25);
--bs-offcanvas-height: min(80vh, 420px); color: #fff;
--bs-offcanvas-transform: translateY(100%); }
border-radius: 1rem 1rem 0 0;
width: 100%; .support-fab-link:hover,
top: auto; .support-fab-link:focus-visible {
bottom: 0; transform: translateY(-1px);
left: 0; text-decoration: none;
right: 0; }
.support-fab-link:focus-visible {
outline: 2px solid rgba(255, 255, 255, 0.45);
outline-offset: 2px;
}
@media (max-width: 575.98px) {
.support-fab-root {
bottom: 1rem;
right: 1rem;
gap: 0.5rem;
}
.support-fab-root .fab-trigger {
padding: 0.65rem 0.85rem;
font-size: 0.875rem;
}
.support-fab-menu {
width: min(260px, 80vw);
} }
} }

View File

@ -1,327 +1,80 @@
(function () { (function () {
function initTelegramFab() { function initSupportFab() {
const root = document.getElementById('telegramFabRoot'); const root = document.querySelector('[data-support-fab]');
if (!root || root.dataset.initialized === 'true') { if (!root || root.dataset.initialized === 'true') {
return; return;
} }
const toggle = root.querySelector('[data-support-fab-toggle]');
const menu = root.querySelector('[data-support-fab-menu]');
const focusableSelectors = 'a[href], button:not([disabled])';
if (!toggle || !menu) {
return;
}
root.dataset.initialized = 'true'; root.dataset.initialized = 'true';
const dataset = root.dataset; function openMenu() {
const offcanvasElement = document.getElementById('telegramPremiumOffcanvas'); root.classList.add('is-open');
const loadingSection = document.getElementById('telegramFabLoading'); menu.hidden = false;
const connectedSection = document.getElementById('telegramFabConnected'); toggle.setAttribute('aria-expanded', 'true');
const disconnectedSection = document.getElementById('telegramFabDisconnected');
const alertElement = document.getElementById('telegramFabAlert');
const retryButton = root.querySelector('.telegram-fab-retry');
const openButton = root.querySelector('.telegram-fab-open');
const unlinkButton = root.querySelector('.telegram-fab-unlink');
const usernameElement = root.querySelector('.telegram-fab-username');
const widgetPlaceholder = document.getElementById('telegramLoginWidgetPlaceholder');
const toastContainer = document.getElementById('telegramFabToasts');
const fabButton = document.getElementById('telegramPremiumFab');
if (!offcanvasElement || !loadingSection || !connectedSection || !disconnectedSection) { const firstItem = menu.querySelector(focusableSelectors);
console.warn('[Telegram FAB] Missing required DOM nodes. Aborting initialization.'); if (firstItem) {
firstItem.focus();
}
}
function closeMenu() {
if (!root.classList.contains('is-open')) {
return; return;
} }
const offcanvas = bootstrap.Offcanvas.getOrCreateInstance(offcanvasElement); root.classList.remove('is-open');
let currentStatus = null; menu.hidden = true;
let currentDeepLink = null; toggle.setAttribute('aria-expanded', 'false');
let widgetMounted = false; toggle.focus();
const messages = {
loading: dataset.statusLoading || 'Loading...',
error: dataset.statusError || 'Something went wrong.',
connectedTemplate: dataset.statusConnected || 'Connected as {0}',
disconnected: dataset.statusDisconnected || '',
linkSuccess: dataset.linkSuccess || 'Linked successfully.',
unlinkSuccess: dataset.unlinkSuccess || 'Unlinked successfully.',
linkError: dataset.linkError || 'Unable to link. Try again.',
unlinkError: dataset.unlinkError || 'Unable to unlink. Try again.',
retry: dataset.retryLabel || 'Try again',
open: dataset.openLabel || 'Open',
unlink: dataset.unlinkLabel || 'Unlink'
};
function toggleSections(state) {
loadingSection.classList.toggle('d-none', state !== 'loading');
connectedSection.classList.toggle('d-none', state !== 'connected');
disconnectedSection.classList.toggle('d-none', state !== 'disconnected');
} }
function showAlert(message) { toggle.addEventListener('click', function () {
if (!alertElement) { if (root.classList.contains('is-open')) {
return; closeMenu();
}
alertElement.textContent = message;
alertElement.classList.remove('d-none');
}
function hideAlert() {
if (!alertElement) {
return;
}
alertElement.classList.add('d-none');
alertElement.textContent = '';
}
function showToast(message, type) {
if (!toastContainer || !message) {
return;
}
const palette = {
success: 'text-bg-success',
error: 'text-bg-danger',
info: 'text-bg-info'
};
const toast = document.createElement('div');
toast.className = `toast align-items-center ${palette[type] || palette.info} border-0`;
toast.role = 'status';
toast.setAttribute('aria-live', 'polite');
toast.innerHTML = [
'<div class="d-flex">',
`<div class="toast-body">${message}</div>`,
'<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>',
'</div>'
].join('');
toastContainer.appendChild(toast);
const toastInstance = bootstrap.Toast.getOrCreateInstance(toast, {
delay: 4000,
autohide: true
});
toast.addEventListener('hidden.bs.toast', function () {
toast.remove();
});
toastInstance.show();
}
function updateConnectedView(username, deepLink) {
toggleSections('connected');
hideAlert();
if (usernameElement) {
usernameElement.textContent = username || '';
}
currentDeepLink = deepLink || null;
}
function updateDisconnectedView() {
toggleSections('disconnected');
hideAlert();
if (!widgetMounted) {
mountLoginWidget();
}
}
function applyErrorState(message) {
toggleSections('disconnected');
showAlert(message || messages.error);
if (retryButton) {
retryButton.classList.remove('d-none');
}
}
async function fetchStatus() {
toggleSections('loading');
hideAlert();
try {
const response = await fetch('/telegram/status', {
method: 'GET',
credentials: 'same-origin',
headers: {
'Accept': 'application/json'
}
});
if (response.status === 401 || response.status === 403) {
if (fabButton) {
fabButton.setAttribute('hidden', 'hidden');
fabButton.setAttribute('tabindex', '-1');
}
root.setAttribute('aria-hidden', 'true');
root.style.display = 'none';
return;
}
if (!response.ok) {
throw new Error('status_request_failed');
}
currentStatus = await response.json();
if (currentStatus && currentStatus.connected) {
updateConnectedView(currentStatus.username || '', currentStatus.deepLink || null);
} else { } else {
updateDisconnectedView(); openMenu();
}
} catch (error) {
console.warn('[Telegram FAB] Status request failed:', error);
applyErrorState(messages.error);
}
}
function mountLoginWidget() {
const botUsernameRaw = (dataset.botUsername || '').trim();
if (!botUsernameRaw) {
console.warn('[Telegram FAB] Telegram bot username missing. Configure Telegram:LoginWidgetBotUsername.');
applyErrorState(messages.error);
return;
}
const botUsername = botUsernameRaw.startsWith('@')
? botUsernameRaw.slice(1)
: botUsernameRaw;
if (!widgetPlaceholder) {
return;
}
widgetPlaceholder.innerHTML = '';
const widgetScript = document.createElement('script');
widgetScript.src = 'https://telegram.org/js/telegram-widget.js?22';
widgetScript.async = true;
widgetScript.dataset.telegramLogin = botUsername;
widgetScript.dataset.size = 'large';
widgetScript.dataset.userpic = 'false';
widgetScript.dataset.requestAccess = dataset.requestAccess || 'write';
const lang = (dataset.telegramLang || 'en').replace('-', '_');
widgetScript.dataset.lang = lang;
widgetScript.dataset.onauth = 'telegramFabOnAuth';
widgetPlaceholder.appendChild(widgetScript);
widgetMounted = true;
}
async function linkTelegram(payload) {
toggleSections('loading');
hideAlert();
try {
const response = await fetch('/telegram/link', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error('link_failed');
}
showToast(messages.linkSuccess, 'success');
await fetchStatus();
} catch (error) {
console.error('[Telegram FAB] Link failed:', error);
showToast(messages.linkError, 'error');
applyErrorState(messages.linkError);
}
}
async function unlinkTelegram() {
toggleSections('loading');
hideAlert();
try {
const response = await fetch('/telegram/unlink', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Accept': 'application/json'
} }
}); });
if (!response.ok) { toggle.addEventListener('keydown', function (event) {
throw new Error('unlink_failed'); if (event.key === 'ArrowDown') {
}
showToast(messages.unlinkSuccess, 'success');
widgetMounted = false;
await fetchStatus();
} catch (error) {
console.error('[Telegram FAB] Unlink failed:', error);
showToast(messages.unlinkError, 'error');
toggleSections('connected');
showAlert(messages.unlinkError);
}
}
offcanvasElement.addEventListener('show.bs.offcanvas', function () {
fetchStatus();
});
if (retryButton) {
retryButton.addEventListener('click', function () {
hideAlert();
fetchStatus();
});
}
if (openButton) {
openButton.addEventListener('click', function () {
if (currentDeepLink) {
window.open(currentDeepLink, '_blank', 'noopener');
} else {
fetchStatus();
}
});
}
if (unlinkButton) {
unlinkButton.addEventListener('click', function () {
unlinkTelegram();
});
}
window.telegramFabOnAuth = function (user) {
if (!user) {
showToast(messages.linkError, 'error');
return;
}
const payload = {
id: user.id,
hash: user.hash,
first_name: user.first_name,
last_name: user.last_name,
username: user.username,
photo_url: user.photo_url,
auth_date: user.auth_date,
bot_id: dataset.botId || null
};
linkTelegram(payload);
};
if (fabButton) {
fabButton.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault(); event.preventDefault();
offcanvas.show(); openMenu();
}
});
menu.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
event.preventDefault();
closeMenu();
}
});
menu.querySelectorAll(focusableSelectors).forEach(function (item) {
item.addEventListener('click', closeMenu);
});
document.addEventListener('click', function (event) {
if (!root.contains(event.target)) {
closeMenu();
}
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
closeMenu();
} }
}); });
} }
}
document.addEventListener('DOMContentLoaded', initTelegramFab); document.addEventListener('DOMContentLoaded', initSupportFab);
window.initTelegramFab = initTelegramFab;
})(); })();