diff --git a/Controllers/RatingsController.cs b/Controllers/RatingsController.cs new file mode 100644 index 0000000..63f9be3 --- /dev/null +++ b/Controllers/RatingsController.cs @@ -0,0 +1,120 @@ +using Microsoft.AspNetCore.Mvc; +using QRRapidoApp.Data; +using QRRapidoApp.Models; +using System.Diagnostics; +using System.Security.Claims; + +namespace QRRapidoApp.Controllers +{ + /// + /// Controller for handling user ratings + /// + [Route("api/ratings")] + [ApiController] + public class RatingsController : ControllerBase + { + private readonly MongoDbContext _dbContext; + private readonly ILogger _logger; + + public RatingsController(MongoDbContext dbContext, ILogger logger) + { + _dbContext = dbContext; + _logger = logger; + } + + /// + /// Submit a new rating + /// + [HttpPost] + public async Task SubmitRating([FromBody] RatingSubmissionDto submission) + { + var stopwatch = Stopwatch.StartNew(); + + using (_logger.BeginScope(new Dictionary + { + ["Rating"] = submission.Rating, + ["HasComment"] = !string.IsNullOrWhiteSpace(submission.Comment) + })) + { + _logger.LogInformation("Rating submission - Rating: {Rating}, HasName: {HasName}, HasEmail: {HasEmail}, HasComment: {HasComment}", + submission.Rating, !string.IsNullOrWhiteSpace(submission.Name), !string.IsNullOrWhiteSpace(submission.Email), !string.IsNullOrWhiteSpace(submission.Comment)); + + try + { + // Validate rating value + if (submission.Rating < 1 || submission.Rating > 5) + { + _logger.LogWarning("Invalid rating value - Rating: {Rating}", submission.Rating); + return BadRequest(new { error = "Rating must be between 1 and 5" }); + } + + // Check MongoDB connection + if (!_dbContext.IsConnected || _dbContext.Ratings == null) + { + _logger.LogError("MongoDB not connected or Ratings collection unavailable"); + return StatusCode(503, new { error = "Database unavailable" }); + } + + // Get user ID if authenticated + var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; + + // Get culture from request + var culture = System.Globalization.CultureInfo.CurrentUICulture.Name; + + // Get IP address + var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString(); + + // Create rating object + var rating = new Rating + { + RatingValue = submission.Rating, + Name = submission.Name, + Email = submission.Email, + Comment = submission.Comment, + Url = submission.Url ?? string.Empty, + UserAgent = submission.UserAgent ?? string.Empty, + UserId = userId, + CreatedAt = DateTime.UtcNow, + IpAddress = ipAddress, + Culture = culture + }; + + // Save to MongoDB + await _dbContext.Ratings.InsertOneAsync(rating); + + stopwatch.Stop(); + _logger.LogInformation("Rating saved successfully - RatingId: {RatingId}, Rating: {Rating}, UserId: {UserId}, ProcessingTime: {ProcessingTimeMs}ms", + rating.Id, rating.RatingValue, userId ?? "anonymous", stopwatch.ElapsedMilliseconds); + + return Ok(new + { + success = true, + message = "Thank you for your feedback!", + ratingId = rating.Id + }); + } + catch (Exception ex) + { + stopwatch.Stop(); + _logger.LogError(ex, "Error saving rating - ProcessingTime: {ProcessingTimeMs}ms", + stopwatch.ElapsedMilliseconds); + + return StatusCode(500, new { error = "Error saving rating" }); + } + } + } + } + + /// + /// DTO for rating submission + /// + public class RatingSubmissionDto + { + public int Rating { get; set; } + public string? Name { get; set; } + public string? Email { get; set; } + public string? Comment { get; set; } + public string? Url { get; set; } + public string? UserAgent { get; set; } + } +} diff --git a/Data/MongoDbContext.cs b/Data/MongoDbContext.cs index 79539b8..647530c 100644 --- a/Data/MongoDbContext.cs +++ b/Data/MongoDbContext.cs @@ -35,6 +35,7 @@ namespace QRRapidoApp.Data public IMongoCollection QRCodeHistory => _database.GetCollection("qrCodeHistory"); public IMongoCollection Plans => _database.GetCollection("plans"); public IMongoCollection? AdFreeSessions => _isConnected ? _database?.GetCollection("ad_free_sessions") : null; + public IMongoCollection? Ratings => _isConnected ? _database?.GetCollection("ratings") : null; public IMongoDatabase? Database => _isConnected ? _database : null; public bool IsConnected => _isConnected; @@ -92,6 +93,16 @@ namespace QRRapidoApp.Data ) ) }); + + // Rating indexes + var ratingIndexKeys = Builders.IndexKeys; + await Ratings.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel(ratingIndexKeys.Descending(r => r.CreatedAt)), + new CreateIndexModel(ratingIndexKeys.Ascending(r => r.UserId)), + new CreateIndexModel(ratingIndexKeys.Ascending(r => r.RatingValue)), + new CreateIndexModel(ratingIndexKeys.Ascending(r => r.Culture)) + }); } } } \ No newline at end of file diff --git a/Models/Rating.cs b/Models/Rating.cs new file mode 100644 index 0000000..c1a8b02 --- /dev/null +++ b/Models/Rating.cs @@ -0,0 +1,42 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace QRRapidoApp.Models +{ + public class Rating + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } = string.Empty; + + [BsonElement("rating")] + public int RatingValue { get; set; } // 1-5 stars + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("email")] + public string? Email { get; set; } + + [BsonElement("comment")] + public string? Comment { get; set; } + + [BsonElement("url")] + public string Url { get; set; } = string.Empty; + + [BsonElement("userAgent")] + public string UserAgent { get; set; } = string.Empty; + + [BsonElement("userId")] + public string? UserId { get; set; } // If authenticated + + [BsonElement("createdAt")] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [BsonElement("ipAddress")] + public string? IpAddress { get; set; } + + [BsonElement("culture")] + public string Culture { get; set; } = "pt-BR"; + } +} diff --git a/Resources/SharedResource.es-PY.resx b/Resources/SharedResource.es-PY.resx index 8067577..aa9a548 100644 --- a/Resources/SharedResource.es-PY.resx +++ b/Resources/SharedResource.es-PY.resx @@ -1196,6 +1196,36 @@ Enviar formulario + + Evaluar nuestro servicio + + + ¿Cómo evaluás tu experiencia? + + + Tu nombre + + + Ingresá tu nombre + + + Tu e-mail + + + Tu comentario + + + Contanos más sobre tu experiencia... + + + Enviar Evaluación + + + ¡Gracias por tu evaluación! Tu opinión es muy importante para nosotros. + + + opcional + Contacto con Soporte Premium diff --git a/Resources/SharedResource.pt-BR.resx b/Resources/SharedResource.pt-BR.resx index 286f6d7..3b9c1b2 100644 --- a/Resources/SharedResource.pt-BR.resx +++ b/Resources/SharedResource.pt-BR.resx @@ -1286,6 +1286,36 @@ Enviar formulário + + Avaliar nosso serviço + + + Como você avalia sua experiência? + + + Seu nome + + + Digite seu nome + + + Seu e-mail + + + Seu comentário + + + Conte-nos mais sobre sua experiência... + + + Enviar Avaliação + + + Obrigado pela sua avaliação! Seu feedback é muito importante para nós. + + + opcional + Contato com Suporte Premium diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index 8f3cfde..7df248a 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -186,6 +186,7 @@ + @@ -419,14 +420,13 @@ @await Html.PartialAsync("_CookieConsent") - @if (isPremiumUser) - { - @await Html.PartialAsync("_TelegramPremiumFab") - } + + @await Html.PartialAsync("_TelegramPremiumFab", isPremiumUser) + @if (isDevelopment) { diff --git a/Views/Shared/_TelegramPremiumFab.cshtml b/Views/Shared/_TelegramPremiumFab.cshtml index 973f593..8184513 100644 --- a/Views/Shared/_TelegramPremiumFab.cshtml +++ b/Views/Shared/_TelegramPremiumFab.cshtml @@ -1,8 +1,10 @@ @using Microsoft.Extensions.Localization @inject IStringLocalizer Localizer @inject Microsoft.Extensions.Configuration.IConfiguration Configuration +@model bool @{ + var isPremiumUser = Model; // Receives isPremiumUser from _Layout var telegramUrl = Configuration["Support:TelegramUrl"] ?? "https://t.me/jobmakerbr"; var formConfigured = !string.IsNullOrWhiteSpace(Configuration["Support:FormspreeUrl"]); var formLink = Url.Action("PremiumContact", "Support"); @@ -16,27 +18,44 @@ role="menu" aria-label="@Localizer["PremiumSupportMenuIntro"]" hidden> -

@Localizer["PremiumSupportMenuIntro"]

+ @if (isPremiumUser) + { +

@Localizer["PremiumSupportMenuIntro"]

+ } + else + { +

@Localizer["RateOurService"]

+ } @@ -49,3 +68,66 @@ @Localizer["PremiumSupportFabButton"] + + + diff --git a/wwwroot/css/rating.css b/wwwroot/css/rating.css new file mode 100644 index 0000000..f3f6e3d --- /dev/null +++ b/wwwroot/css/rating.css @@ -0,0 +1,137 @@ +/* Rating System Styles */ + +/* Star Rating Component */ +.star-rating { + display: inline-flex; + gap: 0.5rem; + font-size: 2.5rem; + user-select: none; +} + +.star-rating .star { + cursor: pointer; + transition: all 0.2s ease; + color: #e0e0e0; +} + +.star-rating .star:hover { + transform: scale(1.2); +} + +.star-rating .star.fas { + color: #ffc107; +} + +.star-rating .star.text-warning { + color: #ffc107 !important; +} + +/* Rating Modal Customizations */ +#ratingModal .modal-content { + border-radius: 1rem; + border: none; + box-shadow: 0 0.5rem 2rem rgba(0, 0, 0, 0.2); +} + +#ratingModal .modal-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 1rem 1rem 0 0; + padding: 1.5rem; +} + +#ratingModal .modal-title { + font-weight: 600; + font-size: 1.25rem; +} + +#ratingModal .btn-close { + filter: brightness(0) invert(1); +} + +#ratingModal .modal-body { + padding: 2rem; +} + +/* Rating button styles are in telegram-fab.css */ + +/* Form Styling */ +#ratingForm .form-label { + font-weight: 500; + color: #495057; + margin-bottom: 0.5rem; +} + +#ratingForm .form-control { + border-radius: 0.5rem; + border: 1px solid #dee2e6; + padding: 0.75rem 1rem; + transition: all 0.2s ease; +} + +#ratingForm .form-control:focus { + border-color: #667eea; + box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25); +} + +#ratingForm textarea.form-control { + resize: vertical; + min-height: 100px; +} + +/* Submit Button */ +#submitRating { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 0.5rem; + padding: 0.875rem 1.5rem; + font-weight: 600; + transition: all 0.3s ease; +} + +#submitRating:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 0.5rem 1rem rgba(102, 126, 234, 0.3); +} + +#submitRating:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Success Message */ +#ratingSuccess { + border-radius: 0.75rem; + background-color: #d4edda; + border-color: #c3e6cb; + color: #155724; + padding: 1rem 1.25rem; + animation: slideIn 0.3s ease; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Responsive Adjustments */ +@media (max-width: 576px) { + .star-rating { + font-size: 2rem; + gap: 0.25rem; + } + + #ratingModal .modal-body { + padding: 1.5rem; + } + + #ratingModal .modal-header { + padding: 1rem; + } +} diff --git a/wwwroot/css/telegram-fab.css b/wwwroot/css/telegram-fab.css index b596aae..26972c9 100644 --- a/wwwroot/css/telegram-fab.css +++ b/wwwroot/css/telegram-fab.css @@ -83,44 +83,104 @@ } .support-fab-link { - display: inline-flex; + display: flex; align-items: center; - justify-content: center; - gap: 0.5rem; - border-radius: 999px; - padding: 0.625rem 0.75rem; - transition: transform 0.2s ease, background-color 0.2s ease; + gap: 1rem; + padding: 1rem; + border-radius: 0.75rem; + transition: all 0.3s ease; + text-decoration: none; + color: #fff; + background: none; + border: none; + width: 100%; + text-align: left; + font: inherit; + cursor: pointer; + position: relative; + overflow: hidden; +} + +.support-fab-link::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.05); + opacity: 0; + transition: opacity 0.3s ease; + z-index: 0; +} + +.support-fab-link:hover::before, +.support-fab-link:focus-visible::before { + opacity: 1; } .support-fab-link .icon { - font-size: 1rem; + font-size: 1.25rem; + width: 2.5rem; + height: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.5rem; + flex-shrink: 0; + transition: all 0.3s ease; + z-index: 1; } -.support-fab-link.support-telegram { - background: rgba(59, 130, 246, 0.15); - color: #60a5fa; +.support-fab-link span { + flex: 1; + font-weight: 500; + z-index: 1; + position: relative; } -.support-fab-link.support-telegram:hover, -.support-fab-link.support-telegram:focus-visible { - background: rgba(59, 130, 246, 0.25); +/* Telegram Button */ +.support-fab-link.support-telegram .icon { + background: linear-gradient(135deg, #0088cc 0%, #005580 100%); color: #fff; + box-shadow: 0 4px 12px rgba(0, 136, 204, 0.3); } -.support-fab-link.support-form { - background: rgba(249, 115, 22, 0.15); - color: #f97316; +.support-fab-link.support-telegram:hover .icon, +.support-fab-link.support-telegram:focus-visible .icon { + transform: scale(1.1) rotate(-5deg); + box-shadow: 0 6px 16px rgba(0, 136, 204, 0.5); } -.support-fab-link.support-form:hover, -.support-fab-link.support-form:focus-visible { - background: rgba(249, 115, 22, 0.25); +/* Form Button */ +.support-fab-link.support-form .icon { + background: linear-gradient(135deg, #f97316 0%, #c2410c 100%); color: #fff; + box-shadow: 0 4px 12px rgba(249, 115, 22, 0.3); +} + +.support-fab-link.support-form:hover .icon, +.support-fab-link.support-form:focus-visible .icon { + transform: scale(1.1) rotate(-5deg); + box-shadow: 0 6px 16px rgba(249, 115, 22, 0.5); +} + +/* Rating Button */ +.support-fab-link.support-rating .icon { + background: linear-gradient(135deg, #ffc107 0%, #ff9800 100%); + color: #fff; + box-shadow: 0 4px 12px rgba(255, 193, 7, 0.3); +} + +.support-fab-link.support-rating:hover .icon, +.support-fab-link.support-rating:focus-visible .icon { + transform: scale(1.1) rotate(-5deg); + box-shadow: 0 6px 16px rgba(255, 193, 7, 0.5); } .support-fab-link:hover, .support-fab-link:focus-visible { - transform: translateY(-1px); + transform: translateX(-4px); text-decoration: none; } diff --git a/wwwroot/js/rating.js b/wwwroot/js/rating.js new file mode 100644 index 0000000..9a7e05c --- /dev/null +++ b/wwwroot/js/rating.js @@ -0,0 +1,175 @@ +// Rating System for QR Rapido +(function() { + 'use strict'; + + let selectedRating = 0; + const ratingModal = document.querySelector('[data-rating-modal]'); + const ratingForm = document.getElementById('ratingForm'); + const ratingValue = document.getElementById('ratingValue'); + const successMessage = document.getElementById('ratingSuccess'); + const submitButton = document.getElementById('submitRating'); + + // Initialize + function init() { + setupEventListeners(); + } + + function setupEventListeners() { + // Rating trigger button + const ratingTrigger = document.querySelector('[data-rating-trigger]'); + if (ratingTrigger) { + ratingTrigger.addEventListener('click', openRatingModal); + } + + // Star rating clicks + const stars = document.querySelectorAll('[data-star]'); + stars.forEach(star => { + star.addEventListener('click', handleStarClick); + star.addEventListener('mouseover', handleStarHover); + }); + + // Star rating container mouse leave + const starContainer = document.querySelector('[data-star-rating]'); + if (starContainer) { + starContainer.addEventListener('mouseleave', resetStarHover); + } + + // Form submission + if (ratingForm) { + ratingForm.addEventListener('submit', handleFormSubmit); + } + + // Modal reset on close + if (ratingModal) { + ratingModal.addEventListener('hidden.bs.modal', resetForm); + } + } + + function openRatingModal(e) { + e.preventDefault(); + + // Close support FAB menu + const fabMenu = document.querySelector('[data-support-fab-menu]'); + if (fabMenu) { + fabMenu.hidden = true; + } + + // Open rating modal + const modal = new bootstrap.Modal(ratingModal); + modal.show(); + } + + function handleStarClick(e) { + const starValue = parseInt(e.currentTarget.dataset.star); + selectedRating = starValue; + ratingValue.value = starValue; + updateStars(starValue, true); + } + + function handleStarHover(e) { + const starValue = parseInt(e.currentTarget.dataset.star); + updateStars(starValue, false); + } + + function resetStarHover() { + updateStars(selectedRating, true); + } + + function updateStars(rating, permanent) { + const stars = document.querySelectorAll('[data-star]'); + stars.forEach(star => { + const starValue = parseInt(star.dataset.star); + if (starValue <= rating) { + star.classList.remove('far'); + star.classList.add('fas', 'text-warning'); + } else { + star.classList.remove('fas', 'text-warning'); + star.classList.add('far'); + } + }); + } + + async function handleFormSubmit(e) { + e.preventDefault(); + + // Validate rating + if (selectedRating === 0) { + alert('Por favor, selecione uma avaliação com estrelas.'); + return; + } + + // Get form data + const formData = { + rating: selectedRating, + name: document.getElementById('ratingName').value.trim(), + email: document.getElementById('ratingEmail').value.trim(), + comment: document.getElementById('ratingComment').value.trim(), + url: window.location.href, + userAgent: navigator.userAgent, + timestamp: new Date().toISOString() + }; + + // Disable submit button + submitButton.disabled = true; + submitButton.innerHTML = 'Enviando...'; + + try { + // Send to backend + const response = await fetch('/api/ratings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }); + + if (response.ok) { + // Show success message + ratingForm.classList.add('d-none'); + successMessage.classList.remove('d-none'); + + // Close modal after 3 seconds + setTimeout(() => { + const modal = bootstrap.Modal.getInstance(ratingModal); + if (modal) { + modal.hide(); + } + }, 3000); + } else { + throw new Error('Failed to submit rating'); + } + } catch (error) { + console.error('Error submitting rating:', error); + alert('Erro ao enviar avaliação. Por favor, tente novamente.'); + + // Re-enable submit button + submitButton.disabled = false; + submitButton.innerHTML = 'Enviar Avaliação'; + } + } + + function resetForm() { + // Reset stars + selectedRating = 0; + ratingValue.value = ''; + updateStars(0, true); + + // Reset form fields + ratingForm.reset(); + + // Show form, hide success message + ratingForm.classList.remove('d-none'); + successMessage.classList.add('d-none'); + + // Re-enable submit button + submitButton.disabled = false; + submitButton.innerHTML = 'Enviar Avaliação'; + } + + // Initialize on DOM ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})();