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">
<value>Soporte Premium (usuarios pagos)</value>
</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">
<value>Servicios Profesionales</value>
</data>

View File

@ -1,110 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: 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>
<?xml version='1.0' encoding='utf-8'?>
<root xmlns:ns1="urn:schemas-microsoft-com:xml-msdata" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:schema id="root">
<xs:import namespace="http://www.w3.org/XML/1998/namespace" />
<xs:element name="root" ns1:IsDataSet="true">
<xs:complexType>
<xs:choice maxOccurs="unbounded">
<xs:element name="metadata">
<xs:complexType>
<xs:sequence>
<xs:element name="value" type="xsd:string" minOccurs="0" />
</xs:sequence>
<xs:attribute name="name" use="required" type="xsd:string" />
<xs:attribute name="type" type="xsd:string" />
<xs:attribute name="mimetype" type="xsd:string" />
<xs:attribute ref="xml:space" />
</xs:complexType>
</xs:element>
<xs:element name="assembly">
<xs:complexType>
<xs:attribute name="alias" type="xsd:string" />
<xs:attribute name="name" type="xsd:string" />
</xs:complexType>
</xs:element>
<xs:element name="data">
<xs:complexType>
<xs:sequence>
<xs:element name="value" type="xsd:string" minOccurs="0" ns1:Ordinal="1" />
<xs:element name="comment" type="xsd:string" minOccurs="0" ns1:Ordinal="2" />
</xs:sequence>
<xs:attribute name="name" type="xsd:string" use="required" ns1:Ordinal="1" />
<xs:attribute name="type" type="xsd:string" ns1:Ordinal="3" />
<xs:attribute name="mimetype" type="xsd:string" ns1:Ordinal="4" />
<xs:attribute ref="xml:space" />
</xs:complexType>
</xs:element>
<xs:element name="resheader">
<xs:complexType>
<xs:sequence>
<xs:element name="value" type="xsd:string" minOccurs="0" ns1:Ordinal="1" />
</xs:sequence>
<xs:attribute name="name" type="xsd:string" use="required" />
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
</xs:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
@ -973,4 +914,61 @@
<data name="BackToHome" xml:space="preserve">
<value>Volver al Inicio</value>
</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>

View File

@ -1274,6 +1274,63 @@
<data name="PremiumSupport" xml:space="preserve">
<value>Suporte Premium (usuários pagos)</value>
</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">
<value>Serviços Profissionais</value>
</data>

View File

@ -426,4 +426,61 @@
<data name="Deleting" xml:space="preserve">
<value>Deleting</value>
</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>

View File

