Compare commits
2 Commits
00d924ce3b
...
9634176e18
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9634176e18 | ||
|
|
2a623d1fd5 |
@ -7,7 +7,9 @@
|
||||
"Bash(timeout:*)",
|
||||
"Bash(rm:*)",
|
||||
"Bash(dotnet run:*)",
|
||||
"Bash(curl:*)"
|
||||
"Bash(curl:*)",
|
||||
"Bash(pkill:*)",
|
||||
"Bash(true)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
||||
@ -108,15 +108,157 @@
|
||||
<i class="fas fa-edit"></i> @Localizer["Content"]
|
||||
</label>
|
||||
<textarea id="qr-content"
|
||||
class="form-control form-control-lg"
|
||||
class="form-control form-control-lg required-field"
|
||||
rows="3"
|
||||
placeholder="@Localizer["EnterQRCodeContent"]"
|
||||
required></textarea>
|
||||
<div class="invalid-feedback">Conteúdo deve ter pelo menos 3 caracteres</div>
|
||||
<div class="form-text">
|
||||
<span id="content-hints">@Localizer["ContentHints"]</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VCard Interface (dynamic) -->
|
||||
<div id="vcard-interface" class="mb-3" style="display: none;">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-address-card"></i>
|
||||
<strong>Cartão de Visita Digital</strong> - Este QR Code criará um cartão de visita digital.
|
||||
Quando escaneado, oferecerá para salvar seus contatos automaticamente.
|
||||
</div>
|
||||
|
||||
<!-- Campos Obrigatórios -->
|
||||
<div class="required-fields mb-4">
|
||||
<h6 class="fw-bold text-primary mb-3">
|
||||
<i class="fas fa-user-check"></i> Informações Essenciais
|
||||
</h6>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label fw-semibold">Nome Completo *</label>
|
||||
<input type="text" id="vcard-name" class="form-control required-field" placeholder="Seu Nome Completo" required>
|
||||
<div class="invalid-feedback">Nome é obrigatório</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Telefone Celular *</label>
|
||||
<input type="tel" id="vcard-mobile" class="form-control required-field" placeholder="11999998888" required>
|
||||
<small class="form-text text-muted">Apenas números (DDD + número)</small>
|
||||
<div class="invalid-feedback">Telefone celular é obrigatório</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold">Email *</label>
|
||||
<input type="email" id="vcard-email" class="form-control required-field" placeholder="seu.email@exemplo.com" required>
|
||||
<div class="invalid-feedback">Email válido é obrigatório</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campos Opcionais -->
|
||||
<div class="optional-fields mb-4">
|
||||
<h6 class="fw-bold text-secondary mb-3">
|
||||
<i class="fas fa-plus-circle"></i> Informações Adicionais (Opcionais)
|
||||
</h6>
|
||||
|
||||
<!-- Empresa -->
|
||||
<div class="mb-3">
|
||||
<div class="form-check mb-2">
|
||||
<input type="checkbox" id="enable-company" class="form-check-input">
|
||||
<label for="enable-company" class="form-check-label fw-semibold">
|
||||
<i class="fas fa-building"></i> Empresa
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group" id="company-group" style="display: none;">
|
||||
<input type="text" id="vcard-company" class="form-control" placeholder="Nome da Empresa">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cargo -->
|
||||
<div class="mb-3">
|
||||
<div class="form-check mb-2">
|
||||
<input type="checkbox" id="enable-title" class="form-check-input">
|
||||
<label for="enable-title" class="form-check-label fw-semibold">
|
||||
<i class="fas fa-id-badge"></i> Cargo/Título
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group" id="title-group" style="display: none;">
|
||||
<input type="text" id="vcard-title" class="form-control" placeholder="CEO, Gerente, Desenvolvedor, etc.">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Website -->
|
||||
<div class="mb-3">
|
||||
<div class="form-check mb-2">
|
||||
<input type="checkbox" id="enable-website" class="form-check-input">
|
||||
<label for="enable-website" class="form-check-label fw-semibold">
|
||||
<i class="fas fa-globe"></i> Website
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group" id="website-group" style="display: none;">
|
||||
<input type="url" id="vcard-website" class="form-control" placeholder="https://seusite.com">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Telefone Fixo -->
|
||||
<div class="mb-3">
|
||||
<div class="form-check mb-2">
|
||||
<input type="checkbox" id="enable-phone" class="form-check-input">
|
||||
<label for="enable-phone" class="form-check-label fw-semibold">
|
||||
<i class="fas fa-phone"></i> Telefone Fixo
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group" id="phone-group" style="display: none;">
|
||||
<input type="tel" id="vcard-phone" class="form-control" placeholder="1133334444">
|
||||
<small class="form-text text-muted">Apenas números (DDD + número)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Endereço -->
|
||||
<div class="mb-3">
|
||||
<div class="form-check mb-2">
|
||||
<input type="checkbox" id="enable-address" class="form-check-input">
|
||||
<label for="enable-address" class="form-check-label fw-semibold">
|
||||
<i class="fas fa-map-marker-alt"></i> Endereço
|
||||
</label>
|
||||
</div>
|
||||
<div id="address-group" style="display: none;">
|
||||
<div class="form-group mb-2">
|
||||
<input type="text" id="vcard-address" class="form-control" placeholder="Rua, número - Ex: Rua das Flores, 123">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<input type="text" id="vcard-city" class="form-control" placeholder="Cidade">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="text" id="vcard-state" class="form-control" placeholder="Estado">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="text" id="vcard-zip" class="form-control" placeholder="CEP">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview do VCard -->
|
||||
<div class="vcard-preview mb-3">
|
||||
<h6 class="fw-bold text-success mb-2">
|
||||
<i class="fas fa-eye"></i> Preview do Cartão
|
||||
</h6>
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<pre id="vcard-preview-text" class="mb-0 small text-muted">BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
CHARSET=UTF-8
|
||||
Preencha os campos acima para ver o preview...
|
||||
END:VCARD</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced customization (collapsible) -->
|
||||
<div class="accordion mb-3" id="customization-accordion">
|
||||
<div class="accordion-item">
|
||||
|
||||
@ -1047,4 +1047,79 @@ html[data-theme="light"] {
|
||||
footer a:hover {
|
||||
color: #0056b3 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================ */
|
||||
/* SISTEMA DE FLUXO ASCENDENTE PROGRESSIVO */
|
||||
/* ============================================ */
|
||||
|
||||
/* Classe para destacar campo inicial */
|
||||
.qr-field-highlight {
|
||||
border: 2px solid #3B82F6 !important;
|
||||
box-shadow: 0 0 5px rgba(59, 130, 246, 0.3) !important;
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
/* Feedback visual para campos obrigatórios */
|
||||
.required-field:invalid,
|
||||
.form-control:invalid {
|
||||
border-color: #dc3545 !important;
|
||||
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25) !important;
|
||||
}
|
||||
|
||||
.required-field:valid,
|
||||
.form-control:valid {
|
||||
border-color: #28a745 !important;
|
||||
box-shadow: 0 0 0 0.25rem rgba(40, 167, 69, 0.25) !important;
|
||||
}
|
||||
|
||||
/* Estados de campos desabilitados */
|
||||
.disabled-section {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Transições suaves para seções */
|
||||
.qr-section {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Tooltip para campos bloqueados */
|
||||
.field-blocked-hint {
|
||||
color: #f59e0b;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.field-blocked-hint.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-5px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Tema escuro - ajustes para sistema de fluxo */
|
||||
html[data-theme="dark"] .qr-field-highlight {
|
||||
border-color: #60A5FA !important;
|
||||
box-shadow: 0 0 5px rgba(96, 165, 250, 0.3) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .required-field:invalid,
|
||||
html[data-theme="dark"] .form-control:invalid {
|
||||
border-color: #ef4444 !important;
|
||||
box-shadow: 0 0 0 0.25rem rgba(239, 68, 68, 0.25) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .required-field:valid,
|
||||
html[data-theme="dark"] .form-control:valid {
|
||||
border-color: #22c55e !important;
|
||||
box-shadow: 0 0 0 0.25rem rgba(34, 197, 94, 0.25) !important;
|
||||
}
|
||||
@ -41,10 +41,21 @@ class QRRapidoGenerator {
|
||||
};
|
||||
this.currentLang = localStorage.getItem('qrrapido-lang') || 'pt-BR';
|
||||
|
||||
this.selectedType = null;
|
||||
this.selectedStyle = 'classic'; // Estilo padrão
|
||||
this.contentValid = false;
|
||||
|
||||
this.initializeEvents();
|
||||
this.checkAdFreeStatus();
|
||||
this.updateLanguage();
|
||||
this.updateStatsCounters();
|
||||
this.initializeProgressiveFlow();
|
||||
|
||||
// Validar segurança dos dados após carregamento
|
||||
setTimeout(() => {
|
||||
this.validateDataSecurity();
|
||||
this.testUTF8Encoding();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
initializeEvents() {
|
||||
@ -71,12 +82,41 @@ class QRRapidoGenerator {
|
||||
cornerStyle.addEventListener('change', this.handleCornerStyleChange.bind(this));
|
||||
}
|
||||
|
||||
// QR type change with hints
|
||||
// QR type change with hints and flow
|
||||
const qrType = document.getElementById('qr-type');
|
||||
if (qrType) {
|
||||
qrType.addEventListener('change', this.updateContentHints.bind(this));
|
||||
qrType.addEventListener('change', (e) => {
|
||||
this.handleTypeSelection(e.target.value);
|
||||
this.updateContentHints();
|
||||
});
|
||||
}
|
||||
|
||||
// Content validation
|
||||
const qrContent = document.getElementById('qr-content');
|
||||
if (qrContent) {
|
||||
qrContent.addEventListener('input', (e) => {
|
||||
this.handleContentChange(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
// Style selection
|
||||
document.querySelectorAll('input[name="quick-style"]').forEach(radio => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
this.handleStyleSelection(e.target.value);
|
||||
});
|
||||
});
|
||||
|
||||
// VCard fields validation
|
||||
const vcardFields = ['vcard-name', 'vcard-mobile', 'vcard-email'];
|
||||
vcardFields.forEach(fieldId => {
|
||||
const field = document.getElementById(fieldId);
|
||||
if (field) {
|
||||
field.addEventListener('input', () => {
|
||||
this.updateGenerateButton();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Language selector
|
||||
document.querySelectorAll('[data-lang]').forEach(link => {
|
||||
link.addEventListener('click', this.changeLanguage.bind(this));
|
||||
@ -367,13 +407,35 @@ class QRRapidoGenerator {
|
||||
|
||||
validateForm() {
|
||||
const qrType = document.getElementById('qr-type').value;
|
||||
const qrContent = document.getElementById('qr-content').value.trim();
|
||||
|
||||
if (!qrType) {
|
||||
this.showError('Selecione o tipo de QR code');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Special validation for VCard
|
||||
if (qrType === 'vcard') {
|
||||
try {
|
||||
if (window.vcardGenerator) {
|
||||
const errors = window.vcardGenerator.validateVCardData();
|
||||
if (errors.length > 0) {
|
||||
this.showError(errors.join('<br>'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
this.showError('VCard generator não está disponível');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError('Erro na validação do VCard: ' + error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Normal validation for other types
|
||||
const qrContent = document.getElementById('qr-content').value.trim();
|
||||
|
||||
if (!qrContent) {
|
||||
this.showError('Digite o conteúdo do QR code');
|
||||
return false;
|
||||
@ -388,9 +450,36 @@ class QRRapidoGenerator {
|
||||
}
|
||||
|
||||
collectFormData() {
|
||||
const type = document.getElementById('qr-type').value;
|
||||
const quickStyle = document.querySelector('input[name="quick-style"]:checked')?.value || 'classic';
|
||||
const styleSettings = this.getStyleSettings(quickStyle);
|
||||
|
||||
// Handle VCard type
|
||||
if (type === 'vcard') {
|
||||
if (window.vcardGenerator) {
|
||||
const vcardContent = window.vcardGenerator.getVCardContent();
|
||||
const encodedContent = this.prepareContentForQR(vcardContent, 'vcard');
|
||||
return {
|
||||
data: {
|
||||
type: 'vcard', // Keep as vcard type for tracking
|
||||
content: encodedContent,
|
||||
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'
|
||||
};
|
||||
} else {
|
||||
throw new Error('VCard generator não está disponível');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if logo is selected for premium users
|
||||
const logoUpload = document.getElementById('logo-upload');
|
||||
const hasLogo = logoUpload && logoUpload.files && logoUpload.files[0];
|
||||
@ -416,9 +505,12 @@ class QRRapidoGenerator {
|
||||
console.log(' Final Primary Color:', finalPrimaryColor);
|
||||
console.log(' Final Background Color:', finalBackgroundColor);
|
||||
|
||||
// Add basic form fields
|
||||
// Add basic form fields with UTF-8 encoding
|
||||
const rawContent = document.getElementById('qr-content').value;
|
||||
const encodedContent = this.prepareContentForQR(rawContent, type);
|
||||
|
||||
formData.append('type', document.getElementById('qr-type').value);
|
||||
formData.append('content', document.getElementById('qr-content').value);
|
||||
formData.append('content', encodedContent);
|
||||
formData.append('quickStyle', quickStyle);
|
||||
formData.append('primaryColor', finalPrimaryColor);
|
||||
formData.append('backgroundColor', finalBackgroundColor);
|
||||
@ -452,10 +544,13 @@ class QRRapidoGenerator {
|
||||
console.log(' Final Primary Color:', finalPrimaryColor);
|
||||
console.log(' Final Background Color:', finalBackgroundColor);
|
||||
|
||||
const rawContent = document.getElementById('qr-content').value;
|
||||
const encodedContent = this.prepareContentForQR(rawContent, type);
|
||||
|
||||
return {
|
||||
data: {
|
||||
type: document.getElementById('qr-type').value,
|
||||
content: document.getElementById('qr-content').value,
|
||||
content: encodedContent,
|
||||
quickStyle: quickStyle,
|
||||
primaryColor: finalPrimaryColor,
|
||||
backgroundColor: finalBackgroundColor,
|
||||
@ -597,8 +692,28 @@ class QRRapidoGenerator {
|
||||
updateContentHints() {
|
||||
const type = document.getElementById('qr-type')?.value;
|
||||
const hintsElement = document.getElementById('content-hints');
|
||||
const vcardInterface = document.getElementById('vcard-interface');
|
||||
const contentTextarea = document.getElementById('qr-content');
|
||||
|
||||
if (!hintsElement || !type) return;
|
||||
|
||||
// Show/hide VCard interface based on type
|
||||
if (type === 'vcard') {
|
||||
if (vcardInterface) vcardInterface.style.display = 'block';
|
||||
if (contentTextarea) {
|
||||
contentTextarea.style.display = 'none';
|
||||
contentTextarea.removeAttribute('required');
|
||||
}
|
||||
hintsElement.textContent = 'Preencha os campos acima para criar seu cartão de visita digital';
|
||||
return; // Skip normal hints for VCard
|
||||
} else {
|
||||
if (vcardInterface) vcardInterface.style.display = 'none';
|
||||
if (contentTextarea) {
|
||||
contentTextarea.style.display = 'block';
|
||||
contentTextarea.setAttribute('required', 'required');
|
||||
}
|
||||
}
|
||||
|
||||
const hints = {
|
||||
'pt-BR': {
|
||||
'url': 'Ex: https://www.exemplo.com.br',
|
||||
@ -999,7 +1114,7 @@ class QRRapidoGenerator {
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type} alert-dismissible fade show`;
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<div>${message}</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
@ -1037,11 +1152,295 @@ class QRRapidoGenerator {
|
||||
typeField.addEventListener('change', updatePreview);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SISTEMA DE FLUXO ASCENDENTE PROGRESSIVO
|
||||
// ============================================
|
||||
|
||||
initializeProgressiveFlow() {
|
||||
// Estado inicial: apenas tipo habilitado, botão gerar desabilitado
|
||||
const qrContent = document.getElementById('qr-content');
|
||||
const vcardInterface = document.getElementById('vcard-interface');
|
||||
|
||||
// Inicialmente desabilitar campo de conteúdo
|
||||
if (qrContent) {
|
||||
qrContent.disabled = true;
|
||||
}
|
||||
|
||||
// Ocultar interface vCard
|
||||
if (vcardInterface) {
|
||||
vcardInterface.style.display = 'none';
|
||||
}
|
||||
|
||||
this.updateGenerateButton();
|
||||
}
|
||||
|
||||
handleTypeSelection(type) {
|
||||
this.selectedType = type;
|
||||
|
||||
if (type) {
|
||||
this.removeInitialHighlight();
|
||||
// Sempre habilitar campos de conteúdo após selecionar tipo
|
||||
this.enableContentFields(type);
|
||||
} else {
|
||||
this.disableAllFields();
|
||||
}
|
||||
|
||||
this.updateGenerateButton();
|
||||
}
|
||||
|
||||
handleStyleSelection(style) {
|
||||
this.selectedStyle = style;
|
||||
this.updateGenerateButton();
|
||||
}
|
||||
|
||||
handleContentChange(content) {
|
||||
const contentField = document.getElementById('qr-content');
|
||||
this.contentValid = this.validateContent(content);
|
||||
|
||||
// Feedback visual para campo de conteúdo
|
||||
if (contentField) {
|
||||
this.validateField(contentField, this.contentValid, 'Conteúdo deve ter pelo menos 3 caracteres');
|
||||
}
|
||||
|
||||
this.updateGenerateButton();
|
||||
}
|
||||
|
||||
enableContentFields(type) {
|
||||
const qrContent = document.getElementById('qr-content');
|
||||
const vcardInterface = document.getElementById('vcard-interface');
|
||||
|
||||
if (type === 'vcard') {
|
||||
// Para vCard, ocultar textarea e mostrar interface específica
|
||||
if (qrContent) {
|
||||
qrContent.style.display = 'none';
|
||||
qrContent.removeAttribute('required');
|
||||
}
|
||||
if (vcardInterface) {
|
||||
vcardInterface.style.display = 'block';
|
||||
this.enableVCardFields();
|
||||
}
|
||||
} else {
|
||||
// Para outros tipos, mostrar textarea
|
||||
if (qrContent) {
|
||||
qrContent.style.display = 'block';
|
||||
qrContent.setAttribute('required', 'required');
|
||||
qrContent.disabled = false;
|
||||
}
|
||||
if (vcardInterface) {
|
||||
vcardInterface.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enableVCardFields() {
|
||||
const requiredFields = ['vcard-name', 'vcard-mobile', 'vcard-email'];
|
||||
requiredFields.forEach(fieldId => {
|
||||
const field = document.getElementById(fieldId);
|
||||
if (field) {
|
||||
field.disabled = false;
|
||||
field.setAttribute('required', 'required');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
disableAllFields() {
|
||||
// Resetar estado
|
||||
const qrContent = document.getElementById('qr-content');
|
||||
const vcardInterface = document.getElementById('vcard-interface');
|
||||
|
||||
if (qrContent) {
|
||||
qrContent.disabled = true;
|
||||
qrContent.value = '';
|
||||
}
|
||||
|
||||
if (vcardInterface) {
|
||||
vcardInterface.style.display = 'none';
|
||||
// Limpar campos vCard
|
||||
const vcardFields = document.querySelectorAll('#vcard-interface input');
|
||||
vcardFields.forEach(field => {
|
||||
field.value = '';
|
||||
field.disabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
this.contentValid = false;
|
||||
}
|
||||
|
||||
validateContent(content) {
|
||||
if (!content) return false;
|
||||
return content.trim().length >= 3;
|
||||
}
|
||||
|
||||
validateVCardFields() {
|
||||
const nameField = document.getElementById('vcard-name');
|
||||
const mobileField = document.getElementById('vcard-mobile');
|
||||
const emailField = document.getElementById('vcard-email');
|
||||
|
||||
const name = nameField?.value.trim() || '';
|
||||
const mobile = mobileField?.value.trim() || '';
|
||||
const email = emailField?.value.trim() || '';
|
||||
|
||||
// Validação individual dos campos com feedback visual
|
||||
this.validateField(nameField, name !== '', 'Nome é obrigatório');
|
||||
this.validateField(mobileField, mobile !== '' && mobile.length >= 10, 'Telefone deve ter pelo menos 10 dígitos');
|
||||
this.validateField(emailField, email !== '' && email.includes('@'), 'Email válido é obrigatório');
|
||||
|
||||
const isValid = name !== '' && mobile !== '' && email !== '' && email.includes('@') && mobile.length >= 10;
|
||||
this.contentValid = isValid;
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
validateField(field, isValid, errorMessage) {
|
||||
if (!field) return;
|
||||
|
||||
const feedbackElement = field.parentNode.querySelector('.invalid-feedback');
|
||||
|
||||
if (isValid) {
|
||||
field.classList.remove('is-invalid');
|
||||
field.classList.add('is-valid');
|
||||
if (feedbackElement) {
|
||||
feedbackElement.style.display = 'none';
|
||||
}
|
||||
} else if (field.value.trim() !== '') {
|
||||
// Só mostrar erro se o campo tem conteúdo
|
||||
field.classList.remove('is-valid');
|
||||
field.classList.add('is-invalid');
|
||||
if (feedbackElement) {
|
||||
feedbackElement.textContent = errorMessage;
|
||||
feedbackElement.style.display = 'block';
|
||||
}
|
||||
} else {
|
||||
// Campo vazio, remover validações visuais
|
||||
field.classList.remove('is-valid', 'is-invalid');
|
||||
if (feedbackElement) {
|
||||
feedbackElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateGenerateButton() {
|
||||
const generateBtn = document.getElementById('generate-btn');
|
||||
if (!generateBtn) return;
|
||||
|
||||
let shouldEnable = false;
|
||||
|
||||
// Verificar se tem tipo selecionado
|
||||
if (this.selectedType) {
|
||||
if (this.selectedType === 'vcard') {
|
||||
// Para vCard, validar campos obrigatórios
|
||||
shouldEnable = this.validateVCardFields();
|
||||
} else {
|
||||
// Para outros tipos, validar conteúdo
|
||||
const content = document.getElementById('qr-content')?.value || '';
|
||||
shouldEnable = this.validateContent(content);
|
||||
}
|
||||
}
|
||||
|
||||
generateBtn.disabled = !shouldEnable;
|
||||
|
||||
// Feedback visual
|
||||
if (shouldEnable) {
|
||||
generateBtn.classList.remove('btn-secondary');
|
||||
generateBtn.classList.add('btn-primary');
|
||||
} else {
|
||||
generateBtn.classList.remove('btn-primary');
|
||||
generateBtn.classList.add('btn-secondary');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove destaque inicial quando tipo for selecionado
|
||||
removeInitialHighlight() {
|
||||
const typeField = document.getElementById('qr-type');
|
||||
if (typeField && typeField.classList.contains('qr-field-highlight')) {
|
||||
typeField.classList.remove('qr-field-highlight');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// FUNÇÕES DE CODIFICAÇÃO UTF-8
|
||||
// ============================================
|
||||
|
||||
// Garantir codificação UTF-8 para todos os tipos de QR Code
|
||||
prepareContentForQR(content, type) {
|
||||
if (!content) return '';
|
||||
|
||||
// Garantir que o conteúdo seja tratado como UTF-8
|
||||
try {
|
||||
// Verificar se há caracteres especiais
|
||||
const hasSpecialChars = /[^\x00-\x7F]/.test(content);
|
||||
|
||||
if (hasSpecialChars) {
|
||||
console.log('Caracteres especiais detectados, aplicando codificação UTF-8');
|
||||
// Forçar codificação UTF-8
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
const encoded = encoder.encode(content);
|
||||
return decoder.decode(encoded);
|
||||
}
|
||||
|
||||
return content;
|
||||
} catch (error) {
|
||||
console.warn('Erro na codificação UTF-8:', error);
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
// Validação de segurança para dados pessoais
|
||||
validateDataSecurity() {
|
||||
const placeholders = document.querySelectorAll('[placeholder]');
|
||||
placeholders.forEach(el => {
|
||||
const placeholder = el.placeholder;
|
||||
|
||||
// Verificar dados pessoais em placeholders
|
||||
if (placeholder.includes('@') && placeholder.includes('gmail')) {
|
||||
console.warn('Dado pessoal detectado em placeholder:', placeholder);
|
||||
el.placeholder = 'seu.email@exemplo.com';
|
||||
}
|
||||
|
||||
// Verificar números de telefone reais
|
||||
if (placeholder.match(/\d{11}/) && !placeholder.includes('99999')) {
|
||||
console.warn('Possível telefone real em placeholder:', placeholder);
|
||||
el.placeholder = el.placeholder.replace(/\d{11}/, '11999998888');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Teste da codificação UTF-8
|
||||
testUTF8Encoding() {
|
||||
const testStrings = [
|
||||
'Ação rápida çáéíóú',
|
||||
'João Gonçalves',
|
||||
'Coração brasileiro',
|
||||
'Situação especial'
|
||||
];
|
||||
|
||||
console.group('🧪 Teste de Codificação UTF-8');
|
||||
testStrings.forEach(str => {
|
||||
const encoded = this.prepareContentForQR(str, 'text');
|
||||
console.log(`Original: "${str}"`);
|
||||
console.log(`Codificado: "${encoded}"`);
|
||||
console.log('---');
|
||||
});
|
||||
|
||||
// Teste específico para vCard
|
||||
if (window.vcardGenerator) {
|
||||
const testName = 'João Gonçalves';
|
||||
const encoded = window.vcardGenerator.encodeQuotedPrintable(testName);
|
||||
console.log(`vCard Quoted-Printable test:`);
|
||||
console.log(`Original: "${testName}"`);
|
||||
console.log(`Encoded: "${encoded}"`);
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.qrGenerator = new QRRapidoGenerator();
|
||||
window.vcardGenerator = new VCardGenerator();
|
||||
|
||||
// Initialize AdSense if necessary
|
||||
if (window.adsbygoogle && document.querySelector('.adsbygoogle')) {
|
||||
@ -1049,6 +1448,260 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// VCard Generator Class
|
||||
class VCardGenerator {
|
||||
constructor() {
|
||||
this.initializeVCardInterface();
|
||||
}
|
||||
|
||||
// Função para codificar caracteres especiais usando Quoted-Printable
|
||||
encodeQuotedPrintable(text) {
|
||||
if (!text) return '';
|
||||
|
||||
return text.replace(/[^\x20-\x7E]/g, (char) => {
|
||||
// Para caracteres especiais comuns do português, usar codificação UTF-8
|
||||
const utf8Bytes = new TextEncoder().encode(char);
|
||||
return Array.from(utf8Bytes)
|
||||
.map(byte => `=${byte.toString(16).toUpperCase().padStart(2, '0')}`)
|
||||
.join('');
|
||||
});
|
||||
}
|
||||
|
||||
// Função para preparar texto com codificação adequada
|
||||
prepareTextForVCard(text, needsEncoding = true) {
|
||||
if (!text) return '';
|
||||
|
||||
const trimmedText = text.trim();
|
||||
if (!needsEncoding) return trimmedText;
|
||||
|
||||
// Verifica se há caracteres especiais que precisam de codificação
|
||||
const hasSpecialChars = /[^\x20-\x7E]/.test(trimmedText);
|
||||
|
||||
if (hasSpecialChars) {
|
||||
return `CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:${this.encodeQuotedPrintable(trimmedText)}`;
|
||||
}
|
||||
|
||||
return trimmedText;
|
||||
}
|
||||
|
||||
initializeVCardInterface() {
|
||||
// Show/hide optional fields based on checkboxes
|
||||
document.querySelectorAll('#vcard-interface .form-check-input').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
const groupId = e.target.id.replace('enable-', '') + '-group';
|
||||
const group = document.getElementById(groupId);
|
||||
if (group) {
|
||||
group.style.display = e.target.checked ? 'block' : 'none';
|
||||
// Clear values when hiding fields
|
||||
if (!e.target.checked) {
|
||||
const inputs = group.querySelectorAll('input');
|
||||
inputs.forEach(input => input.value = '');
|
||||
}
|
||||
}
|
||||
this.updatePreview();
|
||||
});
|
||||
});
|
||||
|
||||
// Update preview in real-time
|
||||
document.querySelectorAll('#vcard-interface input').forEach(input => {
|
||||
input.addEventListener('input', () => this.updatePreview());
|
||||
input.addEventListener('blur', () => this.validateField(input));
|
||||
});
|
||||
}
|
||||
|
||||
updatePreview() {
|
||||
const vcard = this.generateVCardContent();
|
||||
const previewElement = document.getElementById('vcard-preview-text');
|
||||
if (previewElement) {
|
||||
previewElement.textContent = vcard;
|
||||
}
|
||||
}
|
||||
|
||||
generateVCardContent() {
|
||||
const data = this.collectVCardData();
|
||||
|
||||
let vcard = 'BEGIN:VCARD\nVERSION:3.0\nCHARSET=UTF-8\n';
|
||||
|
||||
// Nome (obrigatório)
|
||||
if (data.name) {
|
||||
const nameParts = data.name.trim().split(' ');
|
||||
const firstName = nameParts[0] || '';
|
||||
const lastName = nameParts.slice(1).join(' ') || '';
|
||||
|
||||
// Codificar nome se necessário
|
||||
const encodedLastName = this.prepareTextForVCard(lastName);
|
||||
const encodedFirstName = this.prepareTextForVCard(firstName);
|
||||
const encodedFullName = this.prepareTextForVCard(data.name.trim());
|
||||
|
||||
vcard += `N:${encodedLastName};${encodedFirstName}\n`;
|
||||
vcard += `FN:${encodedFullName}\n`;
|
||||
}
|
||||
|
||||
// Empresa
|
||||
if (data.company) {
|
||||
const encodedCompany = this.prepareTextForVCard(data.company);
|
||||
vcard += `ORG:${encodedCompany}\n`;
|
||||
}
|
||||
|
||||
// Título
|
||||
if (data.title) {
|
||||
const encodedTitle = this.prepareTextForVCard(data.title);
|
||||
vcard += `TITLE:${encodedTitle}\n`;
|
||||
}
|
||||
|
||||
// Endereço
|
||||
if (data.address || data.city || data.state || data.zip) {
|
||||
const encodedAddress = this.prepareTextForVCard(data.address || '');
|
||||
const encodedCity = this.prepareTextForVCard(data.city || '');
|
||||
const encodedState = this.prepareTextForVCard(data.state || '');
|
||||
const encodedZip = this.prepareTextForVCard(data.zip || '', false); // ZIP não precisa de codificação especial
|
||||
const encodedCountry = this.prepareTextForVCard('Brasil');
|
||||
|
||||
const addr = `;;${encodedAddress};${encodedCity};${encodedState};${encodedZip};${encodedCountry}`;
|
||||
vcard += `ADR:${addr}\n`;
|
||||
}
|
||||
|
||||
// Telefones
|
||||
if (data.phone) vcard += `TEL;WORK;VOICE:${data.phone}\n`;
|
||||
if (data.mobile) vcard += `TEL;CELL:${data.mobile}\n`;
|
||||
|
||||
// Email
|
||||
if (data.email) vcard += `EMAIL;WORK;INTERNET:${data.email}\n`;
|
||||
|
||||
// Website
|
||||
if (data.website) vcard += `URL:${data.website}\n`;
|
||||
|
||||
vcard += 'END:VCARD';
|
||||
|
||||
return vcard;
|
||||
}
|
||||
|
||||
collectVCardData() {
|
||||
return {
|
||||
name: document.getElementById('vcard-name')?.value?.trim() || '',
|
||||
mobile: document.getElementById('vcard-mobile')?.value?.trim() || '',
|
||||
email: document.getElementById('vcard-email')?.value?.trim() || '',
|
||||
company: document.getElementById('enable-company')?.checked ?
|
||||
(document.getElementById('vcard-company')?.value?.trim() || '') : '',
|
||||
title: document.getElementById('enable-title')?.checked ?
|
||||
(document.getElementById('vcard-title')?.value?.trim() || '') : '',
|
||||
website: document.getElementById('enable-website')?.checked ?
|
||||
(document.getElementById('vcard-website')?.value?.trim() || '') : '',
|
||||
address: document.getElementById('enable-address')?.checked ?
|
||||
(document.getElementById('vcard-address')?.value?.trim() || '') : '',
|
||||
city: document.getElementById('enable-address')?.checked ?
|
||||
(document.getElementById('vcard-city')?.value?.trim() || '') : '',
|
||||
state: document.getElementById('enable-address')?.checked ?
|
||||
(document.getElementById('vcard-state')?.value?.trim() || '') : '',
|
||||
zip: document.getElementById('enable-address')?.checked ?
|
||||
(document.getElementById('vcard-zip')?.value?.trim() || '') : '',
|
||||
phone: document.getElementById('enable-phone')?.checked ?
|
||||
(document.getElementById('vcard-phone')?.value?.trim() || '') : ''
|
||||
};
|
||||
}
|
||||
|
||||
validateVCardData() {
|
||||
const data = this.collectVCardData();
|
||||
const errors = [];
|
||||
|
||||
// Required field validations
|
||||
if (!data.name) errors.push('Nome é obrigatório');
|
||||
if (!data.mobile) errors.push('Telefone celular é obrigatório');
|
||||
if (!data.email) errors.push('Email é obrigatório');
|
||||
|
||||
// Format validations
|
||||
if (data.email && !this.isValidEmail(data.email)) {
|
||||
errors.push('Email inválido');
|
||||
}
|
||||
|
||||
if (data.website && !this.isValidURL(data.website)) {
|
||||
errors.push('Website inválido (deve começar com http:// ou https://)');
|
||||
}
|
||||
|
||||
if (data.mobile && !this.isValidPhone(data.mobile)) {
|
||||
errors.push('Telefone celular inválido (deve ter 10-11 dígitos)');
|
||||
}
|
||||
|
||||
if (data.phone && !this.isValidPhone(data.phone)) {
|
||||
errors.push('Telefone fixo inválido (deve ter 10-11 dígitos)');
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
validateField(input) {
|
||||
const value = input.value.trim();
|
||||
let isValid = true;
|
||||
let message = '';
|
||||
|
||||
switch (input.id) {
|
||||
case 'vcard-email':
|
||||
if (value && !this.isValidEmail(value)) {
|
||||
isValid = false;
|
||||
message = 'Email inválido';
|
||||
}
|
||||
break;
|
||||
case 'vcard-website':
|
||||
if (value && !this.isValidURL(value)) {
|
||||
isValid = false;
|
||||
message = 'Website inválido (deve começar com http:// ou https://)';
|
||||
}
|
||||
break;
|
||||
case 'vcard-mobile':
|
||||
case 'vcard-phone':
|
||||
if (value && !this.isValidPhone(value)) {
|
||||
isValid = false;
|
||||
message = 'Telefone inválido (apenas números, 10-11 dígitos)';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Update field validation state
|
||||
if (isValid) {
|
||||
input.classList.remove('is-invalid');
|
||||
input.classList.add('is-valid');
|
||||
} else {
|
||||
input.classList.remove('is-valid');
|
||||
input.classList.add('is-invalid');
|
||||
// Show error message
|
||||
const feedback = input.parentNode.querySelector('.invalid-feedback');
|
||||
if (feedback) {
|
||||
feedback.textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
isValidEmail(email) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
}
|
||||
|
||||
isValidURL(url) {
|
||||
try {
|
||||
new URL(url);
|
||||
return url.startsWith('http://') || url.startsWith('https://');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
isValidPhone(phone) {
|
||||
// Brazilian phone validation (DDD + number)
|
||||
const digitsOnly = phone.replace(/\D/g, '');
|
||||
return /^\d{10,11}$/.test(digitsOnly);
|
||||
}
|
||||
|
||||
// Method to be called by main QR generator
|
||||
getVCardContent() {
|
||||
const errors = this.validateVCardData();
|
||||
if (errors.length > 0) {
|
||||
throw new Error('Erro na validação: ' + errors.join(', '));
|
||||
}
|
||||
return this.generateVCardContent();
|
||||
}
|
||||
}
|
||||
|
||||
// Global functions for ad control
|
||||
window.QRApp = {
|
||||
refreshAds: function() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user