fix: ajuste de warnings e combos
All checks were successful
Deploy QR Rapido / test (push) Successful in 3m50s
Deploy QR Rapido / build-and-push (push) Successful in 16m49s
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 2026-01-25 00:44:34 -03:00
parent 4a7cdbf26d
commit dbd3c8851b
12 changed files with 282 additions and 156 deletions

View File

@ -38,7 +38,7 @@ namespace QRRapidoApp.Configuration
{ {
try try
{ {
var secretValue = File.ReadAllText(secretFilePath).Trim(); var secretValue = (string?)File.ReadAllText(secretFilePath).Trim();
if (!string.IsNullOrEmpty(secretValue)) if (!string.IsNullOrEmpty(secretValue))
{ {
Data[configKey] = secretValue; Data[configKey] = secretValue;

View File

@ -170,7 +170,9 @@ namespace QRRapidoApp.Controllers
var user = await _userService.GetUserByProviderAsync(scheme, providerId); var user = await _userService.GetUserByProviderAsync(scheme, providerId);
if (user == null) if (user == null)
{ {
user = await _userService.CreateUserAsync(email, name ?? email, scheme, providerId); // Fix CS8625: Ensure name is not null
var safeName = !string.IsNullOrEmpty(name) ? name : (email ?? "User");
user = await _userService.CreateUserAsync(email, safeName, scheme, providerId);
} }
else else
{ {
@ -180,10 +182,10 @@ namespace QRRapidoApp.Controllers
// Create application claims // Create application claims
var claims = new List<Claim> var claims = new List<Claim>
{ {
new Claim(ClaimTypes.NameIdentifier, user.Id), new Claim(ClaimTypes.NameIdentifier, user.Id ?? string.Empty), // Fix CS8625
new Claim(ClaimTypes.Email, user.Email), new Claim(ClaimTypes.Email, user.Email ?? string.Empty), // Fix CS8625
new Claim(ClaimTypes.Name, user.Name), new Claim(ClaimTypes.Name, user.Name ?? string.Empty), // Fix CS8625
new Claim("Provider", user.Provider), new Claim("Provider", user.Provider ?? string.Empty),
new Claim("IsPremium", user.IsPremium.ToString()) new Claim("IsPremium", user.IsPremium.ToString())
}; };
@ -230,6 +232,7 @@ namespace QRRapidoApp.Controllers
return RedirectToAction("Login"); return RedirectToAction("Login");
} }
// Ensure we are passing a non-null userId
ViewBag.QRHistory = await _userService.GetUserQRHistoryAsync(userId, 10); ViewBag.QRHistory = await _userService.GetUserQRHistoryAsync(userId, 10);
ViewBag.MonthlyQRCount = await _userService.GetQRCountThisMonthAsync(userId); ViewBag.MonthlyQRCount = await _userService.GetQRCountThisMonthAsync(userId);
ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId); ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId);

View File

@ -33,21 +33,69 @@ namespace QRRapidoApp.Controllers
_markdownService = markdownService; _markdownService = markdownService;
} }
public async Task<IActionResult> Index() // Default fallback route handled by Program.cs map
[HttpGet]
public async Task<IActionResult> Index(string? qrType = null)
{ {
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
// Pass the requested QR type to the view to auto-select in combo
ViewBag.SelectedQRType = qrType;
// Set SEO Meta Tags based on QR Type
if (!string.IsNullOrEmpty(qrType))
{
switch (qrType.ToLower())
{
case "pix":
ViewBag.Title = "Gerador de Pix Grátis";
ViewBag.Description = "Crie QR Code PIX estático gratuitamente. Ideal para receber pagamentos e doações.";
ViewBag.Keywords = "pix, qr code pix, gerador pix, pix estatico, receber pix";
break;
case "wifi":
ViewBag.Title = _localizer["WiFiQRTitle"];
ViewBag.Description = _localizer["WiFiQRDescription"];
break;
case "vcard":
ViewBag.Title = _localizer["VCardQRTitle"] ?? "Gerador de Cartão de Visita Digital";
ViewBag.Description = _localizer["VCardQRDescription"];
break;
case "whatsapp":
ViewBag.Title = "Gerador de Link WhatsApp";
ViewBag.Description = _localizer["WhatsAppQRDescription"];
break;
case "email":
ViewBag.Title = "Gerador de QR Code Email";
ViewBag.Description = _localizer["EmailQRDescription"];
break;
case "sms":
ViewBag.Title = "Gerador de QR Code SMS";
ViewBag.Description = _localizer["SMSQRDescription"];
break;
case "text":
ViewBag.Title = "Gerador de Texto para QR";
ViewBag.Description = _localizer["TextQRDescription"];
break;
case "url":
ViewBag.Title = "Gerador de URL para QR";
ViewBag.Description = _localizer["URLQRDescription"];
break;
}
}
else
{
// Default SEO
ViewBag.Title = _config["App:TaglinePT"];
ViewBag.Keywords = _config["SEO:KeywordsPT"];
ViewBag.Description = _localizer["QRGenerateDescription"];
}
ViewBag.ShowAds = await _adDisplayService.ShouldShowAds(userId); ViewBag.ShowAds = await _adDisplayService.ShouldShowAds(userId);
ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId ?? ""); ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId ?? "");
ViewBag.IsAuthenticated = User.Identity?.IsAuthenticated ?? false; ViewBag.IsAuthenticated = User?.Identity?.IsAuthenticated ?? false;
ViewBag.UserName = User.Identity?.Name ?? ""; ViewBag.UserName = User?.Identity?.Name ?? "";
_adDisplayService.SetViewBagAds(ViewBag); _adDisplayService.SetViewBagAds(ViewBag);
// SEO and Analytics data
ViewBag.Title = _config["App:TaglinePT"];
ViewBag.Keywords = _config["SEO:KeywordsPT"];
ViewBag.Description = _localizer["QRGenerateDescription"];
// User stats for logged in users // User stats for logged in users
if (!string.IsNullOrEmpty(userId)) if (!string.IsNullOrEmpty(userId))
{ {
@ -55,9 +103,42 @@ namespace QRRapidoApp.Controllers
ViewBag.MonthlyQRCount = await _userService.GetQRCountThisMonthAsync(userId); ViewBag.MonthlyQRCount = await _userService.GetQRCountThisMonthAsync(userId);
} }
return View(); return View("Index");
} }
// Dedicated SEO Routes - These act as virtual pages
[Route("pix")]
[Route("{culture}/pix")]
public async Task<IActionResult> Pix() => await Index("pix");
[Route("wifi")]
[Route("{culture}/wifi")]
public async Task<IActionResult> Wifi() => await Index("wifi");
[Route("vcard")]
[Route("{culture}/vcard")]
public async Task<IActionResult> VCard() => await Index("vcard");
[Route("whatsapp")]
[Route("{culture}/whatsapp")]
public async Task<IActionResult> WhatsApp() => await Index("whatsapp");
[Route("email")]
[Route("{culture}/email")]
public async Task<IActionResult> Email() => await Index("email");
[Route("sms")]
[Route("{culture}/sms")]
public async Task<IActionResult> Sms() => await Index("sms");
[Route("text")]
[Route("{culture}/text")]
public async Task<IActionResult> Text() => await Index("text");
[Route("url")]
[Route("{culture}/url")]
public async Task<IActionResult> UrlType() => await Index("url");
public IActionResult Privacy() public IActionResult Privacy()
{ {
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
@ -205,91 +286,102 @@ namespace QRRapidoApp.Controllers
return Ok(new { status = "healthy", timestamp = DateTime.UtcNow }); return Ok(new { status = "healthy", timestamp = DateTime.UtcNow });
} }
// Sitemap endpoint for SEO // Sitemap endpoint for SEO
[Route("sitemap.xml")] [Route("sitemap.xml")]
public async Task<IActionResult> Sitemap() public async Task<IActionResult> Sitemap()
{ {
var baseUrl = "https://qrrapido.site"; var baseUrl = "https://qrrapido.site";
var now = DateTime.UtcNow.ToString("yyyy-MM-dd"); var now = DateTime.UtcNow.ToString("yyyy-MM-dd");
var sitemapBuilder = new System.Text.StringBuilder(); var sitemapBuilder = new System.Text.StringBuilder();
sitemapBuilder.AppendLine(@"<?xml version=""1.0"" encoding=""UTF-8""?>"); sitemapBuilder.AppendLine(@"<?xml version=""1.0"" encoding=""UTF-8""?>");
sitemapBuilder.AppendLine(@"<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">"); sitemapBuilder.AppendLine(@"<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">");
void AppendUrl(string relativePath, string changeFreq, string priority, string? lastModOverride = null) void AppendUrl(string relativePath, string changeFreq, string priority, string? lastModOverride = null)
{ {
var normalizedPath = relativePath.StartsWith("/") var normalizedPath = relativePath.StartsWith("/")
? relativePath ? relativePath
: $"/{relativePath}"; : $"/{relativePath}";
var lastModValue = lastModOverride ?? now; var lastModValue = lastModOverride ?? now;
sitemapBuilder.AppendLine($@" sitemapBuilder.AppendLine($@"
<url> <url>
<loc>{baseUrl}{normalizedPath}</loc> <loc>{baseUrl}{normalizedPath}</loc>
<lastmod>{lastModValue}</lastmod> <lastmod>{lastModValue}</lastmod>
<changefreq>{changeFreq}</changefreq> <changefreq>{changeFreq}</changefreq>
<priority>{priority}</priority> <priority>{priority}</priority>
</url>"); </url>");
} }
// Core entry points // Core entry points
AppendUrl("/", "daily", "1.0"); AppendUrl("/", "daily", "1.0");
AppendUrl("/pt-BR", "daily", "0.9"); AppendUrl("/pt-BR", "daily", "0.9");
AppendUrl("/es-PY", "daily", "0.9"); AppendUrl("/es-PY", "daily", "0.9");
var cultures = new[] { "pt-BR", "es-PY" }; // Tools (Virtual Pages)
var informationalPages = new[] var tools = new[] { "pix", "wifi", "vcard", "whatsapp", "email", "sms", "text", "url" };
{ var cultures = new[] { "pt-BR", "es-PY" };
new { Path = "Home/About", ChangeFreq = "monthly", Priority = "0.8" },
new { Path = "Home/Contact", ChangeFreq = "monthly", Priority = "0.8" }, foreach (var culture in cultures)
new { Path = "Home/FAQ", ChangeFreq = "weekly", Priority = "0.9" }, {
new { Path = "Home/HowToUse", ChangeFreq = "weekly", Priority = "0.8" }, foreach (var tool in tools)
new { Path = "Home/Privacy", ChangeFreq = "monthly", Priority = "0.5" }, {
new { Path = "Home/Terms", ChangeFreq = "monthly", Priority = "0.5" } AppendUrl($"/{culture}/{tool}", "weekly", "0.9");
}; }
}
foreach (var page in informationalPages)
{ var informationalPages = new[]
AppendUrl($"/{page.Path}", page.ChangeFreq, page.Priority); {
} new { Path = "Home/About", ChangeFreq = "monthly", Priority = "0.8" },
new { Path = "Home/Contact", ChangeFreq = "monthly", Priority = "0.8" },
foreach (var culture in cultures) new { Path = "Home/FAQ", ChangeFreq = "weekly", Priority = "0.9" },
{ new { Path = "Home/HowToUse", ChangeFreq = "weekly", Priority = "0.8" },
foreach (var page in informationalPages) new { Path = "Home/Privacy", ChangeFreq = "monthly", Priority = "0.5" },
{ new { Path = "Home/Terms", ChangeFreq = "monthly", Priority = "0.5" }
AppendUrl($"/{culture}/{page.Path}", page.ChangeFreq, page.Priority); };
}
} foreach (var page in informationalPages)
{
// Dynamic tutorial pages AppendUrl($"/{page.Path}", page.ChangeFreq, page.Priority);
try }
{
var allArticles = await _markdownService.GetAllArticlesForSitemapAsync(); foreach (var culture in cultures)
{
foreach (var article in allArticles) foreach (var page in informationalPages)
{ {
var slug = !string.IsNullOrWhiteSpace(article.Slug) AppendUrl($"/{culture}/{page.Path}", page.ChangeFreq, page.Priority);
? article.Slug }
: article.Title.ToLower().Replace(" ", "-"); }
var encodedSlug = System.Uri.EscapeDataString(slug); // Dynamic tutorial pages
var lastMod = article.LastMod.ToString("yyyy-MM-dd"); try
{
AppendUrl($"/{article.Culture}/tutoriais/{encodedSlug}", "weekly", "0.8", lastMod); var allArticles = await _markdownService.GetAllArticlesForSitemapAsync();
}
foreach (var article in allArticles)
_logger.LogInformation("Generated sitemap with {Count} tutorial articles", allArticles.Count); {
} var slug = !string.IsNullOrWhiteSpace(article.Slug)
catch (Exception ex) ? article.Slug
{ : article.Title.ToLower().Replace(" ", "-");
_logger.LogError(ex, "Error adding tutorials to sitemap");
} var encodedSlug = System.Uri.EscapeDataString(slug);
var lastMod = article.LastMod.ToString("yyyy-MM-dd");
sitemapBuilder.AppendLine("</urlset>");
AppendUrl($"/{article.Culture}/tutoriais/{encodedSlug}", "weekly", "0.8", lastMod);
return Content(sitemapBuilder.ToString(), "application/xml"); }
}
_logger.LogInformation("Generated sitemap with {Count} tutorial articles", allArticles.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error adding tutorials to sitemap");
}
sitemapBuilder.AppendLine("</urlset>");
return Content(sitemapBuilder.ToString(), "application/xml");
}
} }
//public class ErrorViewModel //public class ErrorViewModel
@ -297,4 +389,4 @@ namespace QRRapidoApp.Controllers
// public string? RequestId { get; set; } // public string? RequestId { get; set; }
// public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); // public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
//} //}
} }

