fix: ajustes de javascript e funcionamento
Some checks failed
Deploy QR Rapido / test (push) Successful in 4m11s
Deploy QR Rapido / build-and-push (push) Failing after 5m52s
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-09-22 14:54:43 -03:00
parent b4b87f42c1
commit 1b74de34e6
11 changed files with 277 additions and 80 deletions

1
.gitignore vendored
View File

@ -381,3 +381,4 @@ temp/
*.pfx *.pfx
*.crt *.crt
*.key *.key
wwwroot/dist/

19
AGENTS.md Normal file
View File

@ -0,0 +1,19 @@
# Repository Guidelines
## Project Structure & Module Organization
Core MVC code lives in `Controllers`, `Services`, `Models`, and `Middleware`, while Razor views sit in `Views` and static assets in `wwwroot`. Data access and caching helpers are under `Data` and `Providers`. Localized resources (PT-BR, ES, EN) reside in `Resources`. Tests target service logic in `Tests/Services` via `QRRapidoApp.Tests.csproj`. Runtime settings use the `appsettings.*.json` files, and Docker assets, including `docker-compose.yml`, remain at the repository root.
## Build, Test, and Development Commands
Install dependencies with `dotnet restore`, then run `dotnet build` for a release-ready compile. Use `dotnet run` to launch the Kestrel server locally or `dotnet watch run` for hot reload during UI work. Execute `dotnet test` from the root to run xUnit tests; add `--collect:"XPlat Code Coverage"` when you need coverage reports. For full-stack parity, start infrastructure with `docker-compose up -d` and follow component logs through `docker-compose logs -f qrrapido`.
## Coding Style & Naming Conventions
Stick to standard C# styling: four-space indentation, PascalCase for types and public members, camelCase for locals, and `I` prefixes for interfaces. Keep services focused; prefer small, testable classes instead of partials. Place shared copy in `Resources` and surface it via `IStringLocalizer`. Run `dotnet format` before committing to normalize imports, analyzer fixes, and whitespace.
## Testing Guidelines
Keep unit and integration tests close to the subject, e.g., `Tests/Services/QRRapidoServiceTests.cs`. Name classes `<Subject>Tests` and methods as scenario sentences such as `GenerateRapidAsync_WithValidRequest_ReturnsSuccess`. Use Moq for external dependencies and configure defaults in constructor helpers. Every new behavior should gain at least one happy-path and one guard test, and maintain quick, deterministic fixtures.
## Commit & Pull Request Guidelines
Follow the Conventional Commit style observed here (`fix:`, `feat:`, `chore:`) with concise Portuguese summaries for user-facing updates. Group related changes per commit to simplify reversions. Pull requests should outline the context, enumerate key changes, attach `dotnet test` output or coverage deltas, and include UI screenshots or GIFs when behavior shifts. Reference Azure or GitHub issue IDs and call out configuration or migration steps prominently.
## Security & Configuration Tips
Never commit secrets or the contents of `keys/`. Document new configuration values in the PR description and supply safe defaults in `appsettings.Development.json`. Protect added endpoints with existing rate-limiting and authorization middleware, and rotate published test keys after demos.

View File

@ -25,7 +25,6 @@ using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting; using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using WebOptimizer;
// Fix for WSL path issues - disable StaticWebAssets completely // Fix for WSL path issues - disable StaticWebAssets completely
var options = new WebApplicationOptions var options = new WebApplicationOptions
@ -96,22 +95,6 @@ Log.Logger = loggerConfig.CreateLogger();
builder.Host.UseSerilog(); builder.Host.UseSerilog();
// Add services to the container // Add services to the container
builder.Services.AddWebOptimizer(pipelines =>
{
pipelines.AddCssBundle(
"/css/app.min.css",
"css/site.css",
"css/qrrapido-theme.css");
pipelines.AddJavaScriptBundle(
"/js/app.min.js",
"js/test.js",
"js/simple-opcacity.js",
"js/qr-speed-generator.js",
"js/language-switcher.js",
"js/theme-toggle.js",
"js/cookie-consent.js");
});
builder.Services.AddControllersWithViews() builder.Services.AddControllersWithViews()
.AddViewLocalization(Microsoft.AspNetCore.Mvc.Razor.LanguageViewLocationExpanderFormat.Suffix) .AddViewLocalization(Microsoft.AspNetCore.Mvc.Razor.LanguageViewLocationExpanderFormat.Suffix)
@ -316,8 +299,6 @@ if (!app.Environment.IsDevelopment())
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseWebOptimizer();
app.UseStaticFiles(); app.UseStaticFiles();
// Language redirection middleware (before routing) // Language redirection middleware (before routing)

