feat: delete e es-py
Some checks failed
Deploy QR Rapido / test (push) Successful in 29s
Deploy QR Rapido / build-and-push (push) Failing after 4s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped

This commit is contained in:
Ricardo Carneiro 2025-08-04 20:34:29 -03:00
parent 70fbdaa3c2
commit a7af34659b
16 changed files with 663 additions and 82 deletions

View File

@ -3,6 +3,7 @@ using QRRapidoApp.Models;
using QRRapidoApp.Services;
using System.Diagnostics;
using System.Security.Claims;
using Microsoft.Extensions.Localization;
namespace QRRapidoApp.Controllers
{
@ -12,13 +13,15 @@ namespace QRRapidoApp.Controllers
private readonly AdDisplayService _adDisplayService;
private readonly IUserService _userService;
private readonly IConfiguration _config;
private readonly IStringLocalizer<QRRapidoApp.Resources.SharedResource> _localizer;
public HomeController(ILogger<HomeController> logger, AdDisplayService adDisplayService, IUserService userService, IConfiguration config)
public HomeController(ILogger<HomeController> logger, AdDisplayService adDisplayService, IUserService userService, IConfiguration config, IStringLocalizer<QRRapidoApp.Resources.SharedResource> localizer)
{
_logger = logger;
_adDisplayService = adDisplayService;
_userService = userService;
_config = config;
_localizer = localizer;
}
public async Task<IActionResult> Index()
@ -33,7 +36,7 @@ namespace QRRapidoApp.Controllers
// SEO and Analytics data
ViewBag.Title = _config["App:TaglinePT"];
ViewBag.Keywords = _config["SEO:KeywordsPT"];
ViewBag.Description = "QR Rapido: Gere códigos QR em segundos! Gerador ultrarrápido em português e espanhol. Grátis, sem cadastro obrigatório. 30 dias sem anúncios após login.";
ViewBag.Description = _localizer["QRGenerateDescription"];
// User stats for logged in users
if (!string.IsNullOrEmpty(userId))

View File

@ -4,6 +4,7 @@ using QRRapidoApp.Services;
using System.Diagnostics;
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Localization;
namespace QRRapidoApp.Controllers
{
@ -15,13 +16,15 @@ namespace QRRapidoApp.Controllers
private readonly IUserService _userService;
private readonly AdDisplayService _adService;
private readonly ILogger<QRController> _logger;
private readonly IStringLocalizer<QRRapidoApp.Resources.SharedResource> _localizer;
public QRController(IQRCodeService qrService, IUserService userService, AdDisplayService adService, ILogger<QRController> logger)
public QRController(IQRCodeService qrService, IUserService userService, AdDisplayService adService, ILogger<QRController> logger, IStringLocalizer<QRRapidoApp.Resources.SharedResource> localizer)
{
_qrService = qrService;
_userService = userService;
_adService = adService;
_logger = logger;
_localizer = localizer;
}
[HttpPost("GenerateRapid")]
@ -51,13 +54,13 @@ namespace QRRapidoApp.Controllers
if (string.IsNullOrWhiteSpace(request.Content))
{
_logger.LogWarning("QR generation failed - empty content provided");
return BadRequest(new { error = "Conteúdo é obrigatório", success = false });
return BadRequest(new { error = _localizer["RequiredContent"], success = false });
}
if (request.Content.Length > 4000) // Limit to maintain speed
{
_logger.LogWarning("QR generation failed - content too long: {ContentLength} characters", request.Content.Length);
return BadRequest(new { error = "Conteúdo muito longo. Máximo 4000 caracteres.", success = false });
return BadRequest(new { error = _localizer["ContentTooLong"], success = false });
}
// Check user status
@ -70,7 +73,7 @@ namespace QRRapidoApp.Controllers
userId ?? "anonymous", request.CornerStyle);
return BadRequest(new
{
error = "Estilos de borda personalizados são exclusivos do plano Premium. Faça upgrade para usar esta funcionalidade.",
error = _localizer["PremiumCornerStyleRequired"],
requiresPremium = true,
success = false
});
@ -84,7 +87,7 @@ namespace QRRapidoApp.Controllers
userId ?? "anonymous", user?.IsPremium ?? false);
return StatusCode(429, new
{
error = "Limite de QR codes atingido",
error = _localizer["RateLimitReached"],
upgradeUrl = "/Premium/Upgrade",
success = false
});
@ -325,13 +328,13 @@ namespace QRRapidoApp.Controllers
if (string.IsNullOrWhiteSpace(request.Content))
{
_logger.LogWarning("QR generation failed - empty content provided");
return BadRequest(new { error = "Conteúdo é obrigatório", success = false });
return BadRequest(new { error = _localizer["RequiredContent"], success = false });
}
if (request.Content.Length > 4000)
{
_logger.LogWarning("QR generation failed - content too long: {ContentLength} characters", request.Content.Length);
return BadRequest(new { error = "Conteúdo muito longo. Máximo 4000 caracteres.", success = false });
return BadRequest(new { error = _localizer["ContentTooLong"], success = false });
}
// Check user status
@ -343,7 +346,7 @@ namespace QRRapidoApp.Controllers
_logger.LogWarning("Logo upload attempted by non-premium user - UserId: {UserId}", userId ?? "anonymous");
return BadRequest(new
{
error = "Logo personalizado é exclusivo do plano Premium. Faça upgrade para usar esta funcionalidade.",
error = _localizer["PremiumLogoRequired"],
requiresPremium = true,
success = false
});
@ -363,7 +366,7 @@ namespace QRRapidoApp.Controllers
if (logo.Length > 2 * 1024 * 1024)
{
_logger.LogWarning("Logo upload failed - file too large: {FileSize} bytes", logo.Length);
return BadRequest(new { error = "Logo muito grande. Máximo 2MB.", success = false });
return BadRequest(new { error = _localizer["LogoTooLarge"], success = false });
}
// Validate file format
@ -371,7 +374,7 @@ namespace QRRapidoApp.Controllers
if (!allowedTypes.Contains(logo.ContentType?.ToLower()))
{
_logger.LogWarning("Logo upload failed - invalid format: {ContentType}", logo.ContentType);
return BadRequest(new { error = "Formato inválido. Use PNG ou JPG.", success = false });
return BadRequest(new { error = _localizer["InvalidLogoFormat"], success = false });
}
try
@ -388,7 +391,7 @@ namespace QRRapidoApp.Controllers
catch (Exception ex)
{
_logger.LogError(ex, "Error processing logo file");
return BadRequest(new { error = "Erro ao processar logo.", success = false });
return BadRequest(new { error = _localizer["ErrorProcessingLogo"], success = false });
}
}
@ -400,7 +403,7 @@ namespace QRRapidoApp.Controllers
userId ?? "anonymous", user?.IsPremium ?? false);
return StatusCode(429, new
{
error = "Limite de QR codes atingido",
error = _localizer["RateLimitReached"],
upgradeUrl = "/Premium/Upgrade",
success = false
});
@ -512,6 +515,36 @@ namespace QRRapidoApp.Controllers
}
}
[HttpDelete("History/{qrId}")]
public async Task<IActionResult> DeleteFromHistory(string qrId)
{
try
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Unauthorized();
}
var success = await _userService.DeleteQRFromHistoryAsync(userId, qrId);
if (success)
{
_logger.LogInformation("QR code deleted from history - QRId: {QRId}, UserId: {UserId}", qrId, userId);
return Ok(new { success = true, message = _localizer["QRCodeDeleted"] });
}
else
{
return NotFound(new { success = false, message = _localizer["ErrorDeletingQR"] });
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting QR from history - QRId: {QRId}", qrId);
return StatusCode(500, new { success = false, message = _localizer["ErrorDeletingQR"] });
}
}
private async Task<bool> CheckRateLimitAsync(string? userId, Models.User? user)
{
// Premium users have unlimited QR codes

View File

@ -157,8 +157,7 @@ builder.Services.Configure<RequestLocalizationOptions>(options =>
var supportedCultures = new[]
{
new CultureInfo("pt-BR"),
new CultureInfo("es"),
new CultureInfo("en")
new CultureInfo("es-PY")
};
options.DefaultRequestCulture = new RequestCulture("pt-BR", "pt-BR");
@ -267,7 +266,7 @@ app.MapHealthChecks("/health");
// Language routes (must be first)
app.MapControllerRoute(
name: "localized",
pattern: "{culture:regex(^(pt-BR|es|en)$)}/{controller=Home}/{action=Index}/{id?}");
pattern: "{culture:regex(^(pt-BR|es-PY)$)}/{controller=Home}/{action=Index}/{id?}");
// API routes
app.MapControllerRoute(

View File

@ -0,0 +1,217 @@
<?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>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- Content required keys from existing es.resx but adapted for Paraguay Spanish -->
<data name="Tagline" xml:space="preserve">
<value>¡Genera códigos QR en segundos!</value>
</data>
<data name="RequiredContent" xml:space="preserve">
<value>Contenido es obligatorio</value>
</data>
<data name="ContentTooLong" xml:space="preserve">
<value>Contenido muy largo. Máximo 4000 caracteres.</value>
</data>
<data name="PremiumCornerStyleRequired" xml:space="preserve">
<value>Estilos de borde personalizados son exclusivos del plan Premium. Actualiza para usar esta funcionalidad.</value>
</data>
<data name="RateLimitReached" xml:space="preserve">
<value>Límite de códigos QR alcanzado</value>
</data>
<data name="PremiumLogoRequired" xml:space="preserve">
<value>Logo personalizado es exclusivo del plan Premium. Actualiza para usar esta funcionalidad.</value>
</data>
<data name="LogoTooLarge" xml:space="preserve">
<value>Logo muy grande. Máximo 2MB.</value>
</data>
<data name="InvalidLogoFormat" xml:space="preserve">
<value>Formato inválido. Usa PNG o JPG.</value>
</data>
<data name="UserProfileTitle" xml:space="preserve">
<value>Perfil del Usuario</value>
</data>
<data name="HistoryTitle" xml:space="preserve">
<value>Historial de Códigos QR</value>
</data>
<data name="ErrorSavingHistory" xml:space="preserve">
<value>Error al guardar en el historial.</value>
</data>
<data name="FeatureNotAvailable" xml:space="preserve">
<value>Función no disponible</value>
</data>
<data name="QRCodeSavedHistory" xml:space="preserve">
<value>¡Código QR guardado en el historial!</value>
</data>
<data name="ShareError" xml:space="preserve">
<value>Error al compartir. Intenta otro método.</value>
</data>
<data name="LinkCopied" xml:space="preserve">
<value>¡Enlace copiado al portapapeles!</value>
</data>
<data name="EnterQRContent" xml:space="preserve">
<value>Ingresa el contenido del código QR</value>
</data>
<data name="ValidationContentMinLength" xml:space="preserve">
<value>Contenido debe tener al menos 3 caracteres</value>
</data>
<data name="VCardValidationError" xml:space="preserve">
<value>Error en la validación del VCard: </value>
</data>
<data name="FastestGeneratorBrazil" xml:space="preserve">
<value>Código QR generado con QR Rapido - ¡el generador más rápido de Paraguay!</value>
</data>
<data name="QRGenerateDescription" xml:space="preserve">
<value>QR Rapido: ¡Genera códigos QR en segundos! Generador ultra-rápido en español y portugués. Gratis, sin registro obligatorio. 30 días sin anuncios después del login.</value>
</data>
<data name="LogoNotProvided" xml:space="preserve">
<value>Logo no proporcionado</value>
</data>
<data name="LogoTooSmall" xml:space="preserve">
<value>Logo muy pequeño. Mínimo 32x32 píxeles.</value>
</data>
<data name="InvalidImageFormat" xml:space="preserve">
<value>Formato de imagen inválido</value>
</data>
<data name="ErrorProcessingLogo" xml:space="preserve">
<value>Error al procesar logo.</value>
</data>
<data name="DeleteQRCode" xml:space="preserve">
<value>Eliminar Código QR</value>
</data>
<data name="ConfirmDeleteTitle" xml:space="preserve">
<value>Confirmar Eliminación</value>
</data>
<data name="ConfirmDeleteMessage" xml:space="preserve">
<value>¿Estás seguro de que quieres eliminar este código QR de tu historial?</value>
</data>
<data name="Yes" xml:space="preserve">
<value>Sí</value>
</data>
<data name="No" xml:space="preserve">
<value>No</value>
</data>
<data name="QRCodeDeleted" xml:space="preserve">
<value>¡Código QR eliminado exitosamente!</value>
</data>
<data name="ErrorDeletingQR" xml:space="preserve">
<value>Error al eliminar código QR. Inténtalo de nuevo.</value>
</data>
<data name="Deleting" xml:space="preserve">
<value>Eliminando</value>
</data>
</root>

View File

@ -783,4 +783,94 @@
<data name="AnonymousUserLimit" xml:space="preserve">
<value>Usuários anônimos: 3 QR codes por dia</value>
</data>
<data name="RequiredContent" xml:space="preserve">
<value>Conteúdo é obrigatório</value>
</data>
<data name="ContentTooLong" xml:space="preserve">
<value>Conteúdo muito longo. Máximo 4000 caracteres.</value>
</data>
<data name="PremiumCornerStyleRequired" xml:space="preserve">
<value>Estilos de borda personalizados são exclusivos do plano Premium. Faça upgrade para usar esta funcionalidade.</value>
</data>
<data name="RateLimitReached" xml:space="preserve">
<value>Limite de QR codes atingido</value>
</data>
<data name="PremiumLogoRequired" xml:space="preserve">
<value>Logo personalizado é exclusivo do plano Premium. Faça upgrade para usar esta funcionalidade.</value>
</data>
<data name="LogoTooLarge" xml:space="preserve">
<value>Logo muito grande. Máximo 2MB.</value>
</data>
<data name="InvalidLogoFormat" xml:space="preserve">
<value>Formato inválido. Use PNG ou JPG.</value>
</data>
<data name="UserProfileTitle" xml:space="preserve">
<value>Perfil do Usuário</value>
</data>
<data name="HistoryTitle" xml:space="preserve">
<value>Histórico de QR Codes</value>
</data>
<data name="ErrorSavingHistory" xml:space="preserve">
<value>Erro ao salvar no histórico.</value>
</data>
<data name="FeatureNotAvailable" xml:space="preserve">
<value>Funcionalidade não disponível</value>
</data>
<data name="ShareError" xml:space="preserve">
<value>Erro ao compartilhar. Tente outro método.</value>
</data>
<data name="LinkCopied" xml:space="preserve">
<value>Link copiado para a área de transferência!</value>
</data>
<data name="EnterQRContent" xml:space="preserve">
<value>Digite o conteúdo do QR code</value>
</data>
<data name="ValidationContentMinLength" xml:space="preserve">
<value>Conteúdo deve ter pelo menos 3 caracteres</value>
</data>
<data name="VCardValidationError" xml:space="preserve">
<value>Erro na validação do VCard: </value>
</data>
<data name="FastestGeneratorBrazil" xml:space="preserve">
<value>QR Code gerado com QR Rapido - o gerador mais rápido do Brasil!</value>
</data>
<data name="QRGenerateDescription" xml:space="preserve">
<value>QR Rapido: Gere códigos QR em segundos! Gerador ultrarrápido em português e espanhol. Grátis, sem cadastro obrigatório. 30 dias sem anúncios após login.</value>
</data>
<data name="LogoNotProvided" xml:space="preserve">
<value>Logo não fornecido</value>
</data>
<data name="LogoTooSmall" xml:space="preserve">
<value>Logo muito pequeno. Mínimo 32x32 pixels.</value>
</data>
<data name="InvalidImageFormat" xml:space="preserve">
<value>Formato de imagem inválido</value>
</data>
<data name="ErrorProcessingLogo" xml:space="preserve">
<value>Erro ao processar logo.</value>
</data>
<data name="DeleteQRCode" xml:space="preserve">
<value>Excluir QR Code</value>
</data>
<data name="ConfirmDeleteTitle" xml:space="preserve">
<value>Confirmar Exclusão</value>
</data>
<data name="ConfirmDeleteMessage" xml:space="preserve">
<value>Tem certeza que deseja excluir este QR code do seu histórico?</value>
</data>
<data name="Yes" xml:space="preserve">
<value>Sim</value>
</data>
<data name="No" xml:space="preserve">
<value>Não</value>
</data>
<data name="QRCodeDeleted" xml:space="preserve">
<value>QR Code excluído com sucesso!</value>
</data>
<data name="ErrorDeletingQR" xml:space="preserve">
<value>Erro ao excluir QR code. Tente novamente.</value>
</data>
<data name="Deleting" xml:space="preserve">
<value>Excluindo</value>
</data>
</root>

View File

@ -336,4 +336,94 @@
<data name="OpenNetwork" xml:space="preserve">
<value>Sem senha </value>
</data>
<data name="RequiredContent" xml:space="preserve">
<value>Content is required</value>
</data>
<data name="ContentTooLong" xml:space="preserve">
<value>Content too long. Maximum 4000 characters.</value>
</data>
<data name="PremiumCornerStyleRequired" xml:space="preserve">
<value>Custom corner styles are exclusive to Premium plan. Upgrade to use this functionality.</value>
</data>
<data name="RateLimitReached" xml:space="preserve">
<value>QR codes limit reached</value>
</data>
<data name="PremiumLogoRequired" xml:space="preserve">
<value>Custom logo is exclusive to Premium plan. Upgrade to use this functionality.</value>
</data>
<data name="LogoTooLarge" xml:space="preserve">
<value>Logo too large. Maximum 2MB.</value>
</data>
<data name="InvalidLogoFormat" xml:space="preserve">
<value>Invalid format. Use PNG or JPG.</value>
</data>
<data name="UserProfileTitle" xml:space="preserve">
<value>User Profile</value>
</data>
<data name="HistoryTitle" xml:space="preserve">
<value>QR Codes History</value>
</data>
<data name="ErrorSavingHistory" xml:space="preserve">
<value>Error saving to history.</value>
</data>
<data name="FeatureNotAvailable" xml:space="preserve">
<value>Feature not available</value>
</data>
<data name="ShareError" xml:space="preserve">
<value>Error sharing. Try another method.</value>
</data>
<data name="LinkCopied" xml:space="preserve">
<value>Link copied to clipboard!</value>
</data>
<data name="EnterQRContent" xml:space="preserve">
<value>Enter QR code content</value>
</data>
<data name="ValidationContentMinLength" xml:space="preserve">
<value>Content must have at least 3 characters</value>
</data>
<data name="VCardValidationError" xml:space="preserve">
<value>VCard validation error: </value>
</data>
<data name="FastestGeneratorBrazil" xml:space="preserve">
<value>QR Code generated with QR Rapido - the fastest generator in Brazil!</value>
</data>
<data name="QRGenerateDescription" xml:space="preserve">
<value>QR Rapido: Generate QR codes in seconds! Ultra-fast generator. Free, no registration required. 30 days ad-free after login.</value>
</data>
<data name="LogoNotProvided" xml:space="preserve">
<value>Logo not provided</value>
</data>
<data name="LogoTooSmall" xml:space="preserve">
<value>Logo too small. Minimum 32x32 pixels.</value>
</data>
<data name="InvalidImageFormat" xml:space="preserve">
<value>Invalid image format</value>
</data>
<data name="ErrorProcessingLogo" xml:space="preserve">
<value>Error processing logo.</value>
</data>
<data name="DeleteQRCode" xml:space="preserve">
<value>Delete QR Code</value>
</data>
<data name="ConfirmDeleteTitle" xml:space="preserve">
<value>Confirm Deletion</value>
</data>
<data name="ConfirmDeleteMessage" xml:space="preserve">
<value>Are you sure you want to delete this QR code from your history?</value>
</data>
<data name="Yes" xml:space="preserve">
<value>Yes</value>
</data>
<data name="No" xml:space="preserve">
<value>No</value>
</data>
<data name="QRCodeDeleted" xml:space="preserve">
<value>QR Code deleted successfully!</value>
</data>
<data name="ErrorDeletingQR" xml:space="preserve">
<value>Error deleting QR code. Please try again.</value>
</data>
<data name="Deleting" xml:space="preserve">
<value>Deleting</value>
</data>
</root>

View File

@ -22,6 +22,7 @@ namespace QRRapidoApp.Services
Task SaveQRToHistoryAsync(string? userId, QRGenerationResult qrResult);
Task<List<QRCodeHistory>> GetUserQRHistoryAsync(string userId, int limit = 50);
Task<QRCodeHistory?> GetQRDataAsync(string qrId);
Task<bool> DeleteQRFromHistoryAsync(string userId, string qrId);
Task<int> GetQRCountThisMonthAsync(string userId);
Task<string> GetUserEmailAsync(string userId);
Task MarkPremiumCancelledAsync(string userId, DateTime cancelledAt);

View File

@ -11,6 +11,7 @@ using System.Numerics;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Localization;
namespace QRRapidoApp.Services
{
@ -21,13 +22,15 @@ namespace QRRapidoApp.Services
private readonly ILogger<QRRapidoService> _logger;
private readonly SemaphoreSlim _semaphore;
private readonly LogoReadabilityAnalyzer _readabilityAnalyzer;
private readonly IStringLocalizer<QRRapidoApp.Resources.SharedResource> _localizer;
public QRRapidoService(IDistributedCache cache, IConfiguration config, ILogger<QRRapidoService> logger, LogoReadabilityAnalyzer readabilityAnalyzer)
public QRRapidoService(IDistributedCache cache, IConfiguration config, ILogger<QRRapidoService> logger, LogoReadabilityAnalyzer readabilityAnalyzer, IStringLocalizer<QRRapidoApp.Resources.SharedResource> localizer)
{
_cache = cache;
_config = config;
_logger = logger;
_readabilityAnalyzer = readabilityAnalyzer;
_localizer = localizer;
// Limit simultaneous generations to maintain performance
var maxConcurrent = _config.GetValue<int>("Performance:MaxConcurrentGenerations", 100);
@ -215,14 +218,14 @@ namespace QRRapidoApp.Services
if (logoBytes == null || logoBytes.Length == 0)
{
errorMessage = "Logo não fornecido";
errorMessage = _localizer["LogoNotProvided"];
return false;
}
// Validar tamanho máximo
if (logoBytes.Length > 2 * 1024 * 1024) // 2MB
{
errorMessage = "Logo muito grande. Máximo 2MB.";
errorMessage = _localizer["LogoTooLarge"];
return false;
}
@ -234,7 +237,7 @@ namespace QRRapidoApp.Services
// Validar dimensões mínimas
if (image.Width < 32 || image.Height < 32)
{
errorMessage = "Logo muito pequeno. Mínimo 32x32 pixels.";
errorMessage = _localizer["LogoTooSmall"];
return false;
}
@ -245,7 +248,7 @@ namespace QRRapidoApp.Services
}
catch (Exception ex)
{
errorMessage = "Formato de imagem inválido";
errorMessage = _localizer["InvalidImageFormat"];
_logger.LogError(ex, "Error validating logo format");
return false;
}

View File

@ -276,6 +276,36 @@ namespace QRRapidoApp.Services
}
}
public async Task<bool> DeleteQRFromHistoryAsync(string userId, string qrId)
{
try
{
// First verify that the QR code belongs to the user
var qrCode = await _context.QRCodeHistory
.Find(q => q.Id == qrId && q.UserId == userId && q.IsActive)
.FirstOrDefaultAsync();
if (qrCode == null)
{
_logger.LogWarning($"QR code not found or doesn't belong to user - QRId: {qrId}, UserId: {userId}");
return false;
}
// Soft delete: mark as inactive instead of permanently deleting
var update = Builders<QRCodeHistory>.Update.Set(q => q.IsActive, false);
var result = await _context.QRCodeHistory.UpdateOneAsync(
q => q.Id == qrId && q.UserId == userId,
update);
return result.ModifiedCount > 0;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error deleting QR from history - QRId: {qrId}, UserId: {userId}: {ex.Message}");
return false;
}
}
public async Task<int> GetQRCountThisMonthAsync(string userId)
{
try

View File

@ -16,6 +16,7 @@ namespace QRRapidoApp.Tests.Services
private readonly Mock<IConfiguration> _configMock;
private readonly Mock<ILogger<QRRapidoService>> _loggerMock;
private readonly Mock<LogoReadabilityAnalyzer> _readabilityAnalyzerMock;
private readonly Mock<IStringLocalizer<QRRapidoApp.Resources.SharedResource>> _localizerMock;
private readonly QRRapidoService _service;
public QRRapidoServiceTests()
@ -24,9 +25,10 @@ namespace QRRapidoApp.Tests.Services
_configMock = new Mock<IConfiguration>();
_loggerMock = new Mock<ILogger<QRRapidoService>>();
_readabilityAnalyzerMock = new Mock<LogoReadabilityAnalyzer>(Mock.Of<ILogger<LogoReadabilityAnalyzer>>(), Mock.Of<Microsoft.Extensions.Localization.IStringLocalizer<LogoReadabilityAnalyzer>>());
_localizerMock = new Mock<IStringLocalizer<QRRapidoApp.Resources.SharedResource>>();
SetupDefaultConfiguration();
_service = new QRRapidoService(_cacheMock.Object, _configMock.Object, _loggerMock.Object, _readabilityAnalyzerMock.Object);
_service = new QRRapidoService(_cacheMock.Object, _configMock.Object, _loggerMock.Object, _readabilityAnalyzerMock.Object, _localizerMock.Object);
}
private void SetupDefaultConfiguration()

View File

@ -3,7 +3,7 @@
@inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer
@{
ViewData["Title"] = "Histórico de QR Codes";
ViewData["Title"] = Localizer["HistoryTitle"];
Layout = "~/Views/Shared/_Layout.cshtml";
var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
}
@ -29,7 +29,15 @@
@foreach (var qr in Model)
{
<div class="col-12 col-md-6 col-lg-4 mb-4">
<div class="card h-100 shadow-sm">
<div class="card h-100 shadow-sm position-relative">
<!-- Delete button in top-right corner -->
<button type="button"
class="btn btn-sm btn-outline-danger position-absolute"
style="top: 8px; right: 8px; z-index: 10; padding: 4px 8px;"
onclick="confirmDeleteQR('@qr.Id')"
title="@Localizer["DeleteQRCode"]">
<i class="fas fa-trash fa-sm"></i>
</button>
<div class="card-body">
<div class="text-center mb-3">
<img src="data:image/png;base64,@qr.QRCodeBase64"
@ -119,8 +127,29 @@
</div>
</div>
<!-- Modal de Confirmação de Exclusão -->
<div class="modal fade" id="deleteConfirmModal" tabindex="-1" aria-labelledby="deleteConfirmModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteConfirmModalLabel">@Localizer["ConfirmDeleteTitle"]</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>@Localizer["ConfirmDeleteMessage"]</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">@Localizer["No"]</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">@Localizer["Yes"]</button>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
let qrToDelete = null;
function regenerateQR(qrId) {
// Get QR data from history and regenerate
fetch(`/api/QR/History`)
@ -147,6 +176,95 @@ function regenerateQR(qrId) {
});
}
function confirmDeleteQR(qrId) {
qrToDelete = qrId;
const modal = new bootstrap.Modal(document.getElementById('deleteConfirmModal'));
modal.show();
}
function deleteQR(qrId) {
// Show loading state
const confirmBtn = document.getElementById('confirmDeleteBtn');
const originalText = confirmBtn.textContent;
confirmBtn.disabled = true;
confirmBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> @Localizer["Deleting"]...';
fetch(`/api/QR/History/${qrId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Show success message
showToast(data.message || '@Localizer["QRCodeDeleted"]', 'success');
// Remove the card from the UI
const card = document.querySelector(`[onclick="confirmDeleteQR('${qrId}')"]`).closest('.col-12');
if (card) {
card.style.transition = 'opacity 0.3s ease';
card.style.opacity = '0';
setTimeout(() => {
card.remove();
// Check if no more cards exist
const remainingCards = document.querySelectorAll('.card');
if (remainingCards.length === 0) {
location.reload(); // Reload to show "no QR codes" message
}
}, 300);
}
// Hide modal
const modal = bootstrap.Modal.getInstance(document.getElementById('deleteConfirmModal'));
modal.hide();
} else {
showToast(data.message || '@Localizer["ErrorDeletingQR"]', 'error');
}
})
.catch(error => {
console.error('Error deleting QR:', error);
showToast('@Localizer["ErrorDeletingQR"]', 'error');
})
.finally(() => {
// Reset button state
confirmBtn.disabled = false;
confirmBtn.textContent = originalText;
});
}
function showToast(message, type = 'info') {
// Create toast element
const toast = document.createElement('div');
toast.className = `alert alert-${type === 'success' ? 'success' : 'danger'} alert-dismissible fade show position-fixed`;
toast.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
toast.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(toast);
// Auto remove after 5 seconds
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 5000);
}
// Event listeners
document.addEventListener('DOMContentLoaded', function() {
// Handle confirm delete button
document.getElementById('confirmDeleteBtn').addEventListener('click', function() {
if (qrToDelete) {
deleteQR(qrToDelete);
}
});
});
// Auto-refresh the page periodically to show new QR codes
setInterval(() => {
// Only refresh if user is still on this page and there are QR codes

View File

@ -1,6 +1,7 @@
@model QRRapidoApp.Models.User
@inject Microsoft.Extensions.Localization.IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer
@{
ViewData["Title"] = "Perfil do Usuário";
ViewData["Title"] = Localizer["UserProfileTitle"];
var isPremium = ViewBag.IsPremium as bool? ?? false;
var monthlyQRCount = ViewBag.MonthlyQRCount as int? ?? 0;
var qrHistory = ViewBag.QRHistory as List<QRRapidoApp.Models.QRCodeHistory> ?? new List<QRRapidoApp.Models.QRCodeHistory>();

View File

@ -11,7 +11,7 @@
<title>@ViewData["Title"] - QR Rapido | Gerador QR Code Ultrarrápido</title>
<!-- SEO Meta Tags -->
<meta name="description" content="QR Rapido: Gere códigos QR em segundos! Gerador ultrarrápido em português e espanhol. Grátis, sem cadastro obrigatório. 30 dias sem anúncios após login.">
<meta name="description" content="@Localizer["QRGenerateDescription"]">
<meta name="keywords" content="qr rapido, gerador qr rapido, qr code rapido, codigo qr rapido, qr gratis rapido, generador qr rapido, qr ultrarapido">
<meta name="author" content="QR Rapido">
<meta name="robots" content="index, follow">
@ -26,8 +26,8 @@
<link rel="alternate" hreflang="x-default" href="https://qrrapido.site/">
<!-- Open Graph -->
<meta property="og:title" content="QR Rapido - Gerador QR Code Ultrarrápido">
<meta property="og:description" content="Gere códigos QR em segundos! Grátis, rápido e fácil. 30 dias sem anúncios após login.">
<meta property="og:title" content="QR Rapido - @Localizer["FastestQRGeneratorWeb"]">
<meta property="og:description" content="@Localizer["QRGenerateDescription"]">
<meta property="og:image" content="https://qrrapido.site/images/qrrapido-og-image.png">
<meta property="og:url" content="@Context.Request.GetDisplayUrl()">
<meta property="og:type" content="website">
@ -36,8 +36,8 @@
<!-- Twitter Cards -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="QR Rapido - Gerador QR Code Ultrarrápido">
<meta name="twitter:description" content="Gere códigos QR em segundos! Grátis, rápido e fácil.">
<meta name="twitter:title" content="QR Rapido - @Localizer["FastestQRGeneratorWeb"]">
<meta name="twitter:description" content="@Localizer["QRGenerateDescription"]">
<meta name="twitter:image" content="https://qrrapido.site/images/qrrapido-twitter-card.png">
<!-- Structured Data Schema.org -->
@ -129,6 +129,26 @@
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/css/qrrapido-theme.css" asp-append-version="true" />
<!-- Translation variables for JavaScript -->
<script>
window.QRRapidoTranslations = {
shareError: '@Localizer["ShareError"]',
linkCopied: '@Localizer["LinkCopied"]',
enterQRContent: '@Localizer["EnterQRContent"]',
contentTooLong: '@Localizer["ContentTooLong"]',
featureNotAvailable: '@Localizer["FeatureNotAvailable"]',
vCardValidationError: '@Localizer["VCardValidationError"]',
logoTooLarge: '@Localizer["LogoTooLarge"]',
invalidLogoFormat: '@Localizer["InvalidLogoFormat"]',
premiumCornerStyleRequired: '@Localizer["PremiumCornerStyleRequired"]',
fastestGeneratorBrazil: '@Localizer["FastestGeneratorBrazil"]',
validationContentMinLength: '@Localizer["ValidationContentMinLength"]',
errorSavingHistory: '@Localizer["ErrorSavingHistory"]',
rateLimitReached: '@Localizer["RateLimitReached"]',
premiumLogoRequired: '@Localizer["PremiumLogoRequired"]'
};
</script>
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/images/qrrapido-favicon.svg">
<link rel="icon" type="image/png" href="/images/qrrapido-favicon-32x32.png">
@ -307,13 +327,13 @@
<script>
// Fallback inline para garantir funcionamento
document.addEventListener('DOMContentLoaded', function() {
console.log('🔧 Script inline iniciando como fallback...');
// Fallback inline script starting
const btn = document.getElementById('theme-toggle');
if (btn) {
console.log('✅ Botão encontrado pelo script inline');
// Theme toggle button found
btn.onclick = function() {
console.log('🖱️ Clique detectado (inline)');
// Theme toggle clicked
const html = document.documentElement;
const current = html.getAttribute('data-theme') || 'light';
const newTheme = current === 'light' ? 'dark' : 'light';
@ -329,7 +349,7 @@
text.textContent = newTheme === 'dark' ? 'Escuro' : 'Claro';
}
};
console.log('✅ Theme toggle configurado inline!');
// Theme toggle configured inline
} else {
console.error('❌ Botão não encontrado pelo script inline!');
}

View File

@ -249,7 +249,7 @@ class QRRapidoGenerator {
} catch (error) {
console.error('Error sharing:', error);
if (error.name !== 'AbortError') {
this.showError('Erro ao compartilhar. Tente outro método.');
this.showError(window.QRRapidoTranslations?.shareError || 'Error sharing. Try another method.');
}
}
}
@ -257,7 +257,7 @@ class QRRapidoGenerator {
shareWhatsApp() {
if (!this.currentQR) return;
const text = encodeURIComponent('QR Code gerado com QR Rapido - o gerador mais rápido do Brasil! ' + window.location.origin);
const text = encodeURIComponent((window.QRRapidoTranslations?.fastestGeneratorBrazil || 'QR Code generated with QR Rapido!') + ' ' + window.location.origin);
const url = `https://wa.me/?text=${text}`;
if (this.isMobileDevice()) {
@ -272,7 +272,7 @@ class QRRapidoGenerator {
shareTelegram() {
if (!this.currentQR) return;
const text = encodeURIComponent('QR Code gerado com QR Rapido - o gerador mais rápido do Brasil!');
const text = encodeURIComponent(window.QRRapidoTranslations?.fastestGeneratorBrazil || 'QR Code generated with QR Rapido!');
const url = encodeURIComponent(window.location.origin);
const telegramUrl = `https://t.me/share/url?url=${url}&text=${text}`;
@ -313,7 +313,7 @@ class QRRapidoGenerator {
textArea.remove();
}
this.showSuccess('Link copiado para a área de transferência!');
this.showSuccess(window.QRRapidoTranslations?.linkCopied || 'Link copied to clipboard!');
this.trackShareEvent('copy');
} catch (error) {
console.error('Error copying to clipboard:', error);
@ -358,7 +358,7 @@ class QRRapidoGenerator {
}
// Internal tracking
console.log(`QR Code shared via ${method}`);
// QR Code shared via ${method}
}
async generateQRWithTimer(e) {
@ -390,11 +390,7 @@ class QRRapidoGenerator {
};
}
console.log('🚀 Enviando requisição:', {
endpoint: requestData.endpoint,
isMultipart: requestData.isMultipart,
hasLogo: requestData.isMultipart
});
// Sending request to backend
const response = await fetch(requestData.endpoint, fetchOptions);
@ -402,12 +398,12 @@ class QRRapidoGenerator {
const errorData = await response.json().catch(() => ({}));
if (response.status === 429) {
this.showUpgradeModal('Limite de QR codes atingido! Upgrade para QR Rapido Premium e gere códigos ilimitados.');
this.showUpgradeModal(window.QRRapidoTranslations?.rateLimitReached || 'QR codes limit reached!');
return;
}
if (response.status === 400 && errorData.requiresPremium) {
this.showUpgradeModal(errorData.error || 'Logo personalizado é exclusivo do plano Premium.');
this.showUpgradeModal(errorData.error || window.QRRapidoTranslations?.premiumLogoRequired || 'Premium logo required.');
return;
}
@ -464,11 +460,11 @@ class QRRapidoGenerator {
}
return true;
} else {
this.showError('VCard generator não está disponível');
this.showError(window.QRRapidoTranslations?.featureNotAvailable || 'Feature not available');
return false;
}
} catch (error) {
this.showError('Erro na validação do VCard: ' + error.message);
this.showError((window.QRRapidoTranslations?.vCardValidationError || 'VCard validation error: ') + error.message);
return false;
}
} else if (qrType === 'wifi') {
@ -498,12 +494,12 @@ class QRRapidoGenerator {
const qrContent = document.getElementById('qr-content').value.trim();
if (!qrContent) {
this.showError('Digite o conteúdo do QR code');
this.showError(window.QRRapidoTranslations?.enterQRContent || 'Enter QR code content');
return false;
}
if (qrContent.length > 4000) {
this.showError('Conteúdo muito longo. Máximo 4000 caracteres.');
this.showError(window.QRRapidoTranslations?.contentTooLong || 'Content too long. Maximum 4000 characters.');
return false;
}
@ -938,7 +934,7 @@ class QRRapidoGenerator {
if (file) {
// Validate file size (2MB max)
if (file.size > 2 * 1024 * 1024) {
this.showError('Logo muito grande. Máximo 2MB.');
this.showError(window.QRRapidoTranslations?.logoTooLarge || 'Logo too large. Maximum 2MB.');
e.target.value = ''; // Clear the input
logoPreview?.classList.add('d-none');
return;
@ -947,7 +943,7 @@ class QRRapidoGenerator {
// Validate file type
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg'];
if (!allowedTypes.includes(file.type)) {
this.showError('Formato inválido. Use PNG ou JPG.');
this.showError(window.QRRapidoTranslations?.invalidLogoFormat || 'Invalid format. Use PNG or JPG.');
e.target.value = ''; // Clear the input
logoPreview?.classList.add('d-none');
return;
@ -1010,7 +1006,7 @@ class QRRapidoGenerator {
if (option.disabled) {
// Reset to square
e.target.value = 'square';
this.showUpgradeModal('Estilos de borda personalizados são exclusivos do plano Premium. Faça upgrade para usar esta funcionalidade.');
this.showUpgradeModal(window.QRRapidoTranslations?.premiumCornerStyleRequired || 'Custom corner styles are exclusive to Premium plan.');
return;
}
}
@ -1320,7 +1316,7 @@ class QRRapidoGenerator {
} catch (error) {
console.error('Save error:', error);
this.showError('Erro ao salvar no histórico.');
this.showError(window.QRRapidoTranslations?.errorSavingHistory || 'Error saving to history.');
}
}
@ -1645,7 +1641,7 @@ class QRRapidoGenerator {
// Feedback visual para campo de conteúdo
if (contentField) {
this.validateField(contentField, this.contentValid, 'Conteúdo deve ter pelo menos 3 caracteres');
this.validateField(contentField, this.contentValid, window.QRRapidoTranslations?.validationContentMinLength || 'Content must have at least 3 characters');
}
this.updateGenerateButton();

View File

@ -21,8 +21,7 @@
// Estado inicial
this.updateOpacity();
console.log('✅ Sistema simples de opacidade inicializado');
console.log('Controlando:', controls.length, 'controles e', targets.length, 'alvos');
// Simple opacity system initialized
}
updateOpacity() {
@ -33,11 +32,11 @@
if (hasSelection) {
// TEM seleção = div normal
target.classList.remove(this.disabledClass);
console.log('🟢 Div ativada - seleção detectada');
// Div activated - selection detected
} else {
// NÃO tem seleção = div opaca
target.classList.add(this.disabledClass);
console.log('🔴 Div desativada - nenhuma seleção');
// Div deactivated - no selection
}
});
}

View File

@ -1,19 +1,9 @@
console.log('🎯 Theme toggle script iniciando...');
// Aguardar DOM estar pronto
// Theme toggle functionality
document.addEventListener('DOMContentLoaded', function() {
console.log('🎯 DOM carregado, procurando elementos...');
const themeToggle = document.getElementById('theme-toggle');
const themeIcon = document.getElementById('theme-icon');
const themeText = document.getElementById('theme-text');
console.log('🎯 Elementos encontrados:', {
toggle: !!themeToggle,
icon: !!themeIcon,
text: !!themeText
});
if (!themeToggle) {
console.error('❌ Botão theme-toggle não encontrado!');
return;
@ -23,8 +13,6 @@ document.addEventListener('DOMContentLoaded', function() {
let currentTheme = 'light';
function applyTheme(theme) {
console.log('🎯 Aplicando tema:', theme);
const html = document.documentElement;
const body = document.body;
@ -39,8 +27,6 @@ document.addEventListener('DOMContentLoaded', function() {
if (themeText) {
themeText.textContent = 'Escuro';
}
console.log('🌙 Tema escuro aplicado');
} else {
// Modo claro
html.setAttribute('data-theme', 'light');
@ -52,14 +38,11 @@ document.addEventListener('DOMContentLoaded', function() {
if (themeText) {
themeText.textContent = 'Claro';
}
console.log('☀️ Tema claro aplicado');
}
// Salvar no localStorage
try {
localStorage.setItem('qr-rapido-theme', theme);
console.log('💾 Tema salvo no localStorage:', theme);
} catch (e) {
console.warn('⚠️ Não foi possível salvar no localStorage:', e);
}
@ -69,7 +52,6 @@ document.addEventListener('DOMContentLoaded', function() {
// Função de toggle
function toggleTheme() {
console.log('🔄 Toggle theme clicado. Tema atual:', currentTheme);
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
applyTheme(newTheme);
}
@ -77,14 +59,11 @@ document.addEventListener('DOMContentLoaded', function() {
// Event listener
themeToggle.addEventListener('click', function(e) {
e.preventDefault();
console.log('🖱️ Clique detectado no theme toggle');
toggleTheme();
});
// Aplicar tema inicial (sempre claro por enquanto)
applyTheme('light');
console.log('✅ Theme toggle configurado com sucesso!');
});
// Função global para debug