@ -75,7 +75,7 @@
<link rel="preload" href="~/css/qrrapido-theme.css" as="style">
<!-- Hotjar Tracking Code for https://qrrapido.site -->
<script>
<script data-cfasync="false">
(function(h,o,t,j,a,r){
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
h._hjSettings={hjid:6550944,hjsv:6};
@ -174,6 +174,7 @@
<!-- 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 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 -->
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
@ -407,8 +408,14 @@
<!-- Cookie Consent Banner -->
@await Html.PartialAsync("_CookieConsent")
@if (isPremiumUser)
{
@await Html.PartialAsync("_TelegramPremiumFab")
}
<!-- Bootstrap 5 JS -->
<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)
{

View File

@ -1,94 +1,51 @@
@using System.Globalization
@using Microsoft.Extensions.Localization
@inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
@{
var culture = CultureInfo.CurrentUICulture;
var telegramLang = culture.Name.Replace('-', '_');
var botUsername = Configuration["Telegram:LoginWidgetBotUsername"] ?? string.Empty;
var botId = Configuration["Telegram:BotId"] ?? string.Empty;
var requestAccess = Configuration["Telegram:LoginWidgetRequestAccess"] ?? "write";
var telegramUrl = Configuration["Support:TelegramUrl"] ?? "https://t.me/jobmakerbr";
var formConfigured = !string.IsNullOrWhiteSpace(Configuration["Support:FormspreeUrl"]);
var formLink = Url.Action("PremiumContact", "Support");
var formEnabled = formConfigured && !string.IsNullOrEmpty(formLink);
}
<div id="telegramFabRoot"
class="telegram-fab-root"
data-telegram-lang="@telegramLang"
data-bot-username="@botUsername"
data-bot-id="@botId"
data-request-access="@requestAccess"
data-status-loading="@Localizer["TelegramPremiumStatusLoading"]"
data-status-error="@Localizer["TelegramPremiumStatusError"]"
data-status-connected="@Localizer["TelegramPremiumStatusConnected"]"
data-status-disconnected="@Localizer["TelegramPremiumStatusDisconnected"]"
data-link-success="@Localizer["TelegramPremiumLinkSuccess"]"
data-unlink-success="@Localizer["TelegramPremiumUnlinkSuccess"]"
data-link-error="@Localizer["TelegramPremiumLinkError"]"
data-unlink-error="@Localizer["TelegramPremiumUnlinkError"]"
data-retry-label="@Localizer["TelegramPremiumRetry"]"
data-open-label="@Localizer["TelegramPremiumOpenTelegram"]"
data-unlink-label="@Localizer["TelegramPremiumUnlink"]">
<button id="telegramPremiumFab"
type="button"
class="btn btn-primary fab-telegram d-flex align-items-center justify-content-center"
data-bs-toggle="offcanvas"
data-bs-target="#telegramPremiumOffcanvas"
aria-controls="telegramPremiumOffcanvas"
aria-label="@Localizer["TelegramPremiumHelp"]"
title="@Localizer["TelegramPremiumHelp"]">
<i class="fab fa-telegram-plane" aria-hidden="true"></i>
<span class="visually-hidden">@Localizer["TelegramPremiumHelp"]</span>
<div class="support-fab-root" data-support-fab>
<div id="supportFabMenu"
class="support-fab-menu"
data-support-fab-menu
role="menu"
aria-label="@Localizer["PremiumSupportMenuIntro"]"
hidden>
<p class="support-fab-text">@Localizer["PremiumSupportMenuIntro"]</p>
<div class="support-fab-actions">
<a class="support-fab-link support-telegram"
href="@telegramUrl"
target="_blank"
rel="noopener"
role="menuitem">
<i class="fab fa-telegram-plane icon" aria-hidden="true"></i>
<span>@Localizer["PremiumSupportOptionTelegram"]</span>
</a>
@if (formEnabled)
{
<a class="support-fab-link support-form"
href="@formLink"
target="_blank"
rel="noopener"
role="menuitem">
<i class="fas fa-envelope-open-text icon" aria-hidden="true"></i>
<span>@Localizer["PremiumSupportOptionForm"]</span>
</a>
}
</div>
</div>
<button type="button"
class="btn btn-primary fab-trigger"
data-support-fab-toggle
aria-controls="supportFabMenu"
aria-expanded="false">
<i class="fas fa-headset" aria-hidden="true"></i>
<span class="fab-trigger-text">@Localizer["PremiumSupportFabButton"]</span>
</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 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 id="telegramFabConnected" class="telegram-fab-connected d-none">
<p class="mb-3 text-body">@Html.Raw(Localizer["TelegramPremiumStatusConnected", "<span class=\"fw-semibold telegram-fab-username\"></span>"])</p>
<div class="d-grid gap-2">
<button type="button" class="btn btn-success telegram-fab-open">
<i class="fab fa-telegram-plane me-2" aria-hidden="true"></i>
@Localizer["TelegramPremiumOpenTelegram"]
</button>
<button type="button" class="btn btn-outline-secondary telegram-fab-unlink">
@Localizer["TelegramPremiumUnlink"]
</button>
</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!",
"Version": "1.0.0"
},
"Support": {
"TelegramUrl": "https://t.me/jobmakerbr",
"FormspreeUrl": "https://formspree.io/f/xpwynqpj"
},
"ApplicationName": "QRRapido-Dev",
"Environment": "Dev",
"Serilog": {

View File

@ -1,6 +1,10 @@
{
"ApplicationName": "QRRapido-Prod",
"Environment": "Prod",
"Support": {
"TelegramUrl": "https://t.me/jobmakerbr",
"FormspreeUrl": "https://formspree.io/f/xpwynqpj"
},
"ConnectionStrings": {
"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,
"GrowthRateWarningMBPerHour": 100,
"IncludeCollectionStats": true,
"CollectionsToMonitor": [ "Users", "QRCodeHistory", "AdFreeSessions" ]
"CollectionsToMonitor": [
"Users",
"QRCodeHistory",
"AdFreeSessions"
]
},
"HealthChecks": {
"MongoDB": {

View File

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

View File

@ -1,80 +1,147 @@
.fab-telegram {
.support-fab-root {
position: fixed;
bottom: var(--telegram-fab-offset, 1.5rem);
right: var(--telegram-fab-offset, 1.5rem);
width: 3rem;
height: 3rem;
border-radius: 50%;
bottom: var(--support-fab-offset, 1.5rem);
right: var(--support-fab-offset, 1.5rem);
display: flex;
flex-direction: column;
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);
z-index: 1052;
opacity: 0.95;
backdrop-filter: blur(2px);
transition: transform 0.2s ease, opacity 0.2s ease;
}
.fab-telegram:hover,
.fab-telegram:focus-visible {
.support-fab-root .fab-trigger:hover,
.support-fab-root .fab-trigger:focus-visible {
opacity: 1;
transform: translateY(-2px);
}
.fab-telegram:focus-visible {
.support-fab-root .fab-trigger:focus-visible {
outline: 2px solid rgba(0, 123, 255, 0.6);
outline-offset: 2px;
}
@media (max-width: 575.98px) {
.fab-telegram {
width: 2.75rem;
height: 2.75rem;
bottom: 1rem;
right: 1rem;
}
.support-fab-root .fab-trigger .fab-trigger-text {
font-weight: 600;
}
.telegram-fab-root .toast-container {
z-index: 1080;
.support-fab-menu {
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 {
width: 1.5rem;
height: 1.5rem;
}
.telegram-fab-widget-placeholder {
min-height: 90px;
.support-fab-root.is-open .support-fab-menu {
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;
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 {
display: inline-block;
height: 0.875rem;
.support-fab-link .icon {
font-size: 1rem;
}
.telegram-fab-widget-skeleton .placeholder.col-8 {
width: 70%;
.support-fab-link.support-telegram {
background: rgba(59, 130, 246, 0.15);
color: #60a5fa;
}
.telegram-fab-widget-skeleton .placeholder.col-6 {
width: 55%;
.support-fab-link.support-telegram:hover,
.support-fab-link.support-telegram:focus-visible {
background: rgba(59, 130, 246, 0.25);
color: #fff;
}
.offcanvas-sm-bottom {
--bs-offcanvas-width: min(420px, 90vw);
.support-fab-link.support-form {
background: rgba(249, 115, 22, 0.15);
color: #f97316;
}
@media (max-width: 767.98px) {
.offcanvas-sm-bottom {
--bs-offcanvas-width: 100%;
--bs-offcanvas-height: min(80vh, 420px);
--bs-offcanvas-transform: translateY(100%);
border-radius: 1rem 1rem 0 0;
width: 100%;
top: auto;
bottom: 0;
left: 0;
right: 0;
.support-fab-link.support-form:hover,
.support-fab-link.support-form:focus-visible {
background: rgba(249, 115, 22, 0.25);
color: #fff;
}
.support-fab-link:hover,
.support-fab-link:focus-visible {
transform: translateY(-1px);
text-decoration: none;
}
.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 initTelegramFab() {
const root = document.getElementById('telegramFabRoot');
function initSupportFab() {
const root = document.querySelector('[data-support-fab]');
if (!root || root.dataset.initialized === 'true') {
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';
const dataset = root.dataset;
const offcanvasElement = document.getElementById('telegramPremiumOffcanvas');
const loadingSection = document.getElementById('telegramFabLoading');
const connectedSection = document.getElementById('telegramFabConnected');
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');
function openMenu() {
root.classList.add('is-open');
menu.hidden = false;
toggle.setAttribute('aria-expanded', 'true');
if (!offcanvasElement || !loadingSection || !connectedSection || !disconnectedSection) {
console.warn('[Telegram FAB] Missing required DOM nodes. Aborting initialization.');
const firstItem = menu.querySelector(focusableSelectors);
if (firstItem) {
firstItem.focus();
}
}
function closeMenu() {
if (!root.classList.contains('is-open')) {
return;
}
const offcanvas = bootstrap.Offcanvas.getOrCreateInstance(offcanvasElement);
let currentStatus = null;
let currentDeepLink = null;
let widgetMounted = false;
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');
root.classList.remove('is-open');
menu.hidden = true;
toggle.setAttribute('aria-expanded', 'false');
toggle.focus();
}
function showAlert(message) {
if (!alertElement) {
return;
}
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);
toggle.addEventListener('click', function () {
if (root.classList.contains('is-open')) {
closeMenu();
} else {
updateDisconnectedView();
}
} 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'
openMenu();
}
});
if (!response.ok) {
throw new Error('unlink_failed');
}
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 === ' ') {
toggle.addEventListener('keydown', function (event) {
if (event.key === 'ArrowDown') {
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);
window.initTelegramFab = initTelegramFab;
document.addEventListener('DOMContentLoaded', initSupportFab);
})();