diff --git a/wwwroot/css/site.css b/wwwroot/css/site.css
index e27372c..18f6949 100644
--- a/wwwroot/css/site.css
+++ b/wwwroot/css/site.css
@@ -18,6 +18,24 @@ body {
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
+/* URL Validation Feedback */
+#url-validation-error {
+ font-size: 0.875rem;
+ margin-top: 0.5rem;
+ padding: 0.5rem;
+ border-radius: 0.25rem;
+}
+
+.form-control.is-invalid {
+ border-color: #dc3545;
+ box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
+}
+
+.form-control.is-valid {
+ border-color: #28a745;
+ box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
+}
+
/* Share Button Styles */
#share-qr-btn {
position: relative;
@@ -123,4 +141,45 @@ body {
.share-success.show {
transform: translateX(0);
-}
\ No newline at end of file
+}
+
+.premium-feature-box {
+ border: 2px solid #ffc107;
+ border-radius: 8px;
+ padding: 15px;
+ margin: 15px 0;
+ background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
+}
+
+.premium-feature-box h6 {
+ color: #856404;
+ margin-bottom: 5px;
+}
+
+.form-check-input:checked {
+ background-color: #ffc107;
+ border-color: #ffc107;
+}
+
+.premium-upgrade-prompt {
+ text-align: center;
+ padding: 10px;
+}
+
+.premium-upgrade-prompt a {
+ text-decoration: none;
+ font-weight: bold;
+}
+
+.premium-upgrade-prompt a:hover {
+ text-decoration: underline;
+}
+
+#url-preview-content {
+ font-family: monospace;
+ border-left: 4px solid #007bff;
+}
+
+.text-success {
+ color: #28a745 !important;
+}
diff --git a/wwwroot/js/qr-speed-generator.js b/wwwroot/js/qr-speed-generator.js
index a47de8c..558e88b 100644
--- a/wwwroot/js/qr-speed-generator.js
+++ b/wwwroot/js/qr-speed-generator.js
@@ -479,6 +479,30 @@ class QRRapidoGenerator {
const type = document.getElementById('qr-type').value;
const quickStyle = document.querySelector('input[name="quick-style"]:checked')?.value || 'classic';
const styleSettings = this.getStyleSettings(quickStyle);
+
+ if (type === 'url') {
+ const dynamicData = window.dynamicQRManager.getDynamicQRData();
+ if (dynamicData.requiresPremium) {
+ throw new Error('QR Dinâmico é exclusivo para usuários Premium. Faça upgrade para usar analytics.');
+ }
+ return {
+ data: {
+ type: 'url',
+ content: dynamicData.originalUrl,
+ isDynamic: dynamicData.isDynamic,
+ quickStyle: quickStyle,
+ primaryColor: document.getElementById('primary-color').value || (styleSettings.primaryColor || '#000000'),
+ backgroundColor: document.getElementById('bg-color').value || (styleSettings.backgroundColor || '#FFFFFF'),
+ size: parseInt(document.getElementById('qr-size').value),
+ margin: parseInt(document.getElementById('qr-margin').value),
+ cornerStyle: document.getElementById('corner-style')?.value || 'square',
+ optimizeForSpeed: true,
+ language: this.currentLang
+ },
+ isMultipart: false,
+ endpoint: '/api/QR/GenerateRapid'
+ };
+ }
if (type === 'wifi') {
const wifiContent = window.wifiGenerator.generateWiFiString();
@@ -970,11 +994,15 @@ class QRRapidoGenerator {
const timeFloat = parseFloat(generationTime);
// Update average time display in the stats cards
- const avgElement = document.querySelector('.card-body h5:contains("1.2s")');
- if (avgElement) {
- const avgTime = window.qrRapidoStats ? window.qrRapidoStats.getAverageTime() : generationTime;
- avgElement.innerHTML = ` ${avgTime}s`;
- }
+ // Find h5 elements in card bodies and look for one containing time info
+ const cardElements = document.querySelectorAll('.card-body h5');
+ cardElements.forEach(element => {
+ // Look for elements containing time format (e.g., "1.2s", "0.8s", etc.)
+ if (element.textContent.includes('s') && element.textContent.match(/\d+\.\d+s/)) {
+ const avgTime = window.qrRapidoStats ? window.qrRapidoStats.getAverageTime() : generationTime;
+ element.innerHTML = ` ${avgTime}s`;
+ }
+ });
// Update the generation timer in the header
const timerElement = document.querySelector('.generation-timer span');
@@ -1244,10 +1272,8 @@ class QRRapidoGenerator {
const qrContent = document.getElementById('qr-content');
const vcardInterface = document.getElementById('vcard-interface');
- // Inicialmente desabilitar campo de conteúdo
- if (qrContent) {
- qrContent.disabled = true;
- }
+ // FIXED: Don't disable content field initially - let enableContentFields handle it
+ // The field should be available for typing once a type is selected
// Ocultar interface vCard
if (vcardInterface) {
@@ -1255,6 +1281,45 @@ class QRRapidoGenerator {
}
this.updateGenerateButton();
+
+ // Setup URL validation event listeners
+ this.setupURLValidationListeners();
+ }
+
+ setupURLValidationListeners() {
+ const contentField = document.getElementById('qr-content');
+ if (!contentField) return;
+
+ // Auto-fix URL on blur (when user leaves the field)
+ contentField.addEventListener('blur', () => {
+ const type = document.getElementById('qr-type')?.value;
+
+ if (type === 'url' && contentField.value.trim()) {
+ const originalValue = contentField.value.trim();
+ const fixedValue = this.autoFixURL(originalValue);
+
+ if (originalValue !== fixedValue) {
+ contentField.value = fixedValue;
+ console.log('🔧 URL auto-corrigida:', originalValue, '→', fixedValue);
+
+ // Revalidar após correção
+ this.updateGenerateButton();
+ }
+ }
+ });
+
+ // Real-time validation with debounce
+ contentField.addEventListener('input', () => {
+ const type = document.getElementById('qr-type')?.value;
+
+ if (type === 'url') {
+ // Debounce para não validar a cada caractere
+ clearTimeout(contentField.validationTimeout);
+ contentField.validationTimeout = setTimeout(() => {
+ this.updateGenerateButton();
+ }, 500);
+ }
+ });
}
handleTypeSelection(type) {
@@ -1294,12 +1359,16 @@ class QRRapidoGenerator {
const wifiInterface = document.getElementById('wifi-interface');
const smsInterface = document.getElementById('sms-interface');
const emailInterface = document.getElementById('email-interface');
+ const dynamicQRSection = document.getElementById('dynamic-qr-section');
+ const urlPreview = document.getElementById('url-preview');
// Hide all interfaces by default
if (vcardInterface) vcardInterface.style.display = 'none';
if (wifiInterface) wifiInterface.style.display = 'none';
if (smsInterface) smsInterface.style.display = 'none';
if (emailInterface) emailInterface.style.display = 'none';
+ if (dynamicQRSection) dynamicQRSection.style.display = 'none';
+ if (urlPreview) urlPreview.style.display = 'none';
if (contentGroup) contentGroup.style.display = 'block';
if (type === 'vcard') {
@@ -1327,6 +1396,14 @@ class QRRapidoGenerator {
if (emailInterface) {
emailInterface.style.display = 'block';
}
+ } else if (type === 'url') {
+ if (dynamicQRSection) dynamicQRSection.style.display = 'block';
+ if (urlPreview) urlPreview.style.display = 'block';
+ // CRITICAL FIX: Enable content field for URL type
+ const qrContent = document.getElementById('qr-content');
+ if(qrContent) {
+ qrContent.disabled = false;
+ }
} else {
// Para outros tipos, mostrar textarea
if (contentGroup) contentGroup.style.display = 'block';
@@ -1354,7 +1431,7 @@ class QRRapidoGenerator {
const vcardInterface = document.getElementById('vcard-interface');
if (qrContent) {
- qrContent.disabled = true;
+ // FIXED: Don't disable content field, just clear it
qrContent.value = '';
}
@@ -1424,6 +1501,71 @@ class QRRapidoGenerator {
}
}
+ // ============================================
+ // URL VALIDATION FUNCTIONS
+ // ============================================
+
+ isValidURL(url) {
+ if (!url || typeof url !== 'string') return false;
+
+ const trimmedUrl = url.trim();
+
+ // Verificar se começa com http:// ou https://
+ const hasProtocol = trimmedUrl.startsWith('http://') || trimmedUrl.startsWith('https://');
+
+ // Verificar se tem pelo menos um ponto
+ const hasDot = trimmedUrl.includes('.');
+
+ // Verificar se não é só o protocolo
+ const hasContent = trimmedUrl.length > 8; // mais que "https://"
+
+ // Regex mais robusta
+ const urlRegex = /^https?:\/\/.+\..+/i;
+
+ return hasProtocol && hasDot && hasContent && urlRegex.test(trimmedUrl);
+ }
+
+ autoFixURL(url) {
+ if (!url || typeof url !== 'string') return '';
+
+ let fixedUrl = url.trim();
+
+ // Se não tem protocolo, adicionar https://
+ if (!fixedUrl.startsWith('http://') && !fixedUrl.startsWith('https://')) {
+ fixedUrl = 'https://' + fixedUrl;
+ }
+
+ return fixedUrl;
+ }
+
+ showValidationError(message) {
+ let errorDiv = document.getElementById('url-validation-error');
+
+ if (!errorDiv) {
+ // Criar div de erro se não existir
+ errorDiv = document.createElement('div');
+ errorDiv.id = 'url-validation-error';
+ errorDiv.className = 'alert alert-danger mt-2';
+ errorDiv.style.display = 'none';
+
+ // Inserir após o campo de conteúdo
+ const contentGroup = document.getElementById('content-group');
+ if (contentGroup) {
+ contentGroup.appendChild(errorDiv);
+ }
+ }
+
+ errorDiv.innerHTML = ` ${message}`;
+ errorDiv.style.display = 'block';
+ }
+
+ clearValidationError() {
+ const errorDiv = document.getElementById('url-validation-error');
+ if (errorDiv) {
+ errorDiv.style.display = 'none';
+ }
+ }
+
updateGenerateButton() {
const generateBtn = document.getElementById('generate-btn');
if (!generateBtn) return;
@@ -1431,9 +1573,41 @@ class QRRapidoGenerator {
let isValid = false;
const type = this.selectedType;
- if (type === 'url' || type === 'text') {
- const content = document.getElementById('qr-content')?.value || '';
+ if (type === 'url') {
+ const contentField = document.getElementById('qr-content');
+ const content = contentField?.value || '';
+ const trimmedContent = content.trim();
+
+ if (!trimmedContent) {
+ this.showValidationError('URL é obrigatória');
+ if (contentField) {
+ contentField.classList.remove('is-valid');
+ contentField.classList.add('is-invalid');
+ }
+ isValid = false;
+ } else if (!this.isValidURL(trimmedContent)) {
+ this.showValidationError('URL deve começar com http:// ou https:// e conter pelo menos um ponto (ex: https://google.com)');
+ if (contentField) {
+ contentField.classList.remove('is-valid');
+ contentField.classList.add('is-invalid');
+ }
+ isValid = false;
+ } else {
+ this.clearValidationError();
+ if (contentField) {
+ contentField.classList.remove('is-invalid');
+ contentField.classList.add('is-valid');
+ }
+ isValid = true;
+ }
+ } else if (type === 'text') {
+ const contentField = document.getElementById('qr-content');
+ const content = contentField?.value || '';
isValid = content.trim().length >= 3;
+ this.clearValidationError(); // Clear any URL validation errors
+ if (contentField) {
+ contentField.classList.remove('is-valid', 'is-invalid');
+ }
} else if (type === 'vcard') {
isValid = this.validateVCardFields();
} else if (type === 'wifi') {
@@ -1555,6 +1729,7 @@ document.addEventListener('DOMContentLoaded', () => {
window.wifiGenerator = new WiFiQRGenerator();
window.smsGenerator = new SMSQRGenerator();
window.emailGenerator = new EmailQRGenerator();
+ window.dynamicQRManager = new DynamicQRManager();
// Initialize AdSense if necessary
if (window.adsbygoogle && document.querySelector('.adsbygoogle')) {
@@ -1562,6 +1737,115 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
+class DynamicQRManager {
+ constructor() {
+ this.initializeDynamicQR();
+ }
+
+ initializeDynamicQR() {
+ // Verificar se usuário é premium
+ this.isPremium = this.checkUserPremium();
+
+ // Configurar interface baseado no status
+ this.setupPremiumInterface();
+
+ // Event listeners
+ this.setupEventListeners();
+
+ // Preview inicial
+ this.updatePreview();
+ }
+
+
+ checkUserPremium() {
+ // IMPLEMENTAR: verificar se usuário atual é premium
+ // Pode ser via elemento hidden, variável global, ou chamada AJAX
+
+ // Exemplo 1: Via elemento hidden
+ const premiumStatus = document.getElementById('user-premium-status');
+ if (premiumStatus) {
+ return premiumStatus.value === 'true';
+ }
+
+ // Exemplo 2: Via variável global
+ if (window.userInfo && window.userInfo.isPremium !== undefined) {
+ return window.userInfo.isPremium;
+ }
+
+ // Fallback: assumir não-premium
+ return false;
+ }
+
+ setupPremiumInterface() {
+ const toggleContainer = document.getElementById('dynamic-toggle-container');
+ const upgradePrompt = document.querySelector('.premium-upgrade-prompt');
+
+ if (this.isPremium) {
+ // Usuário premium: mostrar toggle funcional
+ if(toggleContainer) toggleContainer.style.display = 'block';
+ if (upgradePrompt) upgradePrompt.style.display = 'none';
+ } else {
+ // Usuário não-premium: mostrar prompt de upgrade
+ if(toggleContainer) toggleContainer.style.display = 'none';
+ if (upgradePrompt) upgradePrompt.style.display = 'block';
+ }
+ }
+
+ setupEventListeners() {
+ // Toggle do QR Dinâmico
+ const dynamicToggle = document.getElementById('qr-dynamic-toggle');
+ if (dynamicToggle) {
+ dynamicToggle.addEventListener('change', () => {
+ this.updatePreview();
+ });
+ }
+
+ // Campo URL
+ const urlField = document.getElementById('qr-content');
+ if (urlField) {
+ urlField.addEventListener('input', () => {
+ this.updatePreview();
+ });
+ }
+ }
+
+ updatePreview() {
+ const urlField = document.getElementById('qr-content');
+ const dynamicToggle = document.getElementById('qr-dynamic-toggle');
+ const finalUrlDisplay = document.getElementById('final-url-display');
+ const typeDisplay = document.getElementById('qr-type-display');
+
+ if (!urlField || !finalUrlDisplay || !typeDisplay) return;
+
+ const originalUrl = urlField.value;
+ const isDynamic = dynamicToggle && dynamicToggle.checked && this.isPremium;
+
+ if (isDynamic && originalUrl) {
+ // QR Dinâmico: mostra URL do QRRapido
+ finalUrlDisplay.textContent = `https://qrrapido.site/r/{ID_UNICO}`;
+ typeDisplay.textContent = "QR Code Dinâmico com Analytics ";
+ typeDisplay.className = "text-success fw-bold";
+ } else {
+ // QR Estático: mostra URL original
+ finalUrlDisplay.textContent = originalUrl || 'Digite uma URL...';
+ typeDisplay.textContent = "QR Code Estático";
+ typeDisplay.className = "text-muted";
+ }
+ }
+
+ // Método para integração com collectFormData()
+ getDynamicQRData() {
+ const dynamicToggle = document.getElementById('qr-dynamic-toggle');
+ const isDynamic = dynamicToggle && dynamicToggle.checked && this.isPremium;
+
+ return {
+ isDynamic: isDynamic,
+ originalUrl: document.getElementById('qr-content').value,
+ requiresPremium: !this.isPremium && isDynamic
+ };
+ }
+}
+
class SMSQRGenerator {
constructor() {
this.initializeSMSInterface();