View File

@ -40,13 +40,9 @@ namespace QRRapidoApp.Middleware
var detectedCulture = DetectBrowserLanguage(context); var detectedCulture = DetectBrowserLanguage(context);
// If the detected culture is the default, do not redirect. // ALWAYS Redirect to include culture in path for consistency and SEO
if (detectedCulture == DefaultCulture) // This ensures /pix becomes /pt-BR/pix or /es-PY/pix
{
await _next(context);
return;
}
var redirectUrl = $"/{detectedCulture}"; var redirectUrl = $"/{detectedCulture}";
if (!string.IsNullOrEmpty(path)) if (!string.IsNullOrEmpty(path))
{ {

View File

@ -30,22 +30,10 @@ using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using AspNetCore.DataProtection.MongoDb; using AspNetCore.DataProtection.MongoDb;
// Fix for WSL path issues - disable StaticWebAssets completely // Disable StaticWebAssets for WSL compatibility (keeping this as it might still be needed for your mixed env)
var options = new WebApplicationOptions
{
Args = args,
ContentRootPath = Directory.GetCurrentDirectory(),
WebRootPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")
};
// Disable StaticWebAssets for WSL compatibility
Environment.SetEnvironmentVariable("ASPNETCORE_HOSTINGSTARTUP__STATICWEBASSETS__ENABLED", "false"); Environment.SetEnvironmentVariable("ASPNETCORE_HOSTINGSTARTUP__STATICWEBASSETS__ENABLED", "false");
var builder = WebApplication.CreateBuilder(options); var builder = WebApplication.CreateBuilder(args);
// Add Docker Secrets as configuration source (for Swarm deployments)
// Secrets override values from appsettings.json
builder.Configuration.AddDockerSecrets();
// Configure Serilog // Configure Serilog
var loggerConfig = new LoggerConfiguration() var loggerConfig = new LoggerConfiguration()

View File

@ -22,7 +22,7 @@
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" /> <PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.1-dev-00953" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.1-dev-00953" />
<PackageReference Include="Serilog.Sinks.OpenSearch" Version="1.3.0" /> <PackageReference Include="Serilog.Sinks.OpenSearch" Version="1.3.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.4" /> <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.4" />
<PackageReference Include="Stripe.net" Version="48.4.0" /> <PackageReference Include="Stripe.net" Version="48.4.0" />
<PackageReference Include="StackExchange.Redis" Version="2.7.4" /> <PackageReference Include="StackExchange.Redis" Version="2.7.4" />

View File

@ -760,6 +760,9 @@
<data name="FastestQRGeneratorDescription" xml:space="preserve"> <data name="FastestQRGeneratorDescription" xml:space="preserve">
<value>El generador de códigos QR pya'eve (ás rápido) de la web. Rei rehe (Gratis), seguro y confiable.</value> <value>El generador de códigos QR pya'eve (ás rápido) de la web. Rei rehe (Gratis), seguro y confiable.</value>
</data> </data>
<data name="Tools" xml:space="preserve">
<value>Herramientas</value>
</data>
<data name="UsefulLinks" xml:space="preserve"> <data name="UsefulLinks" xml:space="preserve">
<value>Enlaces Útiles</value> <value>Enlaces Útiles</value>
</data> </data>

View File

@ -813,6 +813,9 @@
<data name="FastestQRGeneratorDescription" xml:space="preserve"> <data name="FastestQRGeneratorDescription" xml:space="preserve">
<value>O gerador de QR codes mais rápido da web. Grátis, seguro e confiável.</value> <value>O gerador de QR codes mais rápido da web. Grátis, seguro e confiável.</value>
</data> </data>
<data name="Tools" xml:space="preserve">
<value>Ferramentas</value>
</data>
<data name="UsefulLinks" xml:space="preserve"> <data name="UsefulLinks" xml:space="preserve">
<value>Links Úteis</value> <value>Links Úteis</value>
</data> </data>

View File

@ -97,47 +97,61 @@ namespace QRRapidoApp.Services
switch (stripeEvent.Type) switch (stripeEvent.Type)
{ {
case "checkout.session.completed": case "checkout.session.completed":
var session = stripeEvent.Data.Object as Session; if (stripeEvent.Data.Object is Session session)
if (session?.SubscriptionId != null)
{ {
var subscriptionService = new SubscriptionService(); if (session.SubscriptionId != null)
var subscription = await subscriptionService.GetAsync(session.SubscriptionId); {
await ProcessSubscriptionActivation(session.ClientReferenceId, subscription); var subscriptionService = new SubscriptionService();
var subscription = await subscriptionService.GetAsync(session.SubscriptionId);
// Fix CS8604: Ensure ClientReferenceId is not null
var userId = session.ClientReferenceId ??
(session.Metadata != null && session.Metadata.ContainsKey("user_id") ? session.Metadata["user_id"] : null);
if (!string.IsNullOrEmpty(userId))
{
await ProcessSubscriptionActivation(userId, subscription);
}
else
{
_logger.LogWarning($"Missing userId in checkout session {session.Id}");
}
}
} }
break; break;
case "invoice.finalized": case "invoice.finalized":
var invoice = stripeEvent.Data.Object as Invoice; if (stripeEvent.Data.Object is Invoice invoice)
var subscriptionLineItem = invoice.Lines?.Data
.FirstOrDefault(line =>
!string.IsNullOrEmpty(line.SubscriptionId) ||
line.Subscription != null
);
string subscriptionId = null;
if (subscriptionLineItem != null)
{ {
// Tenta obter o ID da assinatura de duas formas diferentes var subscriptionLineItem = invoice.Lines?.Data
subscriptionId = subscriptionLineItem.SubscriptionId .FirstOrDefault(line =>
?? subscriptionLineItem.Subscription?.Id; !string.IsNullOrEmpty(line.SubscriptionId) ||
} line.Subscription != null
);
if (subscriptionId != null) string? subscriptionId = null;
{
var subscriptionService = new SubscriptionService(); if (subscriptionLineItem != null)
var subscription = await subscriptionService.GetAsync(subscriptionId);
var user = await _userService.GetUserByStripeCustomerIdAsync(subscription.CustomerId);
if (user != null)
{ {
await ProcessSubscriptionActivation(user.Id, subscription); // Tenta obter o ID da assinatura de duas formas diferentes
subscriptionId = subscriptionLineItem.SubscriptionId
?? subscriptionLineItem.Subscription?.Id;
}
if (subscriptionId != null)
{
var subscriptionService = new SubscriptionService();
var subscription = await subscriptionService.GetAsync(subscriptionId);
var user = await _userService.GetUserByStripeCustomerIdAsync(subscription.CustomerId);
if (user != null)
{
await ProcessSubscriptionActivation(user.Id, subscription);
}
} }
} }
break; break;
case "customer.subscription.deleted": case "customer.subscription.deleted":
var deletedSubscription = stripeEvent.Data.Object as Subscription; if (stripeEvent.Data.Object is Subscription deletedSubscription)
if (deletedSubscription != null)
{ {
await _userService.DeactivatePremiumStatus(deletedSubscription.Id); await _userService.DeactivatePremiumStatus(deletedSubscription.Id);
} }

View File

@ -1580,6 +1580,24 @@
} }
}); });
} }
// Auto-select QR Type from Server Route (SEO)
const serverSelectedType = '@(ViewBag.SelectedQRType ?? "")';
if (serverSelectedType) {
const qrTypeSelect = document.getElementById('qr-type');
if (qrTypeSelect) {
// Find option with this value (case-insensitive)
const option = Array.from(qrTypeSelect.options).find(opt =>
opt.value.toLowerCase() === serverSelectedType.toLowerCase()
);
if (option) {
qrTypeSelect.value = option.value;
// Trigger change event to load correct interface
qrTypeSelect.dispatchEvent(new Event('change'));
}
}
}
}); });
</script> </script>
} }