View File

@ -9,7 +9,6 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="LigerShark.WebOptimizer.Core" Version="3.0.477" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="8.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="8.0.0" />
<PackageReference Include="MongoDB.Driver" Version="2.22.0" /> <PackageReference Include="MongoDB.Driver" Version="2.22.0" />
@ -66,4 +65,10 @@
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
<Target Name="BuildFrontend" BeforeTargets="Build" Condition="'$(Configuration)'=='Release'">
<Message Importance="High" Text="Executando build do frontend com Vite..." />
<Exec Command="npm install" WorkingDirectory="$(MSBuildProjectDirectory)" Condition="!Exists('node_modules')" />
<Exec Command="npm run build" WorkingDirectory="$(MSBuildProjectDirectory)" />
</Target>
</Project> </Project>

View File

@ -1284,8 +1284,7 @@
@await Html.PartialAsync("_AdSpace", new { position = "footer" }) @await Html.PartialAsync("_AdSpace", new { position = "footer" })
<script src="~/js/simple-opcacity.js"></script> @section Scripts {
<!-- Script para controles de logo aprimorados --> <!-- Script para controles de logo aprimorados -->
<script> <script>
// JavaScript para controles de logo aprimorados // JavaScript para controles de logo aprimorados
@ -1432,6 +1431,7 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
</script> </script>
}
<style> <style>
/* Toast positioning improvements */ /* Toast positioning improvements */

View File

@ -3,6 +3,11 @@
@using Microsoft.Extensions.Localization @using Microsoft.Extensions.Localization
@inject AdDisplayService AdService @inject AdDisplayService AdService
@inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer @inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer
@inject Microsoft.AspNetCore.Hosting.IWebHostEnvironment HostEnvironment
@{
var isDevelopment = HostEnvironment?.IsDevelopment() ?? false;
}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="pt-BR"> <html lang="pt-BR">
<head> <head>
@ -49,8 +54,8 @@
<link rel="preload" href="/webfonts/fa-solid-900.woff2" as="font" type="font/woff2" crossorigin> <link rel="preload" href="/webfonts/fa-solid-900.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/webfonts/fa-brands-400.woff2" as="font" type="font/woff2" crossorigin> <link rel="preload" href="/webfonts/fa-brands-400.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" as="style"> <link rel="preload" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" as="style">
<link rel="preload" href="~/css/app.min.css" as="style"> <link rel="preload" href="~/css/site.css" as="style">
<link rel="preload" href="~/js/app.min.js" as="script"> <link rel="preload" href="~/css/qrrapido-theme.css" as="style">
<!-- Structured Data Schema.org --> <!-- Structured Data Schema.org -->
<script type="application/ld+json"> <script type="application/ld+json">
@ -149,7 +154,8 @@
<link rel="stylesheet" href="~/css/vendor/fontawesome.min.css" asp-append-version="true" media="print" onload="this.media='all'" /> <link rel="stylesheet" href="~/css/vendor/fontawesome.min.css" asp-append-version="true" media="print" onload="this.media='all'" />
<!-- Custom CSS - Critical above fold with cache busting --> <!-- Custom CSS - Critical above fold with cache busting -->
<link rel="stylesheet" href="~/css/app.min.css?v=@DateTime.Now.Ticks" /> <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 --> <!-- Translation variables for JavaScript -->
<script> <script>
@ -369,11 +375,20 @@
<!-- Bootstrap 5 JS --> <!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Custom JS with cache busting --> @if (isDevelopment)
<script src="~/js/app.min.js?v=@DateTime.Now.Ticks" defer></script> {
<script src="~/js/simple-opcacity.js" asp-append-version="true" defer></script>
<!-- Performance optimizations moved to external file --> <script src="~/js/test.js" asp-append-version="true" defer></script>
<script src="~/js/performance-optimizations.js?v=@DateTime.Now.Ticks" defer></script> <script src="~/js/qr-speed-generator.js" asp-append-version="true" defer></script>
<script src="~/js/language-switcher.js" asp-append-version="true" defer></script>
<script src="~/js/theme-toggle.js" asp-append-version="true" defer></script>
<script src="~/js/cookie-consent.js" asp-append-version="true" defer></script>
<script src="~/js/performance-optimizations.js" asp-append-version="true" defer></script>
}
else
{
<script src="~/dist/app.min.js" asp-append-version="true"></script>
}
@await RenderSectionAsync("Scripts", required: false) @await RenderSectionAsync("Scripts", required: false)
</body> </body>

