Compare commits
3 Commits
cd9380bdc2
...
174287f5bf
| Author | SHA1 | Date | |
|---|---|---|---|
| 174287f5bf | |||
| 7acd78e2c3 | |||
| 6d5e8dadea |
120
Controllers/RatingsController.cs
Normal file
120
Controllers/RatingsController.cs
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using QRRapidoApp.Data;
|
||||||
|
using QRRapidoApp.Models;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Controller for handling user ratings
|
||||||
|
/// </summary>
|
||||||
|
[Route("api/ratings")]
|
||||||
|
[ApiController]
|
||||||
|
public class RatingsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly MongoDbContext _dbContext;
|
||||||
|
private readonly ILogger<RatingsController> _logger;
|
||||||
|
|
||||||
|
public RatingsController(MongoDbContext dbContext, ILogger<RatingsController> logger)
|
||||||
|
{
|
||||||
|
_dbContext = dbContext;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Submit a new rating
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> SubmitRating([FromBody] RatingSubmissionDto submission)
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
using (_logger.BeginScope(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["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" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DTO for rating submission
|
||||||
|
/// </summary>
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -35,6 +35,7 @@ namespace QRRapidoApp.Data
|
|||||||
public IMongoCollection<QRCodeHistory> QRCodeHistory => _database.GetCollection<QRCodeHistory>("qrCodeHistory");
|
public IMongoCollection<QRCodeHistory> QRCodeHistory => _database.GetCollection<QRCodeHistory>("qrCodeHistory");
|
||||||
public IMongoCollection<Plan> Plans => _database.GetCollection<Plan>("plans");
|
public IMongoCollection<Plan> Plans => _database.GetCollection<Plan>("plans");
|
||||||
public IMongoCollection<AdFreeSession>? AdFreeSessions => _isConnected ? _database?.GetCollection<AdFreeSession>("ad_free_sessions") : null;
|
public IMongoCollection<AdFreeSession>? AdFreeSessions => _isConnected ? _database?.GetCollection<AdFreeSession>("ad_free_sessions") : null;
|
||||||
|
public IMongoCollection<Rating>? Ratings => _isConnected ? _database?.GetCollection<Rating>("ratings") : null;
|
||||||
|
|
||||||
public IMongoDatabase? Database => _isConnected ? _database : null;
|
public IMongoDatabase? Database => _isConnected ? _database : null;
|
||||||
public bool IsConnected => _isConnected;
|
public bool IsConnected => _isConnected;
|
||||||
@ -92,6 +93,16 @@ namespace QRRapidoApp.Data
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Rating indexes
|
||||||
|
var ratingIndexKeys = Builders<Rating>.IndexKeys;
|
||||||
|
await Ratings.Indexes.CreateManyAsync(new[]
|
||||||
|
{
|
||||||
|
new CreateIndexModel<Rating>(ratingIndexKeys.Descending(r => r.CreatedAt)),
|
||||||
|
new CreateIndexModel<Rating>(ratingIndexKeys.Ascending(r => r.UserId)),
|
||||||
|
new CreateIndexModel<Rating>(ratingIndexKeys.Ascending(r => r.RatingValue)),
|
||||||
|
new CreateIndexModel<Rating>(ratingIndexKeys.Ascending(r => r.Culture))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
42
Models/Rating.cs
Normal file
42
Models/Rating.cs
Normal file
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1196,6 +1196,36 @@
|
|||||||
<data name="PremiumSupportOptionForm" xml:space="preserve">
|
<data name="PremiumSupportOptionForm" xml:space="preserve">
|
||||||
<value>Enviar formulario</value>
|
<value>Enviar formulario</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="RateOurService" xml:space="preserve">
|
||||||
|
<value>Evaluar nuestro servicio</value>
|
||||||
|
</data>
|
||||||
|
<data name="RatingQuestion" xml:space="preserve">
|
||||||
|
<value>¿Cómo evaluás tu experiencia?</value>
|
||||||
|
</data>
|
||||||
|
<data name="YourName" xml:space="preserve">
|
||||||
|
<value>Tu nombre</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnterYourName" xml:space="preserve">
|
||||||
|
<value>Ingresá tu nombre</value>
|
||||||
|
</data>
|
||||||
|
<data name="YourEmail" xml:space="preserve">
|
||||||
|
<value>Tu e-mail</value>
|
||||||
|
</data>
|
||||||
|
<data name="YourComment" xml:space="preserve">
|
||||||
|
<value>Tu comentario</value>
|
||||||
|
</data>
|
||||||
|
<data name="TellUsMore" xml:space="preserve">
|
||||||
|
<value>Contanos más sobre tu experiencia...</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendRating" xml:space="preserve">
|
||||||
|
<value>Enviar Evaluación</value>
|
||||||
|
</data>
|
||||||
|
<data name="RatingThanks" xml:space="preserve">
|
||||||
|
<value>¡Gracias por tu evaluación! Tu opinión es muy importante para nosotros.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Optional" xml:space="preserve">
|
||||||
|
<value>opcional</value>
|
||||||
|
</data>
|
||||||
<data name="PremiumSupportFormPageTitle" xml:space="preserve">
|
<data name="PremiumSupportFormPageTitle" xml:space="preserve">
|
||||||
<value>Contacto con Soporte Premium</value>
|
<value>Contacto con Soporte Premium</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@ -1286,6 +1286,36 @@
|
|||||||
<data name="PremiumSupportOptionForm" xml:space="preserve">
|
<data name="PremiumSupportOptionForm" xml:space="preserve">
|
||||||
<value>Enviar formulário</value>
|
<value>Enviar formulário</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="RateOurService" xml:space="preserve">
|
||||||
|
<value>Avaliar nosso serviço</value>
|
||||||
|
</data>
|
||||||
|
<data name="RatingQuestion" xml:space="preserve">
|
||||||
|
<value>Como você avalia sua experiência?</value>
|
||||||
|
</data>
|
||||||
|
<data name="YourName" xml:space="preserve">
|
||||||
|
<value>Seu nome</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnterYourName" xml:space="preserve">
|
||||||
|
<value>Digite seu nome</value>
|
||||||
|
</data>
|
||||||
|
<data name="YourEmail" xml:space="preserve">
|
||||||
|
<value>Seu e-mail</value>
|
||||||
|
</data>
|
||||||
|
<data name="YourComment" xml:space="preserve">
|
||||||
|
<value>Seu comentário</value>
|
||||||
|
</data>
|
||||||
|
<data name="TellUsMore" xml:space="preserve">
|
||||||
|
<value>Conte-nos mais sobre sua experiência...</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendRating" xml:space="preserve">
|
||||||
|
<value>Enviar Avaliação</value>
|
||||||
|
</data>
|
||||||
|
<data name="RatingThanks" xml:space="preserve">
|
||||||
|
<value>Obrigado pela sua avaliação! Seu feedback é muito importante para nós.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Optional" xml:space="preserve">
|
||||||
|
<value>opcional</value>
|
||||||
|
</data>
|
||||||
<data name="PremiumSupportFormPageTitle" xml:space="preserve">
|
<data name="PremiumSupportFormPageTitle" xml:space="preserve">
|
||||||
<value>Contato com Suporte Premium</value>
|
<value>Contato com Suporte Premium</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@ -133,7 +133,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Google Analytics 4 - Optimized with defer -->
|
<!-- Google Analytics 4 - Optimized with defer -->
|
||||||
<script defer src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
|
<script defer src="https://www.googletagmanager.com/gtag/js?id=G-64FCDJGT44"></script>
|
||||||
<script defer>
|
<script defer>
|
||||||
window.dataLayer = window.dataLayer || [];
|
window.dataLayer = window.dataLayer || [];
|
||||||
function gtag(){dataLayer.push(arguments);}
|
function gtag(){dataLayer.push(arguments);}
|
||||||
@ -144,7 +144,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
gtag('js', new Date());
|
gtag('js', new Date());
|
||||||
gtag('config', 'GA_MEASUREMENT_ID', {
|
gtag('config', 'G-64FCDJGT44', {
|
||||||
send_page_view: false
|
send_page_view: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -186,6 +186,7 @@
|
|||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" media="print" onload="this.media='all'">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" media="print" onload="this.media='all'">
|
||||||
<link rel="stylesheet" href="~/css/vendor/fontawesome.min.css" asp-append-version="true" media="print" onload="this.media='all'" />
|
<link rel="stylesheet" href="~/css/vendor/fontawesome.min.css" asp-append-version="true" media="print" onload="this.media='all'" />
|
||||||
<link rel="stylesheet" href="~/css/telegram-fab.css" asp-append-version="true" />
|
<link rel="stylesheet" href="~/css/telegram-fab.css" asp-append-version="true" />
|
||||||
|
<link rel="stylesheet" href="~/css/rating.css" asp-append-version="true" />
|
||||||
|
|
||||||
<!-- Custom CSS - Critical above fold with cache busting -->
|
<!-- Custom CSS - Critical above fold with cache busting -->
|
||||||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
||||||
@ -419,14 +420,13 @@
|
|||||||
<!-- Cookie Consent Banner -->
|
<!-- Cookie Consent Banner -->
|
||||||
@await Html.PartialAsync("_CookieConsent")
|
@await Html.PartialAsync("_CookieConsent")
|
||||||
|
|
||||||
@if (isPremiumUser)
|
<!-- Support FAB - Available for all users, with rating option -->
|
||||||
{
|
@await Html.PartialAsync("_TelegramPremiumFab", isPremiumUser)
|
||||||
@await Html.PartialAsync("_TelegramPremiumFab")
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Bootstrap 5 JS -->
|
<!-- Bootstrap 5 JS -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="~/js/telegram-fab.js" asp-append-version="true" defer></script>
|
<script src="~/js/telegram-fab.js" asp-append-version="true" defer></script>
|
||||||
|
<script src="~/js/rating.js" asp-append-version="true" defer></script>
|
||||||
|
|
||||||
@if (isDevelopment)
|
@if (isDevelopment)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
@using Microsoft.Extensions.Localization
|
@using Microsoft.Extensions.Localization
|
||||||
@inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer
|
@inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer
|
||||||
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
|
@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 telegramUrl = Configuration["Support:TelegramUrl"] ?? "https://t.me/jobmakerbr";
|
||||||
var formConfigured = !string.IsNullOrWhiteSpace(Configuration["Support:FormspreeUrl"]);
|
var formConfigured = !string.IsNullOrWhiteSpace(Configuration["Support:FormspreeUrl"]);
|
||||||
var formLink = Url.Action("PremiumContact", "Support");
|
var formLink = Url.Action("PremiumContact", "Support");
|
||||||
@ -16,27 +18,44 @@
|
|||||||
role="menu"
|
role="menu"
|
||||||
aria-label="@Localizer["PremiumSupportMenuIntro"]"
|
aria-label="@Localizer["PremiumSupportMenuIntro"]"
|
||||||
hidden>
|
hidden>
|
||||||
<p class="support-fab-text">@Localizer["PremiumSupportMenuIntro"]</p>
|
@if (isPremiumUser)
|
||||||
|
{
|
||||||
|
<p class="support-fab-text">@Localizer["PremiumSupportMenuIntro"]</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<p class="support-fab-text">@Localizer["RateOurService"]</p>
|
||||||
|
}
|
||||||
<div class="support-fab-actions">
|
<div class="support-fab-actions">
|
||||||
<a class="support-fab-link support-telegram"
|
@if (isPremiumUser)
|
||||||
href="@telegramUrl"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
role="menuitem">
|
|
||||||
<i class="fab fa-telegram-plane icon" aria-hidden="true"></i>
|
|
||||||
<span>@Localizer["PremiumSupportOptionTelegram"]</span>
|
|
||||||
</a>
|
|
||||||
@if (formEnabled)
|
|
||||||
{
|
{
|
||||||
<a class="support-fab-link support-form"
|
<a class="support-fab-link support-telegram"
|
||||||
href="@formLink"
|
href="@telegramUrl"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
role="menuitem">
|
role="menuitem">
|
||||||
<i class="fas fa-envelope-open-text icon" aria-hidden="true"></i>
|
<i class="fab fa-telegram-plane icon" aria-hidden="true"></i>
|
||||||
<span>@Localizer["PremiumSupportOptionForm"]</span>
|
<span>@Localizer["PremiumSupportOptionTelegram"]</span>
|
||||||
</a>
|
</a>
|
||||||
|
@if (formEnabled)
|
||||||
|
{
|
||||||
|
<a class="support-fab-link support-form"
|
||||||
|
href="@formLink"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
role="menuitem">
|
||||||
|
<i class="fas fa-envelope-open-text icon" aria-hidden="true"></i>
|
||||||
|
<span>@Localizer["PremiumSupportOptionForm"]</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
<button type="button"
|
||||||
|
class="support-fab-link support-rating"
|
||||||
|
data-rating-trigger
|
||||||
|
role="menuitem">
|
||||||
|
<i class="fas fa-star icon" aria-hidden="true"></i>
|
||||||
|
<span>@Localizer["RateOurService"]</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -49,3 +68,66 @@
|
|||||||
<span class="fab-trigger-text">@Localizer["PremiumSupportFabButton"]</span>
|
<span class="fab-trigger-text">@Localizer["PremiumSupportFabButton"]</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Rating Modal -->
|
||||||
|
<div class="modal fade" id="ratingModal" tabindex="-1" aria-labelledby="ratingModalLabel" aria-hidden="true" data-rating-modal>
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header border-0">
|
||||||
|
<h5 class="modal-title w-100 text-center" id="ratingModalLabel">
|
||||||
|
<i class="fas fa-star text-warning me-2"></i>
|
||||||
|
@Localizer["RateOurService"]
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="@Localizer["Close"]"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="ratingForm">
|
||||||
|
<!-- Star Rating -->
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<p class="mb-3">@Localizer["RatingQuestion"]</p>
|
||||||
|
<div class="star-rating" data-star-rating>
|
||||||
|
<i class="far fa-star star" data-star="1"></i>
|
||||||
|
<i class="far fa-star star" data-star="2"></i>
|
||||||
|
<i class="far fa-star star" data-star="3"></i>
|
||||||
|
<i class="far fa-star star" data-star="4"></i>
|
||||||
|
<i class="far fa-star star" data-star="5"></i>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="rating" id="ratingValue" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Name Field -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ratingName" class="form-label">@Localizer["YourName"] <span class="text-muted">(@Localizer["Optional"])</span></label>
|
||||||
|
<input type="text" class="form-control" id="ratingName" name="name" placeholder="@Localizer["EnterYourName"]">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Field -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ratingEmail" class="form-label">@Localizer["YourEmail"] <span class="text-muted">(@Localizer["Optional"])</span></label>
|
||||||
|
<input type="email" class="form-control" id="ratingEmail" name="email" placeholder="nome@exemplo.com">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Comment Field -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ratingComment" class="form-label">@Localizer["YourComment"] <span class="text-muted">(@Localizer["Optional"])</span></label>
|
||||||
|
<textarea class="form-control" id="ratingComment" name="comment" rows="3" placeholder="@Localizer["TellUsMore"]"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg" id="submitRating">
|
||||||
|
<i class="fas fa-paper-plane me-2"></i>
|
||||||
|
@Localizer["SendRating"]
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Success Message -->
|
||||||
|
<div class="alert alert-success mt-3 d-none" id="ratingSuccess">
|
||||||
|
<i class="fas fa-check-circle me-2"></i>
|
||||||
|
@Localizer["RatingThanks"]
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
137
wwwroot/css/rating.css
Normal file
137
wwwroot/css/rating.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -83,44 +83,104 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.support-fab-link {
|
.support-fab-link {
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 1rem;
|
||||||
gap: 0.5rem;
|
padding: 1rem;
|
||||||
border-radius: 999px;
|
border-radius: 0.75rem;
|
||||||
padding: 0.625rem 0.75rem;
|
transition: all 0.3s ease;
|
||||||
transition: transform 0.2s ease, background-color 0.2s 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 {
|
.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 {
|
.support-fab-link span {
|
||||||
background: rgba(59, 130, 246, 0.15);
|
flex: 1;
|
||||||
color: #60a5fa;
|
font-weight: 500;
|
||||||
|
z-index: 1;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.support-fab-link.support-telegram:hover,
|
/* Telegram Button */
|
||||||
.support-fab-link.support-telegram:focus-visible {
|
.support-fab-link.support-telegram .icon {
|
||||||
background: rgba(59, 130, 246, 0.25);
|
background: linear-gradient(135deg, #0088cc 0%, #005580 100%);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 136, 204, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.support-fab-link.support-form {
|
.support-fab-link.support-telegram:hover .icon,
|
||||||
background: rgba(249, 115, 22, 0.15);
|
.support-fab-link.support-telegram:focus-visible .icon {
|
||||||
color: #f97316;
|
transform: scale(1.1) rotate(-5deg);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 136, 204, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.support-fab-link.support-form:hover,
|
/* Form Button */
|
||||||
.support-fab-link.support-form:focus-visible {
|
.support-fab-link.support-form .icon {
|
||||||
background: rgba(249, 115, 22, 0.25);
|
background: linear-gradient(135deg, #f97316 0%, #c2410c 100%);
|
||||||
color: #fff;
|
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:hover,
|
||||||
.support-fab-link:focus-visible {
|
.support-fab-link:focus-visible {
|
||||||
transform: translateY(-1px);
|
transform: translateX(-4px);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
175
wwwroot/js/rating.js
Normal file
175
wwwroot/js/rating.js
Normal file
@ -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 = '<span class="spinner-border spinner-border-sm me-2"></span>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 = '<i class="fas fa-paper-plane me-2"></i>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 = '<i class="fas fa-paper-plane me-2"></i>Enviar Avaliação';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on DOM ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
Loading…
Reference in New Issue
Block a user