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 QRRapidoApp.Services;
using System.Diagnostics; using System.Diagnostics;
using System.Security.Claims; using System.Security.Claims;
using Microsoft.Extensions.Localization;
namespace QRRapidoApp.Controllers namespace QRRapidoApp.Controllers
{ {
@ -12,13 +13,15 @@ namespace QRRapidoApp.Controllers
private readonly AdDisplayService _adDisplayService; private readonly AdDisplayService _adDisplayService;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IConfiguration _config; 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; _logger = logger;
_adDisplayService = adDisplayService; _adDisplayService = adDisplayService;
_userService = userService; _userService = userService;
_config = config; _config = config;
_localizer = localizer;
} }
public async Task<IActionResult> Index() public async Task<IActionResult> Index()
@ -33,7 +36,7 @@ namespace QRRapidoApp.Controllers
// SEO and Analytics data // SEO and Analytics data
ViewBag.Title = _config["App:TaglinePT"]; ViewBag.Title = _config["App:TaglinePT"];
ViewBag.Keywords = _config["SEO:KeywordsPT"]; 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 // User stats for logged in users
if (!string.IsNullOrEmpty(userId)) if (!string.IsNullOrEmpty(userId))

View File

@ -4,6 +4,7 @@ using QRRapidoApp.Services;
using System.Diagnostics; using System.Diagnostics;
using System.Security.Claims; using System.Security.Claims;
using System.Text; using System.Text;
using Microsoft.Extensions.Localization;
namespace QRRapidoApp.Controllers namespace QRRapidoApp.Controllers
{ {
@ -15,13 +16,15 @@ namespace QRRapidoApp.Controllers
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly AdDisplayService _adService; private readonly AdDisplayService _adService;
private readonly ILogger<QRController> _logger; 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; _qrService = qrService;
_userService = userService; _userService = userService;
_adService = adService; _adService = adService;
_logger = logger; _logger = logger;
_localizer = localizer;
} }
[HttpPost("GenerateRapid")] [HttpPost("GenerateRapid")]
@ -51,13 +54,13 @@ namespace QRRapidoApp.Controllers
if (string.IsNullOrWhiteSpace(request.Content)) if (string.IsNullOrWhiteSpace(request.Content))
{ {
_logger.LogWarning("QR generation failed - empty content provided"); _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 if (request.Content.Length > 4000) // Limit to maintain speed
{ {
_logger.LogWarning("QR generation failed - content too long: {ContentLength} characters", request.Content.Length); _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 // Check user status
@ -70,7 +73,7 @@ namespace QRRapidoApp.Controllers
userId ?? "anonymous", request.CornerStyle); userId ?? "anonymous", request.CornerStyle);
return BadRequest(new 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, requiresPremium = true,
success = false success = false
}); });
@ -84,7 +87,7 @@ namespace QRRapidoApp.Controllers
userId ?? "anonymous", user?.IsPremium ?? false); userId ?? "anonymous", user?.IsPremium ?? false);
return StatusCode(429, new return StatusCode(429, new
{ {
error = "Limite de QR codes atingido", error = _localizer["RateLimitReached"],
upgradeUrl = "/Premium/Upgrade", upgradeUrl = "/Premium/Upgrade",
success = false success = false
}); });
@ -325,13 +328,13 @@ namespace QRRapidoApp.Controllers
if (string.IsNullOrWhiteSpace(request.Content)) if (string.IsNullOrWhiteSpace(request.Content))
{ {
_logger.LogWarning("QR generation failed - empty content provided"); _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) if (request.Content.Length > 4000)
{ {
_logger.LogWarning("QR generation failed - content too long: {ContentLength} characters", request.Content.Length); _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 // Check user status
@ -343,7 +346,7 @@ namespace QRRapidoApp.Controllers
_logger.LogWarning("Logo upload attempted by non-premium user - UserId: {UserId}", userId ?? "anonymous"); _logger.LogWarning("Logo upload attempted by non-premium user - UserId: {UserId}", userId ?? "anonymous");
return BadRequest(new return BadRequest(new
{ {
error = "Logo personalizado é exclusivo do plano Premium. Faça upgrade para usar esta funcionalidade.", error = _localizer["PremiumLogoRequired"],
requiresPremium = true, requiresPremium = true,
success = false success = false
}); });
@ -363,7 +366,7 @@ namespace QRRapidoApp.Controllers
if (logo.Length > 2 * 1024 * 1024) if (logo.Length > 2 * 1024 * 1024)
{ {
_logger.LogWarning("Logo upload failed - file too large: {FileSize} bytes", logo.Length); _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 // Validate file format
@ -371,7 +374,7 @@ namespace QRRapidoApp.Controllers
if (!allowedTypes.Contains(logo.ContentType?.ToLower())) if (!allowedTypes.Contains(logo.ContentType?.ToLower()))
{ {
_logger.LogWarning("Logo upload failed - invalid format: {ContentType}", logo.ContentType); _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 try
@ -388,7 +391,7 @@ namespace QRRapidoApp.Controllers
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error processing logo file"); _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); userId ?? "anonymous", user?.IsPremium ?? false);
return StatusCode(429, new return StatusCode(429, new
{ {
error = "Limite de QR codes atingido", error = _localizer["RateLimitReached"],
upgradeUrl = "/Premium/Upgrade", upgradeUrl = "/Premium/Upgrade",
success = false 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) private async Task<bool> CheckRateLimitAsync(string? userId, Models.User? user)
{ {
// Premium users have unlimited QR codes // Premium users have unlimited QR codes

View File

@ -157,8 +157,7 @@ builder.Services.Configure<RequestLocalizationOptions>(options =>
var supportedCultures = new[] var supportedCultures = new[]
{ {
new CultureInfo("pt-BR"), new CultureInfo("pt-BR"),
new CultureInfo("es"), new CultureInfo("es-PY")
new CultureInfo("en")
}; };
options.DefaultRequestCulture = new RequestCulture("pt-BR", "pt-BR"); options.DefaultRequestCulture = new RequestCulture("pt-BR", "pt-BR");
@ -267,7 +266,7 @@ app.MapHealthChecks("/health");
// Language routes (must be first) // Language routes (must be first)
app.MapControllerRoute( app.MapControllerRoute(
name: "localized", 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 // API routes
app.MapControllerRoute( 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"> <data name="AnonymousUserLimit" xml:space="preserve">
<value>Usuários anônimos: 3 QR codes por dia</value> <value>Usuários anônimos: 3 QR codes por dia</value>
</data> </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> </root>

View File

@ -336,4 +336,94 @@
<data name="OpenNetwork" xml:space="preserve"> <data name="OpenNetwork" xml:space="preserve">
<value>Sem senha </value> <value>Sem senha </value>
</data> </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> </root>

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
@inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer @inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer
@{ @{
ViewData["Title"] = "Histórico de QR Codes"; ViewData["Title"] = Localizer["HistoryTitle"];
Layout = "~/Views/Shared/_Layout.cshtml"; Layout = "~/Views/Shared/_Layout.cshtml";
var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
} }
@ -29,7 +29,15 @@
@foreach (var qr in Model) @foreach (var qr in Model)
{ {
<div class="col-12 col-md-6 col-lg-4 mb-4"> <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="card-body">
<div class="text-center mb-3"> <div class="text-center mb-3">
<img src="data:image/png;base64,@qr.QRCodeBase64" <img src="data:image/png;base64,@qr.QRCodeBase64"
@ -119,8 +127,29 @@
</div> </div>
</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 { @section Scripts {
<script> <script>
let qrToDelete = null;
function regenerateQR(qrId) { function regenerateQR(qrId) {
// Get QR data from history and regenerate // Get QR data from history and regenerate
fetch(`/api/QR/History`) 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 // Auto-refresh the page periodically to show new QR codes
setInterval(() => { setInterval(() => {
// Only refresh if user is still on this page and there are QR codes // Only refresh if user is still on this page and there are QR codes

View File

@ -1,6 +1,7 @@
@model QRRapidoApp.Models.User @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 isPremium = ViewBag.IsPremium as bool? ?? false;
var monthlyQRCount = ViewBag.MonthlyQRCount as int? ?? 0; var monthlyQRCount = ViewBag.MonthlyQRCount as int? ?? 0;
var qrHistory = ViewBag.QRHistory as List<QRRapidoApp.Models.QRCodeHistory> ?? new List<QRRapidoApp.Models.QRCodeHistory>(); 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> <title>@ViewData["Title"] - QR Rapido | Gerador QR Code Ultrarrápido</title>
<!-- SEO Meta Tags --> <!-- 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="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="author" content="QR Rapido">
<meta name="robots" content="index, follow"> <meta name="robots" content="index, follow">
@ -26,8 +26,8 @@
<link rel="alternate" hreflang="x-default" href="https://qrrapido.site/"> <link rel="alternate" hreflang="x-default" href="https://qrrapido.site/">
<!-- Open Graph --> <!-- Open Graph -->
<meta property="og:title" content="QR Rapido - Gerador QR Code Ultrarrápido"> <meta property="og:title" content="QR Rapido - @Localizer["FastestQRGeneratorWeb"]">
<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:description" content="@Localizer["QRGenerateDescription"]">
<meta property="og:image" content="https://qrrapido.site/images/qrrapido-og-image.png"> <meta property="og:image" content="https://qrrapido.site/images/qrrapido-og-image.png">
<meta property="og:url" content="@Context.Request.GetDisplayUrl()"> <meta property="og:url" content="@Context.Request.GetDisplayUrl()">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
@ -36,8 +36,8 @@
<!-- Twitter Cards --> <!-- Twitter Cards -->
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="QR Rapido - Gerador QR Code Ultrarrápido"> <meta name="twitter:title" content="QR Rapido - @Localizer["FastestQRGeneratorWeb"]">
<meta name="twitter:description" content="Gere códigos QR em segundos! Grátis, rápido e fácil."> <meta name="twitter:description" content="@Localizer["QRGenerateDescription"]">
<meta name="twitter:image" content="https://qrrapido.site/images/qrrapido-twitter-card.png"> <meta name="twitter:image" content="https://qrrapido.site/images/qrrapido-twitter-card.png">
<!-- Structured Data Schema.org --> <!-- Structured Data Schema.org -->
@ -129,6 +129,26 @@
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/css/qrrapido-theme.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 --> <!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/images/qrrapido-favicon.svg"> <link rel="icon" type="image/svg+xml" href="/images/qrrapido-favicon.svg">
<link rel="icon" type="image/png" href="/images/qrrapido-favicon-32x32.png"> <link rel="icon" type="image/png" href="/images/qrrapido-favicon-32x32.png">
@ -307,13 +327,13 @@
<script> <script>
// Fallback inline para garantir funcionamento // Fallback inline para garantir funcionamento
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
console.log('🔧 Script inline iniciando como fallback...'); // Fallback inline script starting
const btn = document.getElementById('theme-toggle'); const btn = document.getElementById('theme-toggle');
if (btn) { if (btn) {
console.log('✅ Botão encontrado pelo script inline'); // Theme toggle button found
btn.onclick = function() { btn.onclick = function() {
console.log('🖱️ Clique detectado (inline)'); // Theme toggle clicked
const html = document.documentElement; const html = document.documentElement;
const current = html.getAttribute('data-theme') || 'light'; const current = html.getAttribute('data-theme') || 'light';
const newTheme = current === 'light' ? 'dark' : 'light'; const newTheme = current === 'light' ? 'dark' : 'light';
@ -329,7 +349,7 @@
text.textContent = newTheme === 'dark' ? 'Escuro' : 'Claro'; text.textContent = newTheme === 'dark' ? 'Escuro' : 'Claro';
} }
}; };
console.log('✅ Theme toggle configurado inline!'); // Theme toggle configured inline
} else { } else {
console.error('❌ Botão não encontrado pelo script inline!'); console.error('❌ Botão não encontrado pelo script inline!');
} }

View File

@ -249,7 +249,7 @@ class QRRapidoGenerator {
} catch (error) { } catch (error) {
console.error('Error sharing:', error); console.error('Error sharing:', error);
if (error.name !== 'AbortError') { 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() { shareWhatsApp() {
if (!this.currentQR) return; 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}`; const url = `https://wa.me/?text=${text}`;
if (this.isMobileDevice()) { if (this.isMobileDevice()) {
@ -272,7 +272,7 @@ class QRRapidoGenerator {
shareTelegram() { shareTelegram() {
if (!this.currentQR) return; 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 url = encodeURIComponent(window.location.origin);
const telegramUrl = `https://t.me/share/url?url=${url}&text=${text}`; const telegramUrl = `https://t.me/share/url?url=${url}&text=${text}`;
@ -313,7 +313,7 @@ class QRRapidoGenerator {
textArea.remove(); textArea.remove();
} }
this.showSuccess('Link copiado para a área de transferência!'); this.showSuccess(window.QRRapidoTranslations?.linkCopied || 'Link copied to clipboard!');
this.trackShareEvent('copy'); this.trackShareEvent('copy');
} catch (error) { } catch (error) {
console.error('Error copying to clipboard:', error); console.error('Error copying to clipboard:', error);
@ -358,7 +358,7 @@ class QRRapidoGenerator {
} }
// Internal tracking // Internal tracking
console.log(`QR Code shared via ${method}`); // QR Code shared via ${method}
} }
async generateQRWithTimer(e) { async generateQRWithTimer(e) {
@ -390,11 +390,7 @@ class QRRapidoGenerator {
}; };
} }
console.log('🚀 Enviando requisição:', { // Sending request to backend
endpoint: requestData.endpoint,
isMultipart: requestData.isMultipart,
hasLogo: requestData.isMultipart
});
const response = await fetch(requestData.endpoint, fetchOptions); const response = await fetch(requestData.endpoint, fetchOptions);
@ -402,12 +398,12 @@ class QRRapidoGenerator {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
if (response.status === 429) { 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; return;
} }
if (response.status === 400 && errorData.requiresPremium) { 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; return;
} }
@ -464,11 +460,11 @@ class QRRapidoGenerator {
} }
return true; return true;
} else { } else {
this.showError('VCard generator não está disponível'); this.showError(window.QRRapidoTranslations?.featureNotAvailable || 'Feature not available');
return false; return false;
} }
} catch (error) { } catch (error) {
this.showError('Erro na validação do VCard: ' + error.message); this.showError((window.QRRapidoTranslations?.vCardValidationError || 'VCard validation error: ') + error.message);
return false; return false;
} }
} else if (qrType === 'wifi') { } else if (qrType === 'wifi') {
@ -498,12 +494,12 @@ class QRRapidoGenerator {
const qrContent = document.getElementById('qr-content').value.trim(); const qrContent = document.getElementById('qr-content').value.trim();
if (!qrContent) { if (!qrContent) {
this.showError('Digite o conteúdo do QR code'); this.showError(window.QRRapidoTranslations?.enterQRContent || 'Enter QR code content');
return false; return false;
} }
if (qrContent.length > 4000) { 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; return false;
} }
@ -938,7 +934,7 @@ class QRRapidoGenerator {
if (file) { if (file) {
// Validate file size (2MB max) // Validate file size (2MB max)
if (file.size > 2 * 1024 * 1024) { 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 e.target.value = ''; // Clear the input
logoPreview?.classList.add('d-none'); logoPreview?.classList.add('d-none');
return; return;
@ -947,7 +943,7 @@ class QRRapidoGenerator {
// Validate file type // Validate file type
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg']; const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg'];
if (!allowedTypes.includes(file.type)) { 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 e.target.value = ''; // Clear the input
logoPreview?.classList.add('d-none'); logoPreview?.classList.add('d-none');
return; return;
@ -1010,7 +1006,7 @@ class QRRapidoGenerator {
if (option.disabled) { if (option.disabled) {
// Reset to square // Reset to square
e.target.value = '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; return;
} }
} }
@ -1320,7 +1316,7 @@ class QRRapidoGenerator {
} catch (error) { } catch (error) {
console.error('Save error:', 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 // Feedback visual para campo de conteúdo
if (contentField) { 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(); this.updateGenerateButton();

View File

@ -21,8 +21,7 @@
// Estado inicial // Estado inicial
this.updateOpacity(); this.updateOpacity();
console.log('✅ Sistema simples de opacidade inicializado'); // Simple opacity system initialized
console.log('Controlando:', controls.length, 'controles e', targets.length, 'alvos');
} }
updateOpacity() { updateOpacity() {
@ -33,11 +32,11 @@
if (hasSelection) { if (hasSelection) {
// TEM seleção = div normal // TEM seleção = div normal
target.classList.remove(this.disabledClass); target.classList.remove(this.disabledClass);
console.log('🟢 Div ativada - seleção detectada'); // Div activated - selection detected
} else { } else {
// NÃO tem seleção = div opaca // NÃO tem seleção = div opaca
target.classList.add(this.disabledClass); 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...'); // Theme toggle functionality
// Aguardar DOM estar pronto
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
console.log('🎯 DOM carregado, procurando elementos...');
const themeToggle = document.getElementById('theme-toggle'); const themeToggle = document.getElementById('theme-toggle');
const themeIcon = document.getElementById('theme-icon'); const themeIcon = document.getElementById('theme-icon');
const themeText = document.getElementById('theme-text'); const themeText = document.getElementById('theme-text');
console.log('🎯 Elementos encontrados:', {
toggle: !!themeToggle,
icon: !!themeIcon,
text: !!themeText
});
if (!themeToggle) { if (!themeToggle) {
console.error('❌ Botão theme-toggle não encontrado!'); console.error('❌ Botão theme-toggle não encontrado!');
return; return;
@ -23,8 +13,6 @@ document.addEventListener('DOMContentLoaded', function() {
let currentTheme = 'light'; let currentTheme = 'light';
function applyTheme(theme) { function applyTheme(theme) {
console.log('🎯 Aplicando tema:', theme);
const html = document.documentElement; const html = document.documentElement;
const body = document.body; const body = document.body;
@ -39,8 +27,6 @@ document.addEventListener('DOMContentLoaded', function() {
if (themeText) { if (themeText) {
themeText.textContent = 'Escuro'; themeText.textContent = 'Escuro';
} }
console.log('🌙 Tema escuro aplicado');
} else { } else {
// Modo claro // Modo claro
html.setAttribute('data-theme', 'light'); html.setAttribute('data-theme', 'light');
@ -52,14 +38,11 @@ document.addEventListener('DOMContentLoaded', function() {
if (themeText) { if (themeText) {
themeText.textContent = 'Claro'; themeText.textContent = 'Claro';
} }
console.log('☀️ Tema claro aplicado');
} }
// Salvar no localStorage // Salvar no localStorage
try { try {
localStorage.setItem('qr-rapido-theme', theme); localStorage.setItem('qr-rapido-theme', theme);
console.log('💾 Tema salvo no localStorage:', theme);
} catch (e) { } catch (e) {
console.warn('⚠️ Não foi possível salvar no localStorage:', e); console.warn('⚠️ Não foi possível salvar no localStorage:', e);
} }
@ -69,7 +52,6 @@ document.addEventListener('DOMContentLoaded', function() {
// Função de toggle // Função de toggle
function toggleTheme() { function toggleTheme() {
console.log('🔄 Toggle theme clicado. Tema atual:', currentTheme);
const newTheme = currentTheme === 'light' ? 'dark' : 'light'; const newTheme = currentTheme === 'light' ? 'dark' : 'light';
applyTheme(newTheme); applyTheme(newTheme);
} }
@ -77,14 +59,11 @@ document.addEventListener('DOMContentLoaded', function() {
// Event listener // Event listener
themeToggle.addEventListener('click', function(e) { themeToggle.addEventListener('click', function(e) {
e.preventDefault(); e.preventDefault();
console.log('🖱️ Clique detectado no theme toggle');
toggleTheme(); toggleTheme();
}); });
// Aplicar tema inicial (sempre claro por enquanto) // Aplicar tema inicial (sempre claro por enquanto)
applyTheme('light'); applyTheme('light');
console.log('✅ Theme toggle configurado com sucesso!');
}); });
// Função global para debug // Função global para debug