Compare commits

...

1 Commits

Author SHA1 Message Date
Ricardo Carneiro
e800900203 feat: Onboarding 2025-07-31 13:40:03 -03:00
5 changed files with 1334 additions and 22 deletions

View File

@ -9,7 +9,11 @@
"Bash(dotnet run:*)",
"Bash(curl:*)",
"Bash(pkill:*)",
"Bash(true)"
"Bash(true)",
"Bash(node:*)",
"Bash(grep:*)",
"Bash(mv:*)",
"Bash(sed:*)"
],
"deny": []
}

View File

@ -68,11 +68,11 @@
</div>
<div class="row">
<div class="col-md-6 mb-3">
<div class="col-md-6 mb-3" id="qr-type-container">
<label class="form-label fw-semibold">
<i class="fas fa-list"></i> @Localizer["QRCodeType"]
</label>
<select id="qr-type" class="form-select" required>
<select id="qr-type" class="form-select qr-field-highlight" required>
<option value="">@Localizer["SelectType"]</option>
<option value="url">🌐 @Localizer["URLLink"]</option>
<option value="text">📝 @Localizer["SimpleText"]</option>
@ -87,38 +87,38 @@
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">
<label class="form-label fw-semibold qr-flow-disabled">
<i class="fas fa-palette"></i> @Localizer["QuickStyle"]
</label>
<div class="btn-group w-100" role="group">
<input type="radio" class="btn-check" name="quick-style" id="style-classic" value="classic" checked>
<div class="btn-group w-100 qr-flow-disabled" role="group" id="style-selector">
<input type="radio" class="btn-check" name="quick-style" id="style-classic" value="classic" checked disabled>
<label class="btn btn-outline-secondary" for="style-classic">@Localizer["Classic"]</label>
<input type="radio" class="btn-check" name="quick-style" id="style-modern" value="modern">
<input type="radio" class="btn-check" name="quick-style" id="style-modern" value="modern" disabled>
<label class="btn btn-outline-secondary" for="style-modern">@Localizer["Modern"]</label>
<input type="radio" class="btn-check" name="quick-style" id="style-colorful" value="colorful">
<input type="radio" class="btn-check" name="quick-style" id="style-colorful" value="colorful" disabled>
<label class="btn btn-outline-secondary" for="style-colorful">@Localizer["Colorful"]</label>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">
<label class="form-label fw-semibold qr-flow-disabled">
<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 qr-flow-disabled"
rows="3"
placeholder="@Localizer["EnterQRCodeContent"]"
required></textarea>
required disabled></textarea>
<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 id="vcard-interface" class="mb-3 qr-flow-disabled" 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.
@ -158,7 +158,7 @@
<!-- 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)
<i class="fas fa-plus-circle"></i> Informações Adicionais (Opcionais) - Escolha abaixo
</h6>
<!-- Empresa -->
@ -258,7 +258,7 @@ END:VCARD</pre>
</div>
<!-- Advanced customization (collapsible) -->
<div class="accordion mb-3" id="customization-accordion">
<div class="accordion mb-3 qr-flow-disabled" id="customization-accordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#customization-panel">
@ -370,8 +370,8 @@ END:VCARD</pre>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg" id="generate-btn">
<div class="d-grid" id="generate-button-container" style="display: none;">
<button type="submit" class="btn btn-primary btn-lg" id="generate-btn" disabled>
<i class="fas fa-bolt"></i> @Localizer["GenerateQRCodeQuickly"]
<div class="spinner-border spinner-border-sm ms-2 d-none" role="status">
<span class="visually-hidden">@Localizer["Generating"]</span>
@ -626,5 +626,6 @@ END:VCARD</pre>
</div>
</section>
<!-- Ad Space Footer (conditional) -->
@await Html.PartialAsync("_AdSpace", new { position = "footer" })

643
Views/Home/sedTubiCQ Normal file
View File

