feat: fale conosco
All checks were successful
BCards Deployment Pipeline / Run Tests (push) Successful in 4s
BCards Deployment Pipeline / PR Validation (push) Has been skipped
BCards Deployment Pipeline / Build and Push Image (push) Successful in 11m18s
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (push) Has been skipped
BCards Deployment Pipeline / Deploy to Release Swarm (ARM) (push) Successful in 17s
BCards Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Deployment Pipeline / Deployment Summary (push) Successful in 0s
All checks were successful
BCards Deployment Pipeline / Run Tests (push) Successful in 4s
BCards Deployment Pipeline / PR Validation (push) Has been skipped
BCards Deployment Pipeline / Build and Push Image (push) Successful in 11m18s
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (push) Has been skipped
BCards Deployment Pipeline / Deploy to Release Swarm (ARM) (push) Successful in 17s
BCards Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Deployment Pipeline / Deployment Summary (push) Successful in 0s
This commit is contained in:
parent
94c77fc867
commit
b382688a8f
@ -0,0 +1,86 @@
|
||||
using BCards.Web.Areas.Support.Models;
|
||||
using BCards.Web.Areas.Support.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace BCards.Web.Areas.Support.Controllers;
|
||||
|
||||
[Area("Support")]
|
||||
[Route("api/ratings")]
|
||||
[ApiController]
|
||||
public class RatingsController : ControllerBase
|
||||
{
|
||||
private readonly IRatingService _ratingService;
|
||||
private readonly ILogger<RatingsController> _logger;
|
||||
|
||||
public RatingsController(IRatingService ratingService, ILogger<RatingsController> logger)
|
||||
{
|
||||
_ratingService = ratingService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> SubmitRating([FromBody] RatingSubmissionDto dto)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
_logger.LogWarning("Rating inválido submetido: {Errors}", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage));
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
var success = await _ratingService.SubmitRatingAsync(dto, userId, HttpContext);
|
||||
|
||||
if (success)
|
||||
{
|
||||
_logger.LogInformation("Rating de {Stars} estrelas submetido com sucesso", dto.RatingValue);
|
||||
return Ok(new { message = "Avaliação enviada com sucesso! Obrigado pelo feedback." });
|
||||
}
|
||||
|
||||
_logger.LogError("Falha ao submeter rating");
|
||||
return StatusCode(503, new { message = "Erro ao processar sua avaliação. Tente novamente mais tarde." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao processar rating");
|
||||
return StatusCode(500, new { message = "Erro interno ao processar sua avaliação." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("average")]
|
||||
public async Task<IActionResult> GetAverageRating()
|
||||
{
|
||||
try
|
||||
{
|
||||
var average = await _ratingService.GetAverageRatingAsync();
|
||||
var total = await _ratingService.GetTotalCountAsync();
|
||||
|
||||
return Ok(new { average = Math.Round(average, 2), total });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao buscar média de ratings");
|
||||
return StatusCode(500, new { message = "Erro ao buscar avaliações" });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("recent")]
|
||||
public async Task<IActionResult> GetRecentRatings([FromQuery] int limit = 10)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (limit < 1 || limit > 50)
|
||||
limit = 10;
|
||||
|
||||
var ratings = await _ratingService.GetRecentRatingsAsync(limit);
|
||||
return Ok(ratings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao buscar ratings recentes");
|
||||
return StatusCode(500, new { message = "Erro ao buscar avaliações recentes" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
using BCards.Web.Areas.Support.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace BCards.Web.Areas.Support.Controllers;
|
||||
|
||||
[Area("Support")]
|
||||
[Authorize]
|
||||
public class SupportController : Controller
|
||||
{
|
||||
private readonly ISupportService _supportService;
|
||||
private readonly ILogger<SupportController> _logger;
|
||||
|
||||
public SupportController(ISupportService supportService, ILogger<SupportController> logger)
|
||||
{
|
||||
_supportService = supportService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ContactForm()
|
||||
{
|
||||
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
var options = await _supportService.GetAvailableOptionsAsync(userId);
|
||||
|
||||
if (!options.CanUseContactForm)
|
||||
{
|
||||
_logger.LogWarning("Usuário {UserId} tentou acessar formulário sem permissão", userId);
|
||||
TempData["Error"] = "Seu plano atual não tem acesso ao formulário de contato. Faça upgrade para o plano Básico ou superior.";
|
||||
return RedirectToAction("Index", "Home", new { area = "" });
|
||||
}
|
||||
|
||||
return View(options);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Support/Index")]
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
var options = await _supportService.GetAvailableOptionsAsync(userId);
|
||||
|
||||
return View(options);
|
||||
}
|
||||
}
|
||||
24
src/BCards.Web/Areas/Support/Models/Rating.cs
Normal file
24
src/BCards.Web/Areas/Support/Models/Rating.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace BCards.Web.Areas.Support.Models;
|
||||
|
||||
public class Rating
|
||||
{
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
public int RatingValue { get; set; } // 1-5 stars
|
||||
public string? Name { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Comment { get; set; }
|
||||
|
||||
public string? UserId { get; set; } // null para anônimos
|
||||
public string? IpAddress { get; set; }
|
||||
public string? UserAgent { get; set; }
|
||||
public string? Culture { get; set; }
|
||||
public string? Url { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
19
src/BCards.Web/Areas/Support/Models/RatingSubmissionDto.cs
Normal file
19
src/BCards.Web/Areas/Support/Models/RatingSubmissionDto.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BCards.Web.Areas.Support.Models;
|
||||
|
||||
public class RatingSubmissionDto
|
||||
{
|
||||
[Required]
|
||||
[Range(1, 5, ErrorMessage = "A avaliação deve ser entre 1 e 5 estrelas")]
|
||||
public int RatingValue { get; set; }
|
||||
|
||||
[StringLength(100, ErrorMessage = "O nome deve ter no máximo 100 caracteres")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[EmailAddress(ErrorMessage = "Email inválido")]
|
||||
public string? Email { get; set; }
|
||||
|
||||
[StringLength(500, ErrorMessage = "O comentário deve ter no máximo 500 caracteres")]
|
||||
public string? Comment { get; set; }
|
||||
}
|
||||
11
src/BCards.Web/Areas/Support/Models/SupportOptions.cs
Normal file
11
src/BCards.Web/Areas/Support/Models/SupportOptions.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace BCards.Web.Areas.Support.Models;
|
||||
|
||||
public class SupportOptions
|
||||
{
|
||||
public bool CanRate { get; set; }
|
||||
public bool CanUseContactForm { get; set; }
|
||||
public bool CanAccessTelegram { get; set; }
|
||||
public string? TelegramUrl { get; set; }
|
||||
public string? FormspreeUrl { get; set; }
|
||||
public string UserPlan { get; set; } = "Trial";
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
using BCards.Web.Areas.Support.Models;
|
||||
|
||||
namespace BCards.Web.Areas.Support.Repositories;
|
||||
|
||||
public interface IRatingRepository
|
||||
{
|
||||
Task<Rating> CreateAsync(Rating rating);
|
||||
Task<List<Rating>> GetRecentAsync(int limit = 10);
|
||||
Task<double> GetAverageRatingAsync();
|
||||
Task<int> GetTotalCountAsync();
|
||||
Task<List<Rating>> GetByUserIdAsync(string userId);
|
||||
}
|
||||
123
src/BCards.Web/Areas/Support/Repositories/RatingRepository.cs
Normal file
123
src/BCards.Web/Areas/Support/Repositories/RatingRepository.cs
Normal file
@ -0,0 +1,123 @@
|
||||
using BCards.Web.Areas.Support.Models;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace BCards.Web.Areas.Support.Repositories;
|
||||
|
||||
public class RatingRepository : IRatingRepository
|
||||
{
|
||||
private readonly IMongoCollection<Rating> _ratings;
|
||||
private readonly ILogger<RatingRepository> _logger;
|
||||
|
||||
public RatingRepository(IMongoDatabase database, ILogger<RatingRepository> logger)
|
||||
{
|
||||
_ratings = database.GetCollection<Rating>("ratings");
|
||||
_logger = logger;
|
||||
|
||||
// Criar índices
|
||||
CreateIndexes();
|
||||
}
|
||||
|
||||
private void CreateIndexes()
|
||||
{
|
||||
try
|
||||
{
|
||||
var indexKeysDefinition = Builders<Rating>.IndexKeys.Descending(r => r.CreatedAt);
|
||||
var indexModel = new CreateIndexModel<Rating>(indexKeysDefinition);
|
||||
_ratings.Indexes.CreateOne(indexModel);
|
||||
|
||||
var userIdIndexKeys = Builders<Rating>.IndexKeys.Ascending(r => r.UserId);
|
||||
var userIdIndexModel = new CreateIndexModel<Rating>(userIdIndexKeys);
|
||||
_ratings.Indexes.CreateOne(userIdIndexModel);
|
||||
|
||||
var ratingValueIndexKeys = Builders<Rating>.IndexKeys.Ascending(r => r.RatingValue);
|
||||
var ratingValueIndexModel = new CreateIndexModel<Rating>(ratingValueIndexKeys);
|
||||
_ratings.Indexes.CreateOne(ratingValueIndexModel);
|
||||
|
||||
var cultureIndexKeys = Builders<Rating>.IndexKeys.Ascending(r => r.Culture);
|
||||
var cultureIndexModel = new CreateIndexModel<Rating>(cultureIndexKeys);
|
||||
_ratings.Indexes.CreateOne(cultureIndexModel);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Não foi possível criar índices para a collection ratings");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Rating> CreateAsync(Rating rating)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _ratings.InsertOneAsync(rating);
|
||||
_logger.LogInformation("Rating criado com sucesso: {RatingId}", rating.Id);
|
||||
return rating;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao criar rating");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Rating>> GetRecentAsync(int limit = 10)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _ratings
|
||||
.Find(_ => true)
|
||||
.SortByDescending(r => r.CreatedAt)
|
||||
.Limit(limit)
|
||||
.ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao buscar ratings recentes");
|
||||
return new List<Rating>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<double> GetAverageRatingAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var ratings = await _ratings.Find(_ => true).ToListAsync();
|
||||
if (ratings.Count == 0)
|
||||
return 0;
|
||||
|
||||
return ratings.Average(r => r.RatingValue);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao calcular média de ratings");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalCountAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return (int)await _ratings.CountDocumentsAsync(_ => true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao contar ratings");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Rating>> GetByUserIdAsync(string userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _ratings
|
||||
.Find(r => r.UserId == userId)
|
||||
.SortByDescending(r => r.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao buscar ratings do usuário {UserId}", userId);
|
||||
return new List<Rating>();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/BCards.Web/Areas/Support/Services/IRatingService.cs
Normal file
11
src/BCards.Web/Areas/Support/Services/IRatingService.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using BCards.Web.Areas.Support.Models;
|
||||
|
||||
namespace BCards.Web.Areas.Support.Services;
|
||||
|
||||
public interface IRatingService
|
||||
{
|
||||
Task<bool> SubmitRatingAsync(RatingSubmissionDto dto, string? userId, HttpContext httpContext);
|
||||
Task<double> GetAverageRatingAsync();
|
||||
Task<int> GetTotalCountAsync();
|
||||
Task<List<Rating>> GetRecentRatingsAsync(int limit = 10);
|
||||
}
|
||||
8
src/BCards.Web/Areas/Support/Services/ISupportService.cs
Normal file
8
src/BCards.Web/Areas/Support/Services/ISupportService.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using BCards.Web.Areas.Support.Models;
|
||||
|
||||
namespace BCards.Web.Areas.Support.Services;
|
||||
|
||||
public interface ISupportService
|
||||
{
|
||||
Task<SupportOptions> GetAvailableOptionsAsync(string? userId);
|
||||
}
|
||||
61
src/BCards.Web/Areas/Support/Services/RatingService.cs
Normal file
61
src/BCards.Web/Areas/Support/Services/RatingService.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using BCards.Web.Areas.Support.Models;
|
||||
using BCards.Web.Areas.Support.Repositories;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BCards.Web.Areas.Support.Services;
|
||||
|
||||
public class RatingService : IRatingService
|
||||
{
|
||||
private readonly IRatingRepository _repository;
|
||||
private readonly ILogger<RatingService> _logger;
|
||||
|
||||
public RatingService(IRatingRepository repository, ILogger<RatingService> logger)
|
||||
{
|
||||
_repository = repository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> SubmitRatingAsync(RatingSubmissionDto dto, string? userId, HttpContext httpContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
var rating = new Rating
|
||||
{
|
||||
RatingValue = dto.RatingValue,
|
||||
Name = dto.Name,
|
||||
Email = dto.Email,
|
||||
Comment = dto.Comment,
|
||||
UserId = userId,
|
||||
IpAddress = httpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
UserAgent = httpContext.Request.Headers["User-Agent"].ToString(),
|
||||
Culture = CultureInfo.CurrentCulture.Name,
|
||||
Url = httpContext.Request.Headers["Referer"].ToString(),
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _repository.CreateAsync(rating);
|
||||
_logger.LogInformation("Rating submetido com sucesso por usuário {UserId}", userId ?? "anônimo");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao submeter rating");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<double> GetAverageRatingAsync()
|
||||
{
|
||||
return await _repository.GetAverageRatingAsync();
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalCountAsync()
|
||||
{
|
||||
return await _repository.GetTotalCountAsync();
|
||||
}
|
||||
|
||||
public async Task<List<Rating>> GetRecentRatingsAsync(int limit = 10)
|
||||
{
|
||||
return await _repository.GetRecentAsync(limit);
|
||||
}
|
||||
}
|
||||
101
src/BCards.Web/Areas/Support/Services/SupportService.cs
Normal file
101
src/BCards.Web/Areas/Support/Services/SupportService.cs
Normal file
@ -0,0 +1,101 @@
|
||||
using BCards.Web.Areas.Support.Models;
|
||||
using BCards.Web.Configuration;
|
||||
using BCards.Web.Models;
|
||||
using BCards.Web.Repositories;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BCards.Web.Areas.Support.Services;
|
||||
|
||||
public class SupportService : ISupportService
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IOptions<SupportSettings> _settings;
|
||||
private readonly ILogger<SupportService> _logger;
|
||||
|
||||
public SupportService(
|
||||
IUserRepository userRepository,
|
||||
IOptions<SupportSettings> settings,
|
||||
ILogger<SupportService> logger)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_settings = settings;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<SupportOptions> GetAvailableOptionsAsync(string? userId)
|
||||
{
|
||||
var options = new SupportOptions
|
||||
{
|
||||
CanRate = _settings.Value.EnableRatingForAllUsers,
|
||||
CanUseContactForm = false,
|
||||
CanAccessTelegram = false,
|
||||
TelegramUrl = _settings.Value.TelegramUrl,
|
||||
FormspreeUrl = _settings.Value.FormspreeUrl,
|
||||
UserPlan = "Trial"
|
||||
};
|
||||
|
||||
// Usuário não autenticado ou trial
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
{
|
||||
_logger.LogDebug("Usuário não autenticado - apenas rating disponível");
|
||||
return options;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("Usuário {UserId} não encontrado", userId);
|
||||
return options;
|
||||
}
|
||||
|
||||
var planName = user.CurrentPlan?.ToLower() ?? "trial";
|
||||
options.UserPlan = planName;
|
||||
|
||||
_logger.LogDebug("Verificando opções de suporte para usuário {UserId} com plano {Plan}", userId, planName);
|
||||
|
||||
// Trial: apenas rating
|
||||
if (planName == "trial")
|
||||
{
|
||||
_logger.LogDebug("Plano Trial - apenas rating disponível");
|
||||
return options;
|
||||
}
|
||||
|
||||
// Básico: rating + formulário
|
||||
if (planName == "basic" || planName == "básico")
|
||||
{
|
||||
options.CanUseContactForm = true;
|
||||
options.UserPlan = "Básico";
|
||||
_logger.LogDebug("Plano Básico - rating + formulário disponíveis");
|
||||
return options;
|
||||
}
|
||||
|
||||
// Profissional: rating + formulário (sem telegram)
|
||||
if (planName == "professional" || planName == "profissional")
|
||||
{
|
||||
options.CanUseContactForm = true;
|
||||
options.UserPlan = "Profissional";
|
||||
_logger.LogDebug("Plano Profissional - rating + formulário disponíveis");
|
||||
return options;
|
||||
}
|
||||
|
||||
// Premium e PremiumAffiliate: tudo
|
||||
if (planName == "premium" || planName == "premiumaffiliate" || planName == "premium+afiliados")
|
||||
{
|
||||
options.CanUseContactForm = true;
|
||||
options.CanAccessTelegram = true;
|
||||
options.UserPlan = planName.Contains("affiliate") || planName.Contains("afiliados") ? "Premium+Afiliados" : "Premium";
|
||||
_logger.LogDebug("Plano {Plan} - todas as opções disponíveis", planName);
|
||||
return options;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao verificar opções de suporte para usuário {UserId}", userId);
|
||||
return options;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
using BCards.Web.Areas.Support.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace BCards.Web.Areas.Support.ViewComponents;
|
||||
|
||||
public class SupportFabViewComponent : ViewComponent
|
||||
{
|
||||
private readonly ISupportService _supportService;
|
||||
private readonly ILogger<SupportFabViewComponent> _logger;
|
||||
|
||||
public SupportFabViewComponent(ISupportService supportService, ILogger<SupportFabViewComponent> logger)
|
||||
{
|
||||
_supportService = supportService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = UserClaimsPrincipal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
var options = await _supportService.GetAvailableOptionsAsync(userId);
|
||||
|
||||
_logger.LogDebug("SupportFab invocado para usuário {UserId} - Opções: Rating={CanRate}, Form={CanUseContactForm}, Telegram={CanAccessTelegram}",
|
||||
userId ?? "anônimo", options.CanRate, options.CanUseContactForm, options.CanAccessTelegram);
|
||||
|
||||
return View(options);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao carregar SupportFab ViewComponent");
|
||||
return Content(string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,134 @@
|
||||
@model BCards.Web.Areas.Support.Models.SupportOptions
|
||||
|
||||
<!-- Support FAB (Floating Action Button) -->
|
||||
<div class="support-fab-container">
|
||||
<button class="support-fab-trigger" id="supportFabTrigger" aria-label="Precisa de Ajuda?">
|
||||
<i class="fas fa-question-circle"></i>
|
||||
</button>
|
||||
|
||||
<div class="support-fab-menu" id="supportFabMenu" style="display: none;">
|
||||
<div class="support-fab-header">
|
||||
<h6>Precisa de Ajuda?</h6>
|
||||
<p class="small text-muted mb-0">Plano: @Model.UserPlan</p>
|
||||
</div>
|
||||
|
||||
@if (Model.CanAccessTelegram)
|
||||
{
|
||||
<a href="@Model.TelegramUrl" target="_blank" rel="noopener noreferrer" class="support-fab-option support-fab-telegram">
|
||||
<div class="support-fab-option-icon">
|
||||
<i class="fab fa-telegram"></i>
|
||||
</div>
|
||||
<div class="support-fab-option-content">
|
||||
<strong>Falar no Telegram</strong>
|
||||
<small>Suporte prioritário</small>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
|
||||
@if (Model.CanUseContactForm)
|
||||
{
|
||||
<a href="@Url.Action("ContactForm", "Support", new { area = "Support" })" class="support-fab-option support-fab-form">
|
||||
<div class="support-fab-option-icon">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</div>
|
||||
<div class="support-fab-option-content">
|
||||
<strong>Enviar Mensagem</strong>
|
||||
<small>Formulário de contato</small>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
|
||||
@if (Model.CanRate)
|
||||
{
|
||||
<button type="button" class="support-fab-option support-fab-rating" data-bs-toggle="modal" data-bs-target="#ratingModal">
|
||||
<div class="support-fab-option-icon">
|
||||
<i class="fas fa-star"></i>
|
||||
</div>
|
||||
<div class="support-fab-option-content">
|
||||
<strong>Avaliar Serviço</strong>
|
||||
<small>Conte sua experiência</small>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (!Model.CanUseContactForm && !Model.CanAccessTelegram)
|
||||
{
|
||||
<div class="support-fab-upgrade">
|
||||
<p class="small mb-2">
|
||||
<i class="fas fa-lock text-warning"></i>
|
||||
Faça upgrade para acessar mais opções de suporte!
|
||||
</p>
|
||||
<a href="@Url.Action("Index", "Payment", new { area = "" })" class="btn btn-sm btn-primary">Ver Planos</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rating Modal -->
|
||||
<div class="modal fade" id="ratingModal" tabindex="-1" aria-labelledby="ratingModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="ratingModalLabel">Avalie nossa plataforma</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fechar"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="ratingForm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Sua avaliação: <span class="text-danger">*</span></label>
|
||||
<div class="star-rating" id="starRating">
|
||||
<i class="far fa-star" data-rating="1"></i>
|
||||
<i class="far fa-star" data-rating="2"></i>
|
||||
<i class="far fa-star" data-rating="3"></i>
|
||||
<i class="far fa-star" data-rating="4"></i>
|
||||
<i class="far fa-star" data-rating="5"></i>
|
||||
</div>
|
||||
<input type="hidden" id="ratingValue" name="ratingValue" required>
|
||||
<div class="invalid-feedback d-block" id="ratingError" style="display: none !important;">
|
||||
Por favor, selecione uma avaliação
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="ratingName" class="form-label">Nome (opcional)</label>
|
||||
<input type="text" class="form-control" id="ratingName" name="name" maxlength="100" placeholder="Seu nome">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="ratingEmail" class="form-label">Email (opcional)</label>
|
||||
<input type="email" class="form-control" id="ratingEmail" name="email" placeholder="seu@email.com">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="ratingComment" class="form-label">Comentário (opcional)</label>
|
||||
<textarea class="form-control" id="ratingComment" name="comment" rows="3" maxlength="500" placeholder="Conte-nos sobre sua experiência..."></textarea>
|
||||
<small class="form-text text-muted">
|
||||
<span id="commentCounter">0</span>/500 caracteres
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success d-none" id="ratingSuccessAlert" role="alert">
|
||||
<i class="fas fa-check-circle"></i> Avaliação enviada com sucesso! Obrigado pelo feedback.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger d-none" id="ratingErrorAlert" role="alert">
|
||||
<i class="fas fa-exclamation-triangle"></i> <span id="ratingErrorMessage">Erro ao enviar avaliação. Tente novamente.</span>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary" id="submitRatingBtn">
|
||||
<i class="fas fa-paper-plane"></i> Enviar Avaliação
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<link rel="stylesheet" href="~/css/support-fab.css" asp-append-version="true" />
|
||||
<link rel="stylesheet" href="~/css/rating.css" asp-append-version="true" />
|
||||
<script src="~/js/support-fab.js" asp-append-version="true"></script>
|
||||
<script src="~/js/rating.js" asp-append-version="true"></script>
|
||||
}
|
||||
155
src/BCards.Web/Areas/Support/Views/Support/ContactForm.cshtml
Normal file
155
src/BCards.Web/Areas/Support/Views/Support/ContactForm.cshtml
Normal file
@ -0,0 +1,155 @@
|
||||
@model BCards.Web.Areas.Support.Models.SupportOptions
|
||||
@{
|
||||
ViewData["Title"] = "Formulário de Contato";
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<div class="container my-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h2 class="card-title mb-4">
|
||||
<i class="fas fa-envelope text-primary"></i> Fale Conosco
|
||||
</h2>
|
||||
|
||||
<div class="alert alert-info mb-4">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Tempo de resposta:</strong> Normalmente respondemos em até 24-48 horas.
|
||||
</div>
|
||||
|
||||
<form action="@Model.FormspreeUrl" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">
|
||||
Nome <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required maxlength="100">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">
|
||||
Email <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
<small class="form-text text-muted">Usaremos este email para responder sua mensagem.</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="subject" class="form-label">
|
||||
Assunto <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select class="form-select" id="subject" name="subject" required>
|
||||
<option value="">Selecione um assunto</option>
|
||||
<option value="Suporte Técnico">Suporte Técnico</option>
|
||||
<option value="Dúvida sobre Planos">Dúvida sobre Planos</option>
|
||||
<option value="Problema de Pagamento">Problema de Pagamento</option>
|
||||
<option value="Sugestão de Melhoria">Sugestão de Melhoria</option>
|
||||
<option value="Reportar Bug">Reportar Bug</option>
|
||||
<option value="Outro">Outro</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (Model.CanAccessTelegram)
|
||||
{
|
||||
<div class="mb-3">
|
||||
<label for="preferredContact" class="form-label">
|
||||
Canal de contato preferido
|
||||
</label>
|
||||
<select class="form-select" id="preferredContact" name="preferredContact">
|
||||
<option value="email" selected>Email</option>
|
||||
<option value="telegram">Telegram</option>
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="hidden" name="preferredContact" value="email">
|
||||
}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="message" class="form-label">
|
||||
Mensagem <span class="text-danger">*</span>
|
||||
</label>
|
||||
<textarea class="form-control" id="message" name="message" rows="6" required maxlength="2000" placeholder="Descreva sua dúvida ou problema em detalhes..."></textarea>
|
||||
<small class="form-text text-muted">
|
||||
<span id="messageCounter">0</span>/2000 caracteres
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_language" value="pt-BR">
|
||||
<input type="hidden" name="_subject" value="Novo contato BCards">
|
||||
<input type="hidden" name="userPlan" value="@Model.UserPlan">
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
<i class="fas fa-paper-plane"></i> Enviar Mensagem
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if (Model.CanAccessTelegram)
|
||||
{
|
||||
<hr class="my-4">
|
||||
<div class="text-center">
|
||||
<p class="text-muted mb-3">Ou se preferir, entre em contato direto via Telegram:</p>
|
||||
<a href="@Model.TelegramUrl" target="_blank" rel="noopener noreferrer" class="btn btn-outline-primary">
|
||||
<i class="fab fa-telegram"></i> Abrir Telegram
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h4 class="mb-3">Perguntas Frequentes</h4>
|
||||
<div class="accordion" id="faqAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq1">
|
||||
Como faço upgrade do meu plano?
|
||||
</button>
|
||||
</h2>
|
||||
<div id="faq1" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
|
||||
<div class="accordion-body">
|
||||
Você pode fazer upgrade a qualquer momento através da página de <a href="@Url.Action("Index", "Payment", new { area = "" })">Planos e Preços</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq2">
|
||||
Posso cancelar minha assinatura a qualquer momento?
|
||||
</button>
|
||||
</h2>
|
||||
<div id="faq2" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
|
||||
<div class="accordion-body">
|
||||
Sim! Você pode cancelar sua assinatura a qualquer momento sem multas ou taxas adicionais.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq3">
|
||||
Qual o tempo de resposta do suporte?
|
||||
</button>
|
||||
</h2>
|
||||
<div id="faq3" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
|
||||
<div class="accordion-body">
|
||||
Respondemos em até 24-48h para planos Básico. Usuários Premium têm suporte prioritário com resposta em até 12h.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
// Character counter for message
|
||||
document.getElementById('message').addEventListener('input', function() {
|
||||
document.getElementById('messageCounter').textContent = this.value.length;
|
||||
});
|
||||
</script>
|
||||
}
|
||||
90
src/BCards.Web/Areas/Support/Views/Support/Index.cshtml
Normal file
90
src/BCards.Web/Areas/Support/Views/Support/Index.cshtml
Normal file
@ -0,0 +1,90 @@
|
||||
@model BCards.Web.Areas.Support.Models.SupportOptions
|
||||
@{
|
||||
ViewData["Title"] = "Central de Suporte";
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<div class="container my-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<h1 class="mb-4">
|
||||
<i class="fas fa-headset text-primary"></i> Central de Suporte
|
||||
</h1>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>Plano Atual:</strong> @Model.UserPlan
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mt-3">
|
||||
@if (Model.CanAccessTelegram)
|
||||
{
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 text-center">
|
||||
<div class="card-body">
|
||||
<div class="display-4 text-primary mb-3">
|
||||
<i class="fab fa-telegram"></i>
|
||||
</div>
|
||||
<h5 class="card-title">Telegram</h5>
|
||||
<p class="card-text">Fale conosco diretamente pelo Telegram para suporte prioritário.</p>
|
||||
<a href="@Model.TelegramUrl" target="_blank" rel="noopener noreferrer" class="btn btn-primary">
|
||||
Abrir Telegram
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.CanUseContactForm)
|
||||
{
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 text-center">
|
||||
<div class="card-body">
|
||||
<div class="display-4 text-success mb-3">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</div>
|
||||
<h5 class="card-title">Formulário de Contato</h5>
|
||||
<p class="card-text">Envie sua dúvida ou problema através do nosso formulário.</p>
|
||||
<a href="@Url.Action("ContactForm", "Support", new { area = "Support" })" class="btn btn-success">
|
||||
Enviar Mensagem
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.CanRate)
|
||||
{
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 text-center">
|
||||
<div class="card-body">
|
||||
<div class="display-4 text-warning mb-3">
|
||||
<i class="fas fa-star"></i>
|
||||
</div>
|
||||
<h5 class="card-title">Avalie-nos</h5>
|
||||
<p class="card-text">Conte-nos sobre sua experiência com a plataforma.</p>
|
||||
<button type="button" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#ratingModal">
|
||||
Avaliar Agora
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!Model.CanUseContactForm && !Model.CanAccessTelegram)
|
||||
{
|
||||
<div class="alert alert-warning mt-4">
|
||||
<h5><i class="fas fa-lock"></i> Desbloqueie Mais Recursos de Suporte!</h5>
|
||||
<p class="mb-2">Seu plano atual tem acesso limitado. Faça upgrade para:</p>
|
||||
<ul>
|
||||
<li><strong>Plano Básico:</strong> Formulário de contato + Avaliações</li>
|
||||
<li><strong>Plano Premium:</strong> Telegram + Formulário + Suporte Prioritário</li>
|
||||
</ul>
|
||||
<a href="@Url.Action("Index", "Payment", new { area = "" })" class="btn btn-primary mt-2">
|
||||
Ver Planos e Preços
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
10
src/BCards.Web/Configuration/SupportSettings.cs
Normal file
10
src/BCards.Web/Configuration/SupportSettings.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace BCards.Web.Configuration;
|
||||
|
||||
public class SupportSettings
|
||||
{
|
||||
public string TelegramUrl { get; set; } = string.Empty;
|
||||
public string FormspreeUrl { get; set; } = string.Empty;
|
||||
public List<string> EnableTelegramForPlans { get; set; } = new();
|
||||
public List<string> EnableFormForPlans { get; set; } = new();
|
||||
public bool EnableRatingForAllUsers { get; set; } = true;
|
||||
}
|
||||
@ -484,6 +484,14 @@ builder.Services.AddScoped<IDowngradeService, DowngradeService>();
|
||||
|
||||
builder.Services.AddScoped<IImageStorageService, GridFSImageStorage>();
|
||||
|
||||
// Support Area - Rating and Contact System
|
||||
builder.Services.Configure<BCards.Web.Configuration.SupportSettings>(
|
||||
builder.Configuration.GetSection("Support"));
|
||||
|
||||
builder.Services.AddScoped<BCards.Web.Areas.Support.Repositories.IRatingRepository, BCards.Web.Areas.Support.Repositories.RatingRepository>();
|
||||
builder.Services.AddScoped<BCards.Web.Areas.Support.Services.IRatingService, BCards.Web.Areas.Support.Services.RatingService>();
|
||||
builder.Services.AddScoped<BCards.Web.Areas.Support.Services.ISupportService, BCards.Web.Areas.Support.Services.SupportService>();
|
||||
|
||||
// Configure upload limits for file handling (images up to 5MB)
|
||||
builder.Services.Configure<FormOptions>(options =>
|
||||
{
|
||||
@ -721,6 +729,12 @@ if (app.Environment.IsDevelopment())
|
||||
|
||||
app.UseResponseCaching();
|
||||
|
||||
// Support Area Routes
|
||||
app.MapAreaControllerRoute(
|
||||
name: "support-area",
|
||||
areaName: "Support",
|
||||
pattern: "Support/{controller=Support}/{action=Index}/{id?}");
|
||||
|
||||
app.MapControllerRoute(
|
||||
name: "userpage-preview-path",
|
||||
pattern: "page/preview/{category}/{slug}",
|
||||
|
||||
129
src/BCards.Web/Views/Shared/Components/SupportFab/Default.cshtml
Normal file
129
src/BCards.Web/Views/Shared/Components/SupportFab/Default.cshtml
Normal file
@ -0,0 +1,129 @@
|
||||
@model BCards.Web.Areas.Support.Models.SupportOptions
|
||||
|
||||
<!-- Support FAB (Floating Action Button) -->
|
||||
<div class="support-fab-container">
|
||||
<button class="support-fab-trigger" id="supportFabTrigger" aria-label="Precisa de Ajuda?">
|
||||
<i class="fas fa-question-circle"></i>
|
||||
</button>
|
||||
|
||||
<div class="support-fab-menu" id="supportFabMenu" style="display: none;">
|
||||
<div class="support-fab-header">
|
||||
<h6>Precisa de Ajuda?</h6>
|
||||
<p class="small text-muted mb-0">Plano: @Model.UserPlan</p>
|
||||
</div>
|
||||
|
||||
@if (Model.CanAccessTelegram)
|
||||
{
|
||||
<a href="@Model.TelegramUrl" target="_blank" rel="noopener noreferrer" class="support-fab-option support-fab-telegram">
|
||||
<div class="support-fab-option-icon">
|
||||
<i class="fab fa-telegram"></i>
|
||||
</div>
|
||||
<div class="support-fab-option-content">
|
||||
<strong>Falar no Telegram</strong>
|
||||
<small>Suporte prioritário</small>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
|
||||
@if (Model.CanUseContactForm)
|
||||
{
|
||||
<a href="@Url.Action("ContactForm", "Support", new { area = "Support" })" class="support-fab-option support-fab-form">
|
||||
<div class="support-fab-option-icon">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</div>
|
||||
<div class="support-fab-option-content">
|
||||
<strong>Enviar Mensagem</strong>
|
||||
<small>Formulário de contato</small>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
|
||||
@if (Model.CanRate)
|
||||
{
|
||||
<button type="button" class="support-fab-option support-fab-rating" data-bs-toggle="modal" data-bs-target="#ratingModal">
|
||||
<div class="support-fab-option-icon">
|
||||
<i class="fas fa-star"></i>
|
||||
</div>
|
||||
<div class="support-fab-option-content">
|
||||
<strong>Avaliar Serviço</strong>
|
||||
<small>Conte sua experiência</small>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (!Model.CanUseContactForm && !Model.CanAccessTelegram)
|
||||
{
|
||||
<div class="support-fab-upgrade">
|
||||
<p class="small mb-2">
|
||||
<i class="fas fa-lock text-warning"></i>
|
||||
Faça upgrade para acessar mais opções de suporte!
|
||||
</p>
|
||||
<a href="@Url.Action("Index", "Payment", new { area = "" })" class="btn btn-sm btn-primary">Ver Planos</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rating Modal -->
|
||||
<div class="modal fade" id="ratingModal" tabindex="-1" aria-labelledby="ratingModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="ratingModalLabel">Avalie nossa plataforma</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fechar"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="ratingForm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Sua avaliação: <span class="text-danger">*</span></label>
|
||||
<div class="star-rating" id="starRating">
|
||||
<i class="far fa-star" data-rating="1"></i>
|
||||
<i class="far fa-star" data-rating="2"></i>
|
||||
<i class="far fa-star" data-rating="3"></i>
|
||||
<i class="far fa-star" data-rating="4"></i>
|
||||
<i class="far fa-star" data-rating="5"></i>
|
||||
</div>
|
||||
<input type="hidden" id="ratingValue" name="ratingValue" required>
|
||||
<div class="invalid-feedback d-block" id="ratingError" style="display: none !important;">
|
||||
Por favor, selecione uma avaliação
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="ratingName" class="form-label">Nome (opcional)</label>
|
||||
<input type="text" class="form-control" id="ratingName" name="name" maxlength="100" placeholder="Seu nome">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="ratingEmail" class="form-label">Email (opcional)</label>
|
||||
<input type="email" class="form-control" id="ratingEmail" name="email" placeholder="seu@email.com">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="ratingComment" class="form-label">Comentário (opcional)</label>
|
||||
<textarea class="form-control" id="ratingComment" name="comment" rows="3" maxlength="500" placeholder="Conte-nos sobre sua experiência..."></textarea>
|
||||
<small class="form-text text-muted">
|
||||
<span id="commentCounter">0</span>/500 caracteres
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success d-none" id="ratingSuccessAlert" role="alert">
|
||||
<i class="fas fa-check-circle"></i> Avaliação enviada com sucesso! Obrigado pelo feedback.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger d-none" id="ratingErrorAlert" role="alert">
|
||||
<i class="fas fa-exclamation-triangle"></i> <span id="ratingErrorMessage">Erro ao enviar avaliação. Tente novamente.</span>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary" id="submitRatingBtn">
|
||||
<i class="fas fa-paper-plane"></i> Enviar Avaliação
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CSS e JS serão carregados via _Layout.cshtml -->
|
||||
@ -36,6 +36,8 @@
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
||||
<link rel="stylesheet" href="~/css/support-fab.css" asp-append-version="true" />
|
||||
<link rel="stylesheet" href="~/css/rating.css" asp-append-version="true" />
|
||||
<link rel="icon" type="image/x-icon" href="~/favicon.ico" />
|
||||
|
||||
@await RenderSectionAsync("Styles", required: false)
|
||||
@ -207,6 +209,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Support FAB (Floating Action Button) -->
|
||||
@await Component.InvokeAsync("SupportFab")
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -238,10 +243,10 @@
|
||||
@if (User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
<a asp-controller="Admin" asp-action="Dashboard" class="btn btn-primary btn-sm">Comece Agora</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a asp-controller="Auth" asp-action="Login" class="btn btn-primary btn-sm">Comece Agora</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a asp-controller="Auth" asp-action="Login" class="btn btn-primary btn-sm">Comece Agora</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -253,6 +258,8 @@
|
||||
<script src="~/js/site.js" asp-append-version="true"></script>
|
||||
<script src="~/js/cookie-consent.js" asp-append-version="true"></script>
|
||||
<script src="~/js/email-handler.js" asp-append-version="true"></script>
|
||||
<script src="~/js/support-fab.js" asp-append-version="true"></script>
|
||||
<script src="~/js/rating.js" asp-append-version="true"></script>
|
||||
|
||||
<!-- Scripts para menu ativo e barra de carregamento -->
|
||||
<script>
|
||||
|
||||
@ -153,5 +153,12 @@
|
||||
"FromEmail": "ricardo.carneiro@jobmaker.com.br",
|
||||
"FromName": "Ricardo Carneiro"
|
||||
},
|
||||
"BaseUrl": "https://bcards.site"
|
||||
"BaseUrl": "https://bcards.site",
|
||||
"Support": {
|
||||
"TelegramUrl": "https://t.me/jobmakerbr",
|
||||
"FormspreeUrl": "https://formspree.io/f/xpwynqpj",
|
||||
"EnableTelegramForPlans": [ "Premium", "PremiumAffiliate" ],
|
||||
"EnableFormForPlans": [ "Basic", "Professional", "Premium", "PremiumAffiliate" ],
|
||||
"EnableRatingForAllUsers": true
|
||||
}
|
||||
}
|
||||
190
src/BCards.Web/wwwroot/css/rating.css
Normal file
190
src/BCards.Web/wwwroot/css/rating.css
Normal file
@ -0,0 +1,190 @@
|
||||
/* Rating System Styles for BCards */
|
||||
|
||||
/* Star Rating Component */
|
||||
.star-rating {
|
||||
display: inline-flex;
|
||||
gap: 0.5rem;
|
||||
font-size: 2.5rem;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.star-rating i {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.star-rating i:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.star-rating i.fas {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.star-rating i.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;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#ratingModal .modal-title {
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
#ratingModal .btn-close {
|
||||
filter: brightness(0) invert(1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
#ratingModal .btn-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#ratingModal .modal-body {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#ratingForm textarea.form-control {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
/* Submit Button */
|
||||
#submitRatingBtn {
|
||||
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;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#submitRatingBtn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 0.5rem 1rem rgba(102, 126, 234, 0.3);
|
||||
background: linear-gradient(135deg, #5568d3 0%, #6a3f8f 100%);
|
||||
}
|
||||
|
||||
#submitRatingBtn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
#submitRatingBtn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Alert Messages */
|
||||
#ratingSuccessAlert,
|
||||
#ratingErrorAlert {
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem 1.25rem;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
#ratingSuccessAlert {
|
||||
background-color: #d4edda;
|
||||
border-color: #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
#ratingErrorAlert {
|
||||
background-color: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Character Counter */
|
||||
.form-text {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
#commentCounter {
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
/* Invalid Feedback */
|
||||
.invalid-feedback {
|
||||
display: none;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.invalid-feedback.d-block {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
#ratingModal .modal-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
#submitRatingBtn {
|
||||
padding: 0.75rem 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
186
src/BCards.Web/wwwroot/css/support-fab.css
Normal file
186
src/BCards.Web/wwwroot/css/support-fab.css
Normal file
@ -0,0 +1,186 @@
|
||||
/* Support FAB (Floating Action Button) Styles for BCards */
|
||||
|
||||
.support-fab-container {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
z-index: 1050;
|
||||
}
|
||||
|
||||
.support-fab-trigger {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.support-fab-trigger:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.support-fab-trigger:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.support-fab-menu {
|
||||
position: absolute;
|
||||
bottom: 70px;
|
||||
right: 0;
|
||||
min-width: 280px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
padding: 1rem;
|
||||
animation: fadeInUp 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.support-fab-header {
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.support-fab-header h6 {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.support-fab-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
color: #212529;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.support-fab-option:hover {
|
||||
background: #f8f9fa;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.support-fab-option-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.support-fab-telegram .support-fab-option-icon {
|
||||
background: linear-gradient(135deg, #0088cc 0%, #005580 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.support-fab-form .support-fab-option-icon {
|
||||
background: linear-gradient(135deg, #28a745 0%, #1e7e34 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.support-fab-rating .support-fab-option-icon {
|
||||
background: linear-gradient(135deg, #ffc107 0%, #ff9800 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.support-fab-option-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.support-fab-option-content strong {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.support-fab-option-content small {
|
||||
display: block;
|
||||
color: #6c757d;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.support-fab-upgrade {
|
||||
padding: 0.75rem;
|
||||
background: #fff3cd;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.support-fab-upgrade p {
|
||||
color: #856404;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.support-fab-upgrade .btn-sm {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
}
|
||||
|
||||
/* Mobile Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.support-fab-container {
|
||||
bottom: 1.5rem;
|
||||
right: 1.5rem;
|
||||
}
|
||||
|
||||
.support-fab-trigger {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.support-fab-menu {
|
||||
min-width: 260px;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.support-fab-container {
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.support-fab-trigger {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.support-fab-menu {
|
||||
min-width: calc(100vw - 3rem);
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
212
src/BCards.Web/wwwroot/js/rating.js
Normal file
212
src/BCards.Web/wwwroot/js/rating.js
Normal file
@ -0,0 +1,212 @@
|
||||
// Rating System for BCards
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
let selectedRating = 0;
|
||||
|
||||
// Initialize
|
||||
function init() {
|
||||
setupEventListeners();
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
// Star rating clicks
|
||||
const stars = document.querySelectorAll('#starRating i');
|
||||
stars.forEach(star => {
|
||||
star.addEventListener('click', handleStarClick);
|
||||
star.addEventListener('mouseover', handleStarHover);
|
||||
});
|
||||
|
||||
// Star rating container mouse leave
|
||||
const starContainer = document.getElementById('starRating');
|
||||
if (starContainer) {
|
||||
starContainer.addEventListener('mouseleave', resetStarHover);
|
||||
}
|
||||
|
||||
// Form submission
|
||||
const ratingForm = document.getElementById('ratingForm');
|
||||
if (ratingForm) {
|
||||
ratingForm.addEventListener('submit', handleFormSubmit);
|
||||
}
|
||||
|
||||
// Modal reset on close
|
||||
const ratingModal = document.getElementById('ratingModal');
|
||||
if (ratingModal) {
|
||||
ratingModal.addEventListener('hidden.bs.modal', resetForm);
|
||||
}
|
||||
|
||||
// Character counter for comment
|
||||
const commentField = document.getElementById('ratingComment');
|
||||
if (commentField) {
|
||||
commentField.addEventListener('input', updateCharacterCounter);
|
||||
}
|
||||
}
|
||||
|
||||
function handleStarClick(e) {
|
||||
const starValue = parseInt(e.currentTarget.getAttribute('data-rating'));
|
||||
selectedRating = starValue;
|
||||
document.getElementById('ratingValue').value = starValue;
|
||||
updateStars(starValue, true);
|
||||
|
||||
// Hide error if was showing
|
||||
const errorDiv = document.getElementById('ratingError');
|
||||
if (errorDiv) {
|
||||
errorDiv.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function handleStarHover(e) {
|
||||
const starValue = parseInt(e.currentTarget.getAttribute('data-rating'));
|
||||
updateStars(starValue, false);
|
||||
}
|
||||
|
||||
function resetStarHover() {
|
||||
updateStars(selectedRating, true);
|
||||
}
|
||||
|
||||
function updateStars(rating, permanent) {
|
||||
const stars = document.querySelectorAll('#starRating i');
|
||||
stars.forEach(star => {
|
||||
const starValue = parseInt(star.getAttribute('data-rating'));
|
||||
if (starValue <= rating) {
|
||||
star.classList.remove('far');
|
||||
star.classList.add('fas', 'text-warning');
|
||||
} else {
|
||||
star.classList.remove('fas', 'text-warning');
|
||||
star.classList.add('far');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateCharacterCounter() {
|
||||
const commentField = document.getElementById('ratingComment');
|
||||
const counter = document.getElementById('commentCounter');
|
||||
if (commentField && counter) {
|
||||
counter.textContent = commentField.value.length;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Validate rating
|
||||
if (selectedRating === 0) {
|
||||
const errorDiv = document.getElementById('ratingError');
|
||||
if (errorDiv) {
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get form data
|
||||
const formData = {
|
||||
ratingValue: selectedRating,
|
||||
name: document.getElementById('ratingName').value.trim(),
|
||||
email: document.getElementById('ratingEmail').value.trim(),
|
||||
comment: document.getElementById('ratingComment').value.trim()
|
||||
};
|
||||
|
||||
// Get submit button
|
||||
const submitButton = document.getElementById('submitRatingBtn');
|
||||
const originalButtonHtml = submitButton.innerHTML;
|
||||
|
||||
// Disable submit button
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Enviando...';
|
||||
|
||||
// Hide previous alerts
|
||||
document.getElementById('ratingSuccessAlert').classList.add('d-none');
|
||||
document.getElementById('ratingErrorAlert').classList.add('d-none');
|
||||
|
||||
try {
|
||||
// Send to backend
|
||||
const response = await fetch('/api/ratings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// Show success message
|
||||
document.getElementById('ratingSuccessAlert').classList.remove('d-none');
|
||||
|
||||
// Hide form temporarily
|
||||
document.getElementById('ratingForm').querySelectorAll('.mb-3').forEach(el => {
|
||||
el.style.display = 'none';
|
||||
});
|
||||
submitButton.style.display = 'none';
|
||||
|
||||
// Close modal after 3 seconds
|
||||
setTimeout(() => {
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('ratingModal'));
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
}, 3000);
|
||||
} else {
|
||||
throw new Error(data.message || 'Failed to submit rating');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error submitting rating:', error);
|
||||
|
||||
// Show error message
|
||||
const errorAlert = document.getElementById('ratingErrorAlert');
|
||||
const errorMessage = document.getElementById('ratingErrorMessage');
|
||||
errorMessage.textContent = error.message || 'Erro ao enviar avaliação. Por favor, tente novamente.';
|
||||
errorAlert.classList.remove('d-none');
|
||||
|
||||
// Re-enable submit button
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalButtonHtml;
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
// Reset stars
|
||||
selectedRating = 0;
|
||||
document.getElementById('ratingValue').value = '';
|
||||
updateStars(0, true);
|
||||
|
||||
// Reset form fields
|
||||
const ratingForm = document.getElementById('ratingForm');
|
||||
if (ratingForm) {
|
||||
ratingForm.reset();
|
||||
}
|
||||
|
||||
// Show all form elements
|
||||
document.getElementById('ratingForm').querySelectorAll('.mb-3').forEach(el => {
|
||||
el.style.display = 'block';
|
||||
});
|
||||
|
||||
// Hide alerts
|
||||
document.getElementById('ratingSuccessAlert').classList.add('d-none');
|
||||
document.getElementById('ratingErrorAlert').classList.add('d-none');
|
||||
const errorDiv = document.getElementById('ratingError');
|
||||
if (errorDiv) {
|
||||
errorDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
// Re-enable submit button
|
||||
const submitButton = document.getElementById('submitRatingBtn');
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = '<i class="fas fa-paper-plane"></i> Enviar Avaliação';
|
||||
submitButton.style.display = 'block';
|
||||
|
||||
// Reset character counter
|
||||
const counter = document.getElementById('commentCounter');
|
||||
if (counter) {
|
||||
counter.textContent = '0';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
56
src/BCards.Web/wwwroot/js/support-fab.js
Normal file
56
src/BCards.Web/wwwroot/js/support-fab.js
Normal file
@ -0,0 +1,56 @@
|
||||
// Support FAB (Floating Action Button) for BCards
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function initSupportFab() {
|
||||
const fabTrigger = document.getElementById('supportFabTrigger');
|
||||
const fabMenu = document.getElementById('supportFabMenu');
|
||||
|
||||
if (!fabTrigger || !fabMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle menu on trigger click
|
||||
fabTrigger.addEventListener('click', function () {
|
||||
if (fabMenu.style.display === 'none' || fabMenu.style.display === '') {
|
||||
fabMenu.style.display = 'block';
|
||||
fabTrigger.setAttribute('aria-expanded', 'true');
|
||||
} else {
|
||||
fabMenu.style.display = 'none';
|
||||
fabTrigger.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
});
|
||||
|
||||
// Close menu when clicking outside
|
||||
document.addEventListener('click', function (event) {
|
||||
if (!fabTrigger.contains(event.target) && !fabMenu.contains(event.target)) {
|
||||
fabMenu.style.display = 'none';
|
||||
fabTrigger.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
});
|
||||
|
||||
// Close menu on ESC key
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Escape' && fabMenu.style.display === 'block') {
|
||||
fabMenu.style.display = 'none';
|
||||
fabTrigger.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
});
|
||||
|
||||
// Close menu when clicking menu options
|
||||
const menuOptions = fabMenu.querySelectorAll('.support-fab-option');
|
||||
menuOptions.forEach(function (option) {
|
||||
option.addEventListener('click', function () {
|
||||
fabMenu.style.display = 'none';
|
||||
fabTrigger.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initSupportFab);
|
||||
} else {
|
||||
initSupportFab();
|
||||
}
|
||||
})();
|
||||
Loading…
Reference in New Issue
Block a user