View File

@ -404,15 +404,24 @@
<h5>QR Rapido</h5> <h5>QR Rapido</h5>
<p class="small">@Localizer["FastestQRGeneratorDescription"]</p> <p class="small">@Localizer["FastestQRGeneratorDescription"]</p>
</div> </div>
<div class="col-md-3"> <div class="col-md-2">
<h6>@Localizer["Tools"]</h6>
<ul class="list-unstyled small">
<li><a href="/pix" class="text-light text-decoration-none">Gerador de PIX</a></li>
<li><a href="/wifi" class="text-light text-decoration-none">QR Code WiFi</a></li>
<li><a href="/whatsapp" class="text-light text-decoration-none">Link WhatsApp</a></li>
<li><a href="/vcard" class="text-light text-decoration-none">Cartão Digital</a></li>
</ul>
</div>
<div class="col-md-2">
<h6>@Localizer["UsefulLinks"]</h6> <h6>@Localizer["UsefulLinks"]</h6>
<ul class="list-unstyled"> <ul class="list-unstyled small">
<li><a href="@Url.Action("Privacy", "Home")" class="text-light">@Localizer["Privacy"]</a></li> <li><a href="@Url.Action("Privacy", "Home")" class="text-light">@Localizer["Privacy"]</a></li>
<li><a href="@Url.Action("Terms", "Home")" class="text-light">@Localizer["TermsOfUse"]</a></li> <li><a href="@Url.Action("Terms", "Home")" class="text-light">@Localizer["TermsOfUse"]</a></li>
<li><a href="@Url.Action("Upgrade", "Premium")" class="text-warning">Premium</a></li> <li><a href="@Url.Action("Upgrade", "Premium")" class="text-warning">Premium</a></li>
</ul> </ul>
</div> </div>
<div class="col-md-3"> <div class="col-md-2">
<h6>@Localizer["Support"]</h6> <h6>@Localizer["Support"]</h6>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li><a href="mailto:contato@qrrapido.site" class="text-light">Contato</a></li> <li><a href="mailto:contato@qrrapido.site" class="text-light">Contato</a></li>

View File

@ -1994,8 +1994,8 @@ class QRRapidoGenerator {
const urlPreview = document.getElementById('url-preview'); const urlPreview = document.getElementById('url-preview');
// Helper to safely hide // Helper to safely hide
const safeHide = (el) => { if (el) el.style.display = 'none'; }; const safeHide = (el) => { if (el) { el.style.display = 'none'; el.classList.add('disabled-state'); } };
const safeShow = (el) => { if (el) el.style.display = 'block'; }; const safeShow = (el) => { if (el) { el.style.display = 'block'; el.classList.remove('disabled-state'); } };
// 1. Hide EVERYTHING specific first // 1. Hide EVERYTHING specific first
safeHide(vcardInterface); safeHide(vcardInterface);