13
package.json Normal file
View File

@ -0,0 +1,13 @@
{
"name": "qrrapido-app",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vite --config vite.config.js",
"build": "vite build --config vite.config.js",
"preview": "vite preview --config vite.config.js"
},
"devDependencies": {
"vite": "^5.4.0"
}
}

21
vite.config.js Normal file
View File

@ -0,0 +1,21 @@
import { defineConfig } from 'vite';
export default defineConfig(({ mode }) => ({
root: '.',
publicDir: false,
build: {
target: 'es2020',
outDir: 'wwwroot/dist',
emptyOutDir: true,
assetsDir: '.',
sourcemap: true,
rollupOptions: {
input: 'wwwroot/js/app.entry.js',
output: {
format: 'iife',
entryFileNames: 'app.min.js',
assetFileNames: '[name][extname]'
}
}
}
}));

7
wwwroot/js/app.entry.js Normal file
View File

@ -0,0 +1,7 @@
import './simple-opcacity.js';
import './test.js';
import './qr-speed-generator.js';
import './language-switcher.js';
import './theme-toggle.js';
import './cookie-consent.js';
import './performance-optimizations.js';

View File

@ -1,4 +1,4 @@
// QR Rapido Speed Generator // QR Rapido Speed Generator
class QRRapidoGenerator { class QRRapidoGenerator {
constructor() { constructor() {
this.startTime = 0; this.startTime = 0;
@ -42,6 +42,7 @@ class QRRapidoGenerator {
this.currentLang = localStorage.getItem('qrrapido-lang') || 'pt-BR'; this.currentLang = localStorage.getItem('qrrapido-lang') || 'pt-BR';
this.selectedType = null; this.selectedType = null;
this.previousType = null;
this.selectedStyle = 'classic'; // Estilo padrão this.selectedStyle = 'classic'; // Estilo padrão
this.contentValid = false; this.contentValid = false;
@ -49,6 +50,7 @@ class QRRapidoGenerator {
this.contentDelayTimer = null; this.contentDelayTimer = null;
this.hasShownContentToast = false; this.hasShownContentToast = false;
this.buttonReadyState = false; this.buttonReadyState = false;
this.urlPrefix = 'https://';
this.initializeEvents(); this.initializeEvents();
this.checkAdFreeStatus(); this.checkAdFreeStatus();
@ -132,9 +134,6 @@ class QRRapidoGenerator {
this.updateGenerateButton(); this.updateGenerateButton();
}, 200); // Additional URL validation delay }, 200); // Additional URL validation delay
} }
// Trigger UX improvements with delay
this.handleContentInputWithDelay(e.target.value);
}, 300); }, 300);
}); });
} }
@ -198,6 +197,146 @@ class QRRapidoGenerator {
this.generateQRWithTimer(e); this.generateQRWithTimer(e);
}); });
} }
this.setupUrlFieldHandlers();
}
setupUrlFieldHandlers() {
const contentField = document.getElementById('qr-content');
if (!contentField) return;
contentField.addEventListener('focus', () => {
if (this.selectedType === 'url') {
if (!contentField.value.trim()) {
contentField.value = this.urlPrefix;
} else {
contentField.value = this.ensureUrlPrefix(contentField.value);
}
if (this.isProtocolOnly(contentField.value.trim())) {
this.clearValidationError();
contentField.classList.remove('is-valid', 'is-invalid');
this.contentValid = false;
this.updateGenerateButton();
this.setCaretPosition(contentField, this.getProtocolLength(contentField.value));
}
}
});
contentField.addEventListener('paste', (event) => {
if (this.selectedType !== 'url') return;
event.preventDefault();
const text = (event.clipboardData || window.clipboardData)?.getData('text') || '';
const normalized = this.normalizeUrlInput(text);
contentField.value = normalized;
this.setCaretPosition(contentField, normalized.length);
this.handleContentChange(normalized);
this.updateGenerateButton();
});
contentField.addEventListener('input', () => {
if (this.selectedType !== 'url') return;
const currentValue = contentField.value;
const sanitized = this.ensureUrlPrefix(currentValue);
if (sanitized !== currentValue) {
const caret = typeof contentField.selectionStart === 'number' ? contentField.selectionStart : sanitized.length;
const delta = sanitized.length - currentValue.length;
contentField.value = sanitized;
const protocolLength = this.getProtocolLength(sanitized);
const newPos = Math.max(protocolLength, caret + delta);
this.setCaretPosition(contentField, newPos);
}
if (this.isProtocolOnly(contentField.value.trim())) {
this.clearValidationError();
contentField.classList.remove('is-valid', 'is-invalid');
}
});
}
prefillContentField(type, previousType = null) {
const contentField = document.getElementById('qr-content');
if (!contentField) return;
if (type === 'url') {
const returningToUrl = previousType === 'url';
const hasValue = !!contentField.value.trim();
if (!returningToUrl || !hasValue || this.isProtocolOnly(contentField.value.trim())) {
contentField.value = this.urlPrefix;
} else {
contentField.value = this.ensureUrlPrefix(contentField.value);
}
contentField.classList.remove('is-valid', 'is-invalid');
this.clearValidationError();
this.contentValid = false;
} else {
contentField.value = '';
contentField.classList.remove('is-valid', 'is-invalid');
this.clearValidationError();
this.contentValid = false;
}
this.updateGenerateButton();
}
isProtocolOnly(value) {
if (!value) return true;
const normalized = value.toString().trim().toLowerCase();
return normalized === 'https://' || normalized === 'http://';
}
normalizeUrlInput(raw) {
if (!raw) {
return this.urlPrefix;
}
let value = raw.toString().trim();
if (!value) {
return this.urlPrefix;
}
const protocolMatch = value.match(/^(https?:\/\/)/i);
let protocol = this.urlPrefix;
if (protocolMatch) {
protocol = protocolMatch[0].toLowerCase();
value = value.slice(protocolMatch[0].length);
}
value = value.replace(/^(https?:\/\/)/i, '');
return protocol + value;
}
ensureUrlPrefix(value) {
if (!value) {
return this.urlPrefix;
}
let working = value.toString().trimStart();
const protocolMatch = working.match(/^(https?:\/\/)/i);
let protocol = this.urlPrefix;
if (protocolMatch) {
protocol = protocolMatch[0].toLowerCase();
working = working.slice(protocolMatch[0].length);
}
working = working.replace(/^(https?:\/\/)/i, '');
return protocol + working;
}
getProtocolLength(value) {
if (!value) return this.urlPrefix.length;
if (value.startsWith('http://')) return 'http://'.length;
if (value.startsWith('https://')) return 'https://'.length;
return this.urlPrefix.length;
}
setCaretPosition(input, position) {
if (!input || typeof input.setSelectionRange !== 'function') return;
const pos = Math.max(0, Math.min(position, input.value.length));
requestAnimationFrame(() => {
input.setSelectionRange(pos, pos);
});
} }
setupDownloadButtons() { setupDownloadButtons() {
@ -1597,6 +1736,7 @@ class QRRapidoGenerator {
} }
handleTypeSelection(type) { handleTypeSelection(type) {
const previousType = this.selectedType;
this.selectedType = type; this.selectedType = type;
// Reset UX improvements flags for new type selection // Reset UX improvements flags for new type selection
@ -1610,6 +1750,7 @@ class QRRapidoGenerator {
this.removeInitialHighlight(); this.removeInitialHighlight();
// Sempre habilitar campos de conteúdo após selecionar tipo // Sempre habilitar campos de conteúdo após selecionar tipo
this.enableContentFields(type); this.enableContentFields(type);
this.prefillContentField(type, previousType);
// Show guidance toast for the selected type // Show guidance toast for the selected type
this.showTypeGuidanceToast(type); this.showTypeGuidanceToast(type);
} else { } else {
@ -1617,6 +1758,7 @@ class QRRapidoGenerator {
} }
this.updateGenerateButton(); this.updateGenerateButton();
this.previousType = type;
} }
handleStyleSelection(style) { handleStyleSelection(style) {
@ -1626,50 +1768,40 @@ class QRRapidoGenerator {
handleContentChange(content) { handleContentChange(content) {
const contentField = document.getElementById('qr-content'); const contentField = document.getElementById('qr-content');
const trimmedContent = typeof content === 'string' ? content.trim() : '';
if (this.selectedType === 'url' && this.isProtocolOnly(trimmedContent)) {
this.contentValid = false;
} else {
this.contentValid = this.validateContent(content); this.contentValid = this.validateContent(content);
}
// Feedback visual para campo de conteúdo // Feedback visual para campo de conteúdo
if (contentField) { if (contentField) {
if (this.selectedType === 'url') {
contentField.classList.remove('is-valid');
if (!trimmedContent || this.isProtocolOnly(trimmedContent)) {
contentField.classList.remove('is-invalid');
}
} else {
this.validateField(contentField, this.contentValid, window.QRRapidoTranslations?.validationContentMinLength || 'Content must have at least 3 characters'); this.validateField(contentField, this.contentValid, window.QRRapidoTranslations?.validationContentMinLength || 'Content must have at least 3 characters');
} }
}
this.updateGenerateButton(); this.updateGenerateButton();
} }
// UX Improvements - Intelligent delay system // UX Improvements - Intelligent delay system
handleContentInputWithDelay(content) { handleContentInputWithDelay(content) {
// Clear existing timer // UX delay desabilitado para evitar correções automáticas tardias
if (this.contentDelayTimer) { if (this.contentDelayTimer) {
clearTimeout(this.contentDelayTimer); clearTimeout(this.contentDelayTimer);
} }
// Only proceed with delay logic if we have valid content and selected type
if (this.selectedType && this.validateContent(content) && !this.hasShownContentToast) {
this.contentDelayTimer = setTimeout(() => {
this.triggerContentReadyUX();
}, 7000); // 7 seconds delay after the initial 300ms debounce
}
} }
triggerContentReadyUX() { triggerContentReadyUX() {
if (this.selectedType && this.contentValid && !this.hasShownContentToast) { // Função mantida por compatibilidade, sem efeitos colaterais
// Mark as shown to prevent multiple toasts return;
this.hasShownContentToast = true;
const contentField = document.getElementById('qr-content');
const originalValue = contentField.value.trim();
const fixedValue = this.autoFixURL(originalValue);
if (originalValue !== fixedValue) {
contentField.value = fixedValue;
this.updateGenerateButton();
}
// Show educational toast
this.showContentAddedToast();
// Update button state with ready indicator
this.updateGenerateButtonToReady();
}
} }
showContentAddedToast() { showContentAddedToast() {
@ -1995,12 +2127,12 @@ class QRRapidoGenerator {
const contentField = document.getElementById('qr-content'); const contentField = document.getElementById('qr-content');
const content = contentField?.value || ''; const content = contentField?.value || '';
const trimmedContent = content.trim(); const trimmedContent = content.trim();
const protocolOnly = this.isProtocolOnly(trimmedContent);
if (!trimmedContent) { if (!trimmedContent || protocolOnly) {
this.showValidationError('URL é obrigatória'); this.clearValidationError();
if (contentField) { if (contentField) {
contentField.classList.remove('is-valid'); contentField.classList.remove('is-valid', 'is-invalid');
contentField.classList.add('is-invalid');
} }
isValid = false; isValid = false;
} else if (!this.isValidURL(trimmedContent)) { } else if (!this.isValidURL(trimmedContent)) {

View File

@ -1,4 +1,4 @@
class SimpleOpacityController { class SimpleOpacityController {
constructor(controlSelector, targetSelector) { constructor(controlSelector, targetSelector) {
this.controlSelector = controlSelector; this.controlSelector = controlSelector;
this.targetSelector = targetSelector; this.targetSelector = targetSelector;
@ -61,3 +61,6 @@
return false; return false;
} }
} }
if (typeof window !== 'undefined') {
window.SimpleOpacityController = SimpleOpacityController;
}