@ -0,0 +1,643 @@
@using QRRapidoApp.Services
@using Microsoft.Extensions.Localization
@inject AdDisplayService AdService
@inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer
@{
ViewData["Title"] = "Home";
var userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="container">
<div class="row">
<!-- QR Generator Form -->
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h3 class="h5 mb-0">
<i class="fas fa-qrcode"></i> @Localizer["CreateQRCodeQuickly"]
</h3>
</div>
<div class="card-body">
@if (User.Identity.IsAuthenticated)
{
var isPremium = await AdService.HasValidPremiumSubscription(userId);
@if (isPremium)
{
<div class="alert alert-success border-0">
<i class="fas fa-crown text-warning"></i>
<strong>@Localizer["PremiumUserActive"]</strong>
<span class="badge bg-success">@Localizer["NoAdsHistoryUnlimitedQR"]</span>
</div>
}
}
<form id="qr-speed-form" class="needs-validation" novalidate>
<!-- Generation timer -->
<div class="row mb-3">
<div class="col-md-8">
<div class="d-flex align-items-center gap-3">
<div class="generation-timer d-none">
<i class="fas fa-stopwatch text-primary"></i>
<span class="fw-bold text-primary">0.0s</span>
</div>
<div class="speed-badge d-none">
<span class="badge bg-success">
<i class="fas fa-bolt"></i> @Localizer["UltraFastGeneration"]
</span>
</div>
</div>
</div>
<div class="col-md-4 text-end">
@if (User.Identity.IsAuthenticated)
{
<small class="text-muted">
<span class="qr-counter">@Localizer["UnlimitedToday"]</span>
</small>
}
else
{
<small class="text-muted">
<span class="qr-counter">@Localizer["QRCodesRemaining"]</span>
</small>
}
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3" id="qr-type-container">
<label class="form-label fw-semibold">
<i class="fas fa-list"></i> @Localizer["QRCodeType"]
<i class="fas fa-chevron-down chevron-animated d-none" id="type-chevron"></i>
</label>
<select id="qr-type" class="form-select qr-field-highlight" required>
<option value="">@Localizer["SelectType"]</option>
<option value="url">🌐 @Localizer["URLLink"]</option>
<option value="text">📝 @Localizer["SimpleText"]</option>
<option value="wifi">📶 @Localizer["WiFi"]</option>
<option value="vcard">👤 @Localizer["VCard"]</option>
<option value="sms">💬 @Localizer["SMS"]</option>
<option value="email">📧 @Localizer["Email"]</option>
@if (User.Identity.IsAuthenticated)
{
<option value="dynamic">⚡ @Localizer["DynamicQRPremium"]</option>
}
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold qr-flow-disabled">
<i class="fas fa-palette"></i> @Localizer["QuickStyle"]
</label>
<div class="btn-group w-100 qr-flow-disabled" role="group" id="style-selector">
<input type="radio" class="btn-check" name="quick-style" id="style-classic" value="classic" checked disabled>
<label class="btn btn-outline-secondary" for="style-classic">@Localizer["Classic"]</label>
<input type="radio" class="btn-check" name="quick-style" id="style-modern" value="modern" disabled>
<label class="btn btn-outline-secondary" for="style-modern">@Localizer["Modern"]</label>
<input type="radio" class="btn-check" name="quick-style" id="style-colorful" value="colorful" disabled>
<label class="btn btn-outline-secondary" for="style-colorful">@Localizer["Colorful"]</label>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold qr-flow-disabled">
<i class="fas fa-edit"></i> @Localizer["Content"]
</label>
<textarea id="qr-content"
class="form-control form-control-lg qr-flow-disabled"
rows="3"
placeholder="@Localizer["EnterQRCodeContent"]"
required disabled></textarea>
<div class="form-text">
<span id="content-hints">@Localizer["ContentHints"]</span>
</div>
</div>
<!-- VCard Interface (dynamic) -->
<div id="vcard-interface" class="mb-3 qr-flow-disabled" 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" placeholder="Ricardo Gonçalves" 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" placeholder="11961534225" 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" placeholder="seu@email.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
Preencha os campos acima para ver o preview...
END:VCARD</pre>
</div>
</div>
</div>
</div>
<!-- Advanced customization (collapsible) -->
<div class="accordion mb-3 qr-flow-disabled" id="customization-accordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#customization-panel">
<i class="fas fa-sliders-h me-2"></i> @Localizer["AdvancedCustomization"]
</button>
</h2>
<div id="customization-panel" class="accordion-collapse collapse">
<div class="accordion-body">
<div class="row">
<div class="col-md-3 mb-3">
<label class="form-label">@Localizer["PrimaryColor"]</label>
<input type="color" id="primary-color" class="form-control form-control-color" value="#007BFF">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">@Localizer["BackgroundColor"]</label>
<input type="color" id="bg-color" class="form-control form-control-color" value="#FFFFFF">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">@Localizer["Size"]</label>
<select id="qr-size" class="form-select">
<option value="200">@Localizer["SmallSize200px"]</option>
<option value="300" selected>@Localizer["MediumSize300px"]</option>
<option value="500">@Localizer["LargeSize500px"]</option>
<option value="800">@Localizer["XLSize800px"]</option>
</select>
</div>
<div class="col-md-3 mb-3">
<label class="form-label">@Localizer["Margin"]</label>
<select id="qr-margin" class="form-select">
<option value="1">@Localizer["Minimal"]</option>
<option value="2" selected>@Localizer["Normal"]</option>
<option value="4">@Localizer["Large"]</option>
</select>
</div>
</div>
@if (User.Identity.IsAuthenticated)
{
<div class="row">
@{
var userService = Context.RequestServices.GetService<QRRapidoApp.Services.IUserService>();
var currentUserId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
var currentUser = currentUserId != null ? await userService.GetUserAsync(currentUserId) : null;
var isPremium = currentUser?.IsPremium == true;
}
@if (isPremium)
{
<div class="col-md-6 mb-3">
<label class="form-label">
@Localizer["LogoIcon"]
<span class="badge bg-warning text-dark ms-1">Premium</span>
</label>
<input type="file" id="logo-upload" class="form-control" accept="image/png,image/jpeg,image/jpg">
<div class="form-text">@Localizer["PNGJPGUp2MB"]</div>
<div id="logo-preview" class="mt-2 d-none">
<small class="text-success">
<i class="fas fa-check-circle"></i>
<span id="logo-filename"></span>
</small>
</div>
</div>
}
else
{
<div class="col-md-6 mb-3">
<div class="premium-upgrade-box p-3 border rounded bg-light">
<h6 class="fw-bold mb-2">
<i class="fas fa-crown text-warning"></i>
Logo Personalizado - Premium
</h6>
<p class="mb-2 small">Adicione sua marca aos QR Codes! Upgrade para Premium e personalize com seu logo.</p>
<a href="/Pagamento/SelecaoPlano" class="btn btn-warning btn-sm">
<i class="fas fa-arrow-up"></i> Fazer Upgrade
</a>
</div>
</div>
}
<div class="col-md-6 mb-3">
<label class="form-label">@Localizer["BorderStyle"]</label>
<select id="corner-style" class="form-select @(isPremium ? "" : "border-warning")">
<option value="square">@Localizer["Square"] (Grátis)</option>
@if (isPremium)
{
<option value="rounded">@Localizer["Rounded"] 👑</option>
<option value="circle">Círculos 👑</option>
<option value="leaf">Folha 👑</option>
}
else
{
<option value="rounded" disabled>@Localizer["Rounded"] - Premium 👑</option>
<option value="circle" disabled>Círculos - Premium 👑</option>
<option value="leaf" disabled>Folha - Premium 👑</option>
}
</select>
@if (!isPremium)
{
<div class="form-text text-warning">
<i class="fas fa-crown"></i>
<a href="/Pagamento/SelecaoPlano" class="text-warning">Upgrade Premium</a> para estilos personalizados
</div>
}
</div>
</div>
}
</div>
</div>
</div>
</div>
<div class="d-grid" id="generate-button-container" style="display: none;">
<button type="submit" class="btn btn-primary btn-lg" id="generate-btn" disabled>
<i class="fas fa-bolt"></i> @Localizer["GenerateQRCodeQuickly"]
<div class="spinner-border spinner-border-sm ms-2 d-none" role="status">
<span class="visually-hidden">@Localizer["Generating"]</span>
</div>
</button>
</div>
</form>
</div>
</div>
<!-- Speed statistics -->
<div class="row mt-4">
<div class="col-md-4">
<div class="card text-center border-success">
<div class="card-body">
<h5 class="text-success">
<i class="fas fa-stopwatch"></i> 1.2s
</h5>
<small class="text-muted">@Localizer["AverageTime"]</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center border-primary">
<div class="card-body">
<h5 class="text-primary">
<i class="fas fa-chart-line"></i> 99.9%
</h5>
<small class="text-muted">@Localizer["Availability"]</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center border-warning">
<div class="card-body">
<h5 class="text-warning">
<i class="fas fa-users"></i> <span id="total-qrs">10.5K</span>
</h5>
<small class="text-muted">@Localizer["QRsGeneratedToday"]</small>
</div>
</div>
</div>
</div>
<!-- Ad Space Between Content (conditional) -->
@await Html.PartialAsync("_AdSpace", new { position = "content" })
</div>
<!-- Sidebar with preview and ads -->
<div class="col-lg-4">
<!-- Preview with timer -->
<div class="card shadow-sm mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-eye"></i> Preview
</h5>
<div class="generation-stats d-none">
<small class="text-success">
<i class="fas fa-check-circle"></i> Gerado em <span class="generation-time">0s</span>
</small>
</div>
</div>
<div class="card-body text-center">
<div id="qr-preview" class="mb-3">
<div class="placeholder-qr p-5">
<i class="fas fa-qrcode fa-4x text-muted mb-3"></i>
<p class="text-muted">@Localizer["YourQRCodeWillAppear"]</p>
<small class="text-muted">
<i class="fas fa-bolt"></i> @Localizer["UltraFastGenerationGuaranteed"]
</small>
</div>
</div>
<div id="download-section" style="display: none;">
<div class="btn-group-vertical w-100 mb-3">
<button id="download-png" class="btn btn-success">
<i class="fas fa-download"></i> @Localizer["DownloadPNG"]
</button>
<button id="download-svg" class="btn btn-outline-success">
<i class="fas fa-vector-square"></i> @Localizer["DownloadSVGVector"]
</button>
<button id="download-pdf" class="btn btn-outline-success">
<i class="fas fa-file-pdf"></i> @Localizer["DownloadPDF"]
</button>
</div>
<!-- Share Button with Dropdown -->
<div class="dropdown w-100 mb-3">
<button class="btn btn-primary dropdown-toggle w-100" type="button" id="share-qr-btn" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-share-alt"></i> @Localizer["ShareQRCode"]
</button>
<ul class="dropdown-menu w-100" aria-labelledby="share-qr-btn" id="share-dropdown">
<!-- Native share option (mobile only) -->
<li class="d-none" id="native-share-option">
<a class="dropdown-item" href="#" id="native-share">
<i class="fas fa-mobile-alt text-primary"></i> @Localizer["ShareSystem"]
</a>
</li>
<!-- WhatsApp -->
<li>
<a class="dropdown-item" href="#" id="share-whatsapp">
<i class="fab fa-whatsapp text-success"></i> WhatsApp
</a>
</li>
<!-- Telegram -->
<li>
<a class="dropdown-item" href="#" id="share-telegram">
<i class="fab fa-telegram text-info"></i> Telegram
</a>
</li>
<!-- Email -->
<li>
<a class="dropdown-item" href="#" id="share-email">
<i class="fas fa-envelope text-warning"></i> Email
</a>
</li>
<!-- Copy to clipboard -->
<li>
<a class="dropdown-item" href="#" id="copy-qr-link">
<i class="fas fa-copy text-secondary"></i> Copiar Link
</a>
</li>
<!-- Save to gallery (mobile only) -->
<li class="d-none" id="save-gallery-option">
<a class="dropdown-item" href="#" id="save-to-gallery">
<i class="fas fa-images text-purple"></i> Salvar na Galeria
</a>
</li>
</ul>
</div>
@if (User.Identity.IsAuthenticated)
{
<button id="save-to-history" class="btn btn-outline-primary w-100">
<i class="fas fa-save"></i> @Localizer["SaveToHistory"]
</button>
}
else
{
<div class="text-center">
<small class="text-muted">
<a href="/Account/Login" class="text-primary">@Localizer["Login"]</a>
@Localizer["ToSaveToHistory"]
</small>
</div>
}
</div>
</div>
</div>
<!-- Premium Card for non-premium users -->
@if (User.Identity.IsAuthenticated && await AdService.ShouldShowAds(userId))
{
<div class="card border-warning mb-4">
<div class="card-header bg-warning text-dark">
<h6 class="mb-0">
<i class="fas fa-rocket"></i> QR Rapido Premium
</h6>
</div>
<div class="card-body">
<div class="text-center mb-3">
<div class="badge bg-success mb-2">@Localizer["ThreeTimesFaster"]</div>
</div>
<ul class="list-unstyled">
<li><i class="fas fa-check text-success"></i> @Localizer["NoAdsForever"]</li>
<li><i class="fas fa-check text-success"></i> @Localizer["UnlimitedQRCodes"]</li>
<li><i class="fas fa-check text-success"></i> @Localizer["PriorityGeneration"]</li>
<li><i class="fas fa-check text-success"></i> @Localizer["DynamicQRCodes"]</li>
<li><i class="fas fa-check text-success"></i> @Localizer["RealTimeAnalytics"]</li>
<li><i class="fas fa-check text-success"></i> @Localizer["DeveloperAPI"]</li>
</ul>
<div class="text-center">
<a href="/Premium/Upgrade" class="btn btn-warning w-100">
<i class="fas fa-bolt"></i> @Localizer["AcceleratePrice"]
</a>
<small class="text-muted d-block mt-1">@Localizer["CancelAnytime"]</small>
</div>
</div>
</div>
}
<!-- Speed Tips Card -->
<div class="card bg-light mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-lightbulb text-warning"></i> @Localizer["TipsFasterQR"]
</h6>
</div>
<div class="card-body">
<ul class="list-unstyled small">
<li><i class="fas fa-arrow-right text-primary"></i> @Localizer["ShortURLsFaster"]</li>
<li><i class="fas fa-arrow-right text-primary"></i> @Localizer["LessTextMoreSpeed"]</li>
<li><i class="fas fa-arrow-right text-primary"></i> @Localizer["SolidColorsOptimize"]</li>
<li><i class="fas fa-arrow-right text-primary"></i> @Localizer["SmallerSizesAccelerate"]</li>
</ul>
</div>
</div>
<!-- Ad Space Sidebar (conditional) -->
@await Html.PartialAsync("_AdSpace", new { position = "sidebar" })
</div>
</div>
</div>
<!-- Speed Comparison Section -->
<section class="mt-5 mb-4">
<div class="container">
<div class="text-center mb-4">
<h3><i class="fas fa-tachometer-alt text-primary"></i> @Localizer["WhyQRRapidoFaster"]</h3>
<p class="text-muted">@Localizer["ComparisonOtherGenerators"]</p>
</div>
<div class="row">
<div class="col-md-3">
<div class="card h-100 border-success">
<div class="card-body text-center">
<h5 class="text-success">QR Rapido</h5>
<div class="display-4 text-success fw-bold">1.2s</div>
<p class="text-muted">@Localizer["OptimizedForSpeed"]</p>
<i class="fas fa-crown text-warning"></i>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="text-muted">@Localizer["CompetitorA"]</h5>
<div class="display-4 text-muted">3.5s</div>
<p class="text-muted">@Localizer["TraditionalGenerator"]</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="text-muted">@Localizer["CompetitorB"]</h5>
<div class="display-4 text-muted">4.8s</div>
<p class="text-muted">@Localizer["HeavyInterface"]</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="text-muted">@Localizer["CompetitorC"]</h5>
<div class="display-4 text-muted">6.2s</div>
<p class="text-muted">@Localizer["ManyAds"]</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Botão de ajuda flutuante -->
<button type="button" class="help-button-floating" id="help-button" title="Guia de Preenchimento">
<i class="fas fa-question"></i>
</button>
<!-- Container para toasts do onboarding -->
<div class="toast-container-onboarding" id="toast-container"></div>
<!-- Scripts do onboarding -->
<script src="~/js/qr-onboarding.js"></script>
<!-- Ad Space Footer (conditional) -->
@await Html.PartialAsync("_AdSpace", new { position = "footer" })

View File

@ -376,6 +376,334 @@ footer a:hover {
border-image: linear-gradient(135deg, var(--qr-primary) 0%, var(--qr-accent) 100%) 1;
}
/* =================================
FLUXO ASCENDENTE - ESTADOS VISUAIS PROGRESSIVOS
================================= */
/* Estados desabilitados - cor e opacidade reduzida */
.qr-flow-disabled {
opacity: 0.6 !important;
pointer-events: none !important;
transition: all 0.3s ease !important;
}
.qr-flow-disabled .form-control,
.qr-flow-disabled .form-select {
background-color: #f5f5f5 !important;
border-color: #e0e0e0 !important;
color: #999999 !important;
cursor: not-allowed !important;
}
.qr-flow-disabled .form-label {
color: #999999 !important;
}
.qr-flow-disabled .btn-group .btn {
background-color: #f5f5f5 !important;
border-color: #e0e0e0 !important;
color: #999999 !important;
cursor: not-allowed !important;
}
/* Estados habilitados - transição suave */
.qr-flow-enabled {
opacity: 1 !important;
pointer-events: auto !important;
transition: all 0.3s ease !important;
}
.qr-flow-enabled .form-control,
.qr-flow-enabled .form-select {
background-color: #ffffff !important;
border-color: #ced4da !important;
color: #495057 !important;
cursor: auto !important;
}
.qr-flow-enabled .form-label {
color: #212529 !important;
}
.qr-flow-enabled .btn-group .btn {
background-color: #ffffff !important;
border-color: #6c757d !important;
color: #6c757d !important;
cursor: pointer !important;
}
/* Estados para o tema escuro */
html[data-theme="dark"] .qr-flow-disabled .form-control,
html[data-theme="dark"] .qr-flow-disabled .form-select {
background-color: #3a3a3a !important;
border-color: #555555 !important;
color: #777777 !important;
}
html[data-theme="dark"] .qr-flow-disabled .form-label {
color: #777777 !important;
}
html[data-theme="dark"] .qr-flow-disabled .btn-group .btn {
background-color: #3a3a3a !important;
border-color: #555555 !important;
color: #777777 !important;
}
html[data-theme="dark"] .qr-flow-enabled .form-control,
html[data-theme="dark"] .qr-flow-enabled .form-select {
background-color: #4a5568 !important;
border-color: #718096 !important;
color: #e2e8f0 !important;
}
html[data-theme="dark"] .qr-flow-enabled .form-label {
color: #e2e8f0 !important;
}
html[data-theme="dark"] .qr-flow-enabled .btn-group .btn {
background-color: #4a5568 !important;
border-color: #718096 !important;
color: #e2e8f0 !important;
}
/* Animação de ascensão */
@keyframes ascend {
from {
opacity: 0.6;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.qr-flow-ascending {
animation: ascend 0.4s ease-out forwards !important;
}
/* Botão de geração - estado oculto inicial */
#generate-button-container {
transition: all 0.4s ease !important;
transform: translateY(20px);
opacity: 0;
}
#generate-button-container.show {
display: block !important;
transform: translateY(0);
opacity: 1;
animation: ascend 0.4s ease-out forwards;
}
/* =================================
DESTAQUE ELEGANTE PARA CAMPO INICIAL
================================= */
/* Animação pulsante elegante */
@keyframes pulseHighlight {
0% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
border-color: #3B82F6;
}
70% {
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
border-color: #3B82F6;
}
100% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
border-color: #3B82F6;
}
}
/* 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;
}
/* Remover destaque suavemente */
.qr-field-highlight.removing {
animation: none !important;
border-color: #ced4da !important;
box-shadow: none !important;
}
/* Ícone de seta animado */
@keyframes chevronBounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-3px);
}
60% {
transform: translateY(-1px);
}
}
.chevron-animated {
animation: chevronBounce 2s infinite;
color: #3B82F6;
margin-left: 8px;
font-size: 0.875rem;
}
/* Tooltip para campos bloqueados */
.blocked-field-tooltip {
position: relative;
}
.blocked-field-tooltip::after {
content: "Selecione primeiro o Tipo de QR Code";
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: rgba(33, 37, 41, 0.9);
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 0.75rem;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
z-index: 1000;
margin-bottom: 5px;
}
.blocked-field-tooltip::before {
content: "";
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: rgba(33, 37, 41, 0.9);
margin-bottom: -5px;
opacity: 0;
transition: opacity 0.3s ease;
}
.blocked-field-tooltip:hover::after,
.blocked-field-tooltip:hover::before {
opacity: 1;
}
/* Botão de ajuda flutuante */
.help-button-floating {
position: fixed;
top: 20px;
right: 20px;
z-index: 1050;
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(135deg, #3B82F6, #6366F1);
border: none;
color: white;
font-size: 1.25rem;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.help-button-floating:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.6);
background: linear-gradient(135deg, #2563EB, #4F46E5);
}
.help-button-floating:active {
transform: translateY(0);
}
/* Seta apontando para o select */
.pointer-arrow {
position: absolute;
width: 0;
height: 0;
border-left: 15px solid transparent;
border-right: 15px solid transparent;
border-bottom: 20px solid #3B82F6;
top: -25px;
left: 50%;
transform: translateX(-50%);
animation: arrowBounce 1.5s ease-in-out infinite;
}
@keyframes arrowBounce {
0%, 100% {
transform: translateX(-50%) translateY(0);
}
50% {
transform: translateX(-50%) translateY(-5px);
}
}
/* Toasts customizados */
.toast-onboarding {
background: linear-gradient(135deg, #3B82F6, #6366F1);
color: white;
border: none;
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.3);
border-radius: 12px;
overflow: hidden;
}
.toast-onboarding .toast-header {
background: rgba(255, 255, 255, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
color: white;
}
.toast-onboarding .toast-body {
font-weight: 500;
line-height: 1.5;
}
.toast-onboarding .btn-close {
filter: invert(1);
opacity: 0.8;
}
.toast-onboarding .btn-close:hover {
opacity: 1;
}
/* Container de toasts personalizado */
.toast-container-onboarding {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1055;
max-width: 350px;
}
/* Tema escuro - ajustes para destaque */
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"] .chevron-animated {
color: #60A5FA;
}
html[data-theme="dark"] .blocked-field-tooltip::after {
background: rgba(248, 250, 252, 0.95);
color: #1f2937;
}
html[data-theme="dark"] .blocked-field-tooltip::before {
border-top-color: rgba(248, 250, 252, 0.95);
}
/* Print Styles */
@media print {
.ad-container,

View File

@ -4,6 +4,9 @@ class QRRapidoGenerator {
this.startTime = 0;
this.currentQR = null;
this.timerInterval = null;
this.selectedType = null;
this.selectedStyle = null;
this.contentValid = false;
this.languageStrings = {
'pt-BR': {
tagline: 'Gere QR codes em segundos!',
@ -45,6 +48,7 @@ class QRRapidoGenerator {
this.checkAdFreeStatus();
this.updateLanguage();
this.updateStatsCounters();
this.initializeProgressiveFlow();
}
initializeEvents() {
@ -54,11 +58,22 @@ class QRRapidoGenerator {
form.addEventListener('submit', this.generateQRWithTimer.bind(this));
}
// Quick style selection
// Quick style selection with flow control
document.querySelectorAll('input[name="quick-style"]').forEach(radio => {
radio.addEventListener('change', this.applyQuickStyle.bind(this));
radio.addEventListener('change', (e) => {
this.handleStyleSelection(e.target.value);
this.applyQuickStyle(e);
});
});
// Content field monitoring
const contentField = document.getElementById('qr-content');
if (contentField) {
contentField.addEventListener('input', (e) => {
this.handleContentChange(e.target.value);
});
}
// Logo upload feedback
const logoUpload = document.getElementById('logo-upload');
if (logoUpload) {
@ -71,10 +86,13 @@ class QRRapidoGenerator {
cornerStyle.addEventListener('change', this.handleCornerStyleChange.bind(this));
}
// QR type change with hints
// QR type change with hints and flow control
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();
});
}
// Language selector
@ -98,6 +116,7 @@ class QRRapidoGenerator {
if (saveBtn) {
saveBtn.addEventListener('click', this.saveToHistory.bind(this));
}
}
setupDownloadButtons() {
@ -373,6 +392,12 @@ class QRRapidoGenerator {
return false;
}
// Check if flow is properly completed
if (!this.selectedType || !this.selectedStyle || !this.contentValid) {
this.showError('Complete todas as etapas antes de gerar o QR Code');
return false;
}
// Special validation for VCard
if (qrType === 'vcard') {
try {
@ -652,7 +677,11 @@ class QRRapidoGenerator {
// Show/hide VCard interface based on type
if (type === 'vcard') {
if (vcardInterface) vcardInterface.style.display = 'block';
if (vcardInterface) {
vcardInterface.style.display = 'block';
// Add VCard input monitoring for progressive flow
this.setupVCardMonitoring();
}
if (contentTextarea) {
contentTextarea.style.display = 'none';
contentTextarea.removeAttribute('required');
@ -690,6 +719,30 @@ class QRRapidoGenerator {
hintsElement.textContent = langHints[type] || 'Digite o conteúdo apropriado para o tipo selecionado';
}
setupVCardMonitoring() {
// Monitor VCard required fields for progressive flow
const requiredFields = ['vcard-name', 'vcard-mobile', 'vcard-email'];
requiredFields.forEach(fieldId => {
const field = document.getElementById(fieldId);
if (field) {
field.addEventListener('input', () => {
// Use a small delay to allow for validation
setTimeout(() => {
const isValid = this.validateContent(''); // VCard validation is internal
this.contentValid = isValid;
if (isValid && this.selectedType && this.selectedStyle) {
this.showGenerateButton();
} else {
this.hideGenerateButton();
}
}, 100);
});
}
});
}
changeLanguage(e) {
e.preventDefault();
this.currentLang = e.target.dataset.lang;
@ -1085,6 +1138,281 @@ class QRRapidoGenerator {
}, type === 'success' ? 3000 : 5000);
}
initializeProgressiveFlow() {
// Reset all states to initial disabled state
this.selectedType = null;
this.selectedStyle = null;
this.contentValid = false;
// Ensure proper initial state
this.disableStyleSelection();
this.disableContentFields();
this.disableAdvancedOptions();
this.hideGenerateButton();
}
handleTypeSelection(type) {
this.selectedType = type;
if (type) {
// Remove highlight when type is selected
this.removeInitialHighlight();
// Enable style selection
this.enableStyleSelection();
// Reset subsequent selections
this.selectedStyle = null;
this.contentValid = false;
this.disableContentFields();
this.hideGenerateButton();
} else {
// Disable everything if no type selected
this.disableStyleSelection();
this.disableContentFields();
this.disableAdvancedOptions();
this.hideGenerateButton();
}
}
handleStyleSelection(style) {
this.selectedStyle = style;
if (style && this.selectedType) {
// Enable content fields
this.enableContentFields();
// Open advanced panel if 'advanced' style is selected (future enhancement)
if (style === 'advanced') {
this.openAdvancedCustomization();
}
// Reset content validation
this.contentValid = false;
this.hideGenerateButton();
}
}
handleContentChange(content) {
const isValid = this.validateContent(content);
this.contentValid = isValid;
if (isValid && this.selectedType && this.selectedStyle) {
this.showGenerateButton();
} else {
this.hideGenerateButton();
}
}
validateContent(content) {
if (!content) return false;
const trimmedContent = content.trim();
// VCard has its own validation
if (this.selectedType === 'vcard') {
try {
if (window.vcardGenerator) {
const errors = window.vcardGenerator.validateVCardData();
return errors.length === 0;
}
} catch (error) {
return false;
}
return false;
}
// General validation: at least 3 characters
return trimmedContent.length >= 3;
}
enableStyleSelection() {
const styleContainer = document.getElementById('style-selector');
const styleInputs = document.querySelectorAll('input[name="quick-style"]');
const styleLabel = styleContainer?.parentElement.querySelector('.form-label');
if (styleContainer) {
styleContainer.classList.remove('qr-flow-disabled');
styleContainer.classList.add('qr-flow-enabled', 'qr-flow-ascending');
}
if (styleLabel) {
styleLabel.classList.remove('qr-flow-disabled');
styleLabel.classList.add('qr-flow-enabled');
}
styleInputs.forEach(input => {
input.disabled = false;
});
}
disableStyleSelection() {
const styleContainer = document.getElementById('style-selector');
const styleInputs = document.querySelectorAll('input[name="quick-style"]');
const styleLabel = styleContainer?.parentElement.querySelector('.form-label');
if (styleContainer) {
styleContainer.classList.remove('qr-flow-enabled', 'qr-flow-ascending');
styleContainer.classList.add('qr-flow-disabled');
}
if (styleLabel) {
styleLabel.classList.remove('qr-flow-enabled');
styleLabel.classList.add('qr-flow-disabled');
}
styleInputs.forEach(input => {
input.disabled = true;
input.checked = false;
});
// Reset to default selection
const defaultStyle = document.getElementById('style-classic');
if (defaultStyle) {
defaultStyle.checked = true;
}
}
enableContentFields() {
const contentTextarea = document.getElementById('qr-content');
const contentLabel = contentTextarea?.parentElement.querySelector('.form-label');
const vcardInterface = document.getElementById('vcard-interface');
if (contentTextarea) {
contentTextarea.classList.remove('qr-flow-disabled');
contentTextarea.classList.add('qr-flow-enabled', 'qr-flow-ascending');
contentTextarea.disabled = false;
}
if (contentLabel) {
contentLabel.classList.remove('qr-flow-disabled');
contentLabel.classList.add('qr-flow-enabled');
}
if (vcardInterface && this.selectedType === 'vcard') {
vcardInterface.classList.remove('qr-flow-disabled');
vcardInterface.classList.add('qr-flow-enabled', 'qr-flow-ascending');
// Enable VCard form fields
const vcardInputs = vcardInterface.querySelectorAll('input, textarea, select');
vcardInputs.forEach(input => {
input.disabled = false;
});
}
// Enable advanced customization
this.enableAdvancedOptions();
}
disableContentFields() {
const contentTextarea = document.getElementById('qr-content');
const contentLabel = contentTextarea?.parentElement.querySelector('.form-label');
const vcardInterface = document.getElementById('vcard-interface');
if (contentTextarea) {
contentTextarea.classList.remove('qr-flow-enabled', 'qr-flow-ascending');
contentTextarea.classList.add('qr-flow-disabled');
contentTextarea.disabled = true;
contentTextarea.value = '';
}
if (contentLabel) {
contentLabel.classList.remove('qr-flow-enabled');
contentLabel.classList.add('qr-flow-disabled');
}
if (vcardInterface) {
vcardInterface.classList.remove('qr-flow-enabled', 'qr-flow-ascending');
vcardInterface.classList.add('qr-flow-disabled');
// Disable VCard form fields
const vcardInputs = vcardInterface.querySelectorAll('input, textarea, select');
vcardInputs.forEach(input => {
input.disabled = true;
input.value = '';
input.checked = false;
});
}
this.disableAdvancedOptions();
}
enableAdvancedOptions() {
const advancedAccordion = document.getElementById('customization-accordion');
if (advancedAccordion) {
advancedAccordion.classList.remove('qr-flow-disabled');
advancedAccordion.classList.add('qr-flow-enabled', 'qr-flow-ascending');
// Enable form controls inside accordion
const advancedInputs = advancedAccordion.querySelectorAll('input, select');
advancedInputs.forEach(input => {
input.disabled = false;
});
}
}
disableAdvancedOptions() {
const advancedAccordion = document.getElementById('customization-accordion');
if (advancedAccordion) {
advancedAccordion.classList.remove('qr-flow-enabled', 'qr-flow-ascending');
advancedAccordion.classList.add('qr-flow-disabled');
// Disable form controls inside accordion
const advancedInputs = advancedAccordion.querySelectorAll('input, select');
advancedInputs.forEach(input => {
input.disabled = true;
});
}
}
showGenerateButton() {
const buttonContainer = document.getElementById('generate-button-container');
const generateBtn = document.getElementById('generate-btn');
if (buttonContainer) {
buttonContainer.style.display = 'block';
buttonContainer.classList.add('show');
}
if (generateBtn) {
generateBtn.disabled = false;
}
}
hideGenerateButton() {
const buttonContainer = document.getElementById('generate-button-container');
const generateBtn = document.getElementById('generate-btn');
if (buttonContainer) {
buttonContainer.classList.remove('show');
setTimeout(() => {
if (!buttonContainer.classList.contains('show')) {
buttonContainer.style.display = 'none';
}
}, 400);
}
if (generateBtn) {
generateBtn.disabled = true;
}
}
openAdvancedCustomization() {
const advancedPanel = document.getElementById('customization-panel');
const accordionButton = document.querySelector('[data-bs-target="#customization-panel"]');
if (advancedPanel && accordionButton) {
// Use Bootstrap's collapse to open the panel
const collapse = new bootstrap.Collapse(advancedPanel, {
show: true
});
}
}
setupRealTimePreview() {
const contentField = document.getElementById('qr-content');
const typeField = document.getElementById('qr-type');
@ -1105,6 +1433,14 @@ class QRRapidoGenerator {
typeField.addEventListener('change', updatePreview);
}
}
// 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');
}
}
}
// Initialize when DOM loads