Compare commits
No commits in common. "174287f5bf80d69de7bbbcd9e37ae262c41247d5" and "cd9380bdc2c95461ed0609207135c238510be9b0" have entirely different histories.
174287f5bf
...
cd9380bdc2
@ -1,120 +0,0 @@
|
|||||||
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,7 +35,6 @@ 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;
|
||||||
@ -93,16 +92,6 @@ 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))
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,42 +0,0 @@
|
|||||||
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,36 +1196,6 @@
|
|||||||
<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,36 +1286,6 @@
|
|||||||
<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=G-64FCDJGT44"></script>
|
<script defer src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></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', 'G-64FCDJGT44', {
|
gtag('config', 'GA_MEASUREMENT_ID', {
|
||||||
send_page_view: false
|
send_page_view: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -186,7 +186,6 @@
|
|||||||
<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" />
|
||||||
@ -420,13 +419,14 @@
|
|||||||
<!-- Cookie Consent Banner -->
|
<!-- Cookie Consent Banner -->
|
||||||
@await Html.PartialAsync("_CookieConsent")
|
@await Html.PartialAsync("_CookieConsent")
|
||||||
|
|
||||||
<!-- Support FAB - Available for all users, with rating option -->
|
@if (isPremiumUser)
|
||||||
@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,10 +1,8 @@
|
|||||||
@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");
|
||||||
@ -18,44 +16,27 @@
|
|||||||
role="menu"
|
role="menu"
|
||||||
aria-label="@Localizer["PremiumSupportMenuIntro"]"
|
aria-label="@Localizer["PremiumSupportMenuIntro"]"
|
||||||
hidden>
|
hidden>
|
||||||
@if (isPremiumUser)
|
<p class="support-fab-text">@Localizer["PremiumSupportMenuIntro"]</p>
|
||||||
{
|
|
||||||
<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">
|
||||||
@if (isPremiumUser)
|
<a class="support-fab-link support-telegram"
|
||||||
|
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-telegram"
|
<a class="support-fab-link support-form"
|
||||||
href="@telegramUrl"
|
href="@formLink"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
role="menuitem">
|
role="menuitem">
|
||||||
<i class="fab fa-telegram-plane icon" aria-hidden="true"></i>
|
<i class="fas fa-envelope-open-text icon" aria-hidden="true"></i>
|
||||||
<span>@Localizer["PremiumSupportOptionTelegram"]</span>
|
<span>@Localizer["PremiumSupportOptionForm"]</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>
|
||||||
|
|
||||||
@ -68,66 +49,3 @@
|
|||||||
<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>
|
|
||||||
|
|||||||
@ -1,137 +0,0 @@
|
|||||||
/* 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,104 +83,44 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.support-fab-link {
|
.support-fab-link {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
justify-content: center;
|
||||||
padding: 1rem;
|
gap: 0.5rem;
|
||||||
border-radius: 0.75rem;
|
border-radius: 999px;
|
||||||
transition: all 0.3s ease;
|
padding: 0.625rem 0.75rem;
|
||||||
text-decoration: none;
|
transition: transform 0.2s ease, background-color 0.2s ease;
|
||||||
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: 1.25rem;
|
font-size: 1rem;
|
||||||
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 span {
|
.support-fab-link.support-telegram {
|
||||||
flex: 1;
|
background: rgba(59, 130, 246, 0.15);
|
||||||
font-weight: 500;
|
color: #60a5fa;
|
||||||
z-index: 1;
|
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Telegram Button */
|
.support-fab-link.support-telegram:hover,
|
||||||
.support-fab-link.support-telegram .icon {
|
.support-fab-link.support-telegram:focus-visible {
|
||||||
background: linear-gradient(135deg, #0088cc 0%, #005580 100%);
|
background: rgba(59, 130, 246, 0.25);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
box-shadow: 0 4px 12px rgba(0, 136, 204, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.support-fab-link.support-telegram:hover .icon,
|
.support-fab-link.support-form {
|
||||||
.support-fab-link.support-telegram:focus-visible .icon {
|
background: rgba(249, 115, 22, 0.15);
|
||||||
transform: scale(1.1) rotate(-5deg);
|
color: #f97316;
|
||||||
box-shadow: 0 6px 16px rgba(0, 136, 204, 0.5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form Button */
|
.support-fab-link.support-form:hover,
|
||||||
.support-fab-link.support-form .icon {
|
.support-fab-link.support-form:focus-visible {
|
||||||
background: linear-gradient(135deg, #f97316 0%, #c2410c 100%);
|
background: rgba(249, 115, 22, 0.25);
|
||||||
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: translateX(-4px);
|
transform: translateY(-1px);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,175 +0,0 @@
|
|||||